From fa29adaba3a3b1895714dc8287f8b8d47b693163 Mon Sep 17 00:00:00 2001 From: pikann22 Date: Fri, 5 Jun 2026 04:35:17 +0000 Subject: [PATCH 1/2] feat: add interactive install script and deployment assets for GitHub Releases - Introduced `install.sh` for easy setup of Paca, allowing users to install without cloning the repository. - Updated `cd.yml` to upload deployment assets (install.sh, docker-compose.yml, gateway.conf) to GitHub Releases. - Enhanced `README.md` to include instructions for using the install script and manual setup. - Modified `docker-compose.prod.yml` for standalone deployment without local source tree, pulling images from DockerHub. --- .github/workflows/cd.yml | 45 +++ deploy/README.md | 154 +++++---- deploy/docker-compose.prod.yml | 198 ++++++------ scripts/install.sh | 565 +++++++++++++++++++++++++++++++++ 4 files changed, 790 insertions(+), 172 deletions(-) create mode 100755 scripts/install.sh diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 6cbf2e7d..a2034753 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -15,6 +15,10 @@ name: cd # /paca-ai-agent: — AI Agent service Docker image # registry.npmjs.org @paca-ai/paca-mcp — MCP server npm package # registry.npmjs.org @paca-ai/plugin-sdk-react — Plugin frontend SDK npm package +# GitHub Release assets: +# install.sh — one-shot interactive install script +# docker-compose.yml — standalone compose (no source tree required) +# gateway.conf — nginx gateway configuration on: release: @@ -305,3 +309,44 @@ jobs: else npm publish --provenance --access public fi + + # ───────────────────────────────────────────────────────────────────────────── + # Deployment assets → GitHub Release + # + # Uploads three files so that users can run Paca without cloning the repo: + # install.sh — interactive setup wizard (download + configure + start) + # docker-compose.yml — standalone compose referencing pre-built DockerHub images + # gateway.conf — nginx gateway configuration required by the compose file + # + # End-users download and run: + # curl -fsSL https://github.com/Paca-AI/paca/releases/latest/download/install.sh -o install.sh + # bash install.sh + # ───────────────────────────────────────────────────────────────────────────── + release-assets: + name: Upload deployment assets to release + runs-on: ubuntu-latest + needs: [api-image, realtime-image, web-image, ai-agent-image, publish-mcp] + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Prepare assets + run: | + # Rename files to the names end-users will download. + cp deploy/docker-compose.prod.yml docker-compose.yml + cp deploy/nginx/gateway.conf gateway.conf + cp scripts/install.sh install.sh + chmod +x install.sh + + - name: Upload assets to GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release upload "${{ github.event.release.tag_name }}" \ + install.sh \ + docker-compose.yml \ + gateway.conf \ + --clobber diff --git a/deploy/README.md b/deploy/README.md index 4318245e..55fecc8e 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -3,24 +3,106 @@ This directory contains deployment assets for two distinct use cases: - contributor-friendly local development; -- production-oriented container deployment examples. - -Keeping those concerns separate makes the repository easier to understand and avoids presenting a local-only stack as a production recommendation. +- production container deployment for self-hosters. ## Contents | File | Description | |---|---| | `docker-compose.dev.yml` | Local development stack: PostgreSQL, Valkey, MinIO, and optional app containers | -| `docker-compose.prod.yml` | Production-oriented single-host stack: web, API, PostgreSQL, Valkey, and MinIO | +| `docker-compose.prod.yml` | Production stack: pulls pre-built images from DockerHub, no source checkout required | | `docker-compose.e2e.yml` | End-to-end test stack mirroring production topology with fixed test credentials | | `.env.dev.example` | Optional environment file for `docker-compose.dev.yml` (tunnel / custom domain) | -| `.env.production.example` | Example environment file for `docker-compose.prod.yml` | +| `.env.production.example` | Example environment file for manual production deployments | Service container definitions live with each service: - [`services/api/Dockerfile`](../services/api/Dockerfile) - [`apps/web/Dockerfile`](../apps/web/Dockerfile) +## Production Deployment + +### Recommended: install script + +The easiest way to run Paca without cloning the repository is via the install script +published with each release. It downloads the compose file and nginx config, walks you +through configuration interactively (database, storage, AI agent), generates a `.env` +with strong random secrets, and starts the stack. + +```bash +curl -fsSL https://github.com/Paca-AI/paca/releases/latest/download/install.sh -o install.sh +bash install.sh +``` + +The installer supports: + +| Option | Description | +|---|---| +| Bundled PostgreSQL | Starts a postgres container (default) | +| External PostgreSQL | Supply a `DATABASE_URL`; postgres container is suppressed | +| Self-hosted MinIO | Starts a MinIO container for S3-compatible file storage (default) | +| AWS S3 | Supply AWS credentials; MinIO container is suppressed | +| AI Agent | Enabled by default; can be skipped to reduce resource usage | + +### Manual setup + +Download the two required files from the latest release: + +```bash +mkdir -p paca/nginx && cd paca +curl -fsSL https://github.com/Paca-AI/paca/releases/latest/download/docker-compose.yml -o docker-compose.yml +curl -fsSL https://github.com/Paca-AI/paca/releases/latest/download/gateway.conf -o nginx/gateway.conf +``` + +Create your environment file: + +```bash +cp /path/to/repo/deploy/.env.production.example .env +# Edit .env — set JWT_SECRET, ADMIN_PASSWORD, POSTGRES_PASSWORD, storage credentials, etc. +``` + +Start the full stack (bundled PostgreSQL + MinIO): + +```bash +docker compose --env-file .env up -d +``` + +**With external PostgreSQL** (suppress the bundled container): + +```bash +# Set DATABASE_URL in .env to your managed connection string. +docker compose --env-file .env up -d --scale postgres=0 +``` + +**With AWS S3** (suppress MinIO): + +```bash +# Set STORAGE_PROVIDER=s3 and real AWS credentials in .env. +docker compose --env-file .env up -d --scale minio=0 +``` + +**Without the AI agent**: + +```bash +docker compose --env-file .env up -d --scale ai-agent=0 +``` + +Flags can be combined: + +```bash +docker compose --env-file .env up -d --scale postgres=0 --scale minio=0 +``` + +### Pinning a release version + +Set the image variables in `.env` to lock to a specific release: + +```bash +PACA_API_IMAGE=pacaai/paca-api:1.2.3 +PACA_WEB_IMAGE=pacaai/paca-web:1.2.3 +PACA_REALTIME_IMAGE=pacaai/paca-realtime:1.2.3 +PACA_AI_AGENT_IMAGE=pacaai/paca-ai-agent:1.2.3 +``` + ## Development Compose Use [`docker-compose.dev.yml`](./docker-compose.dev.yml) for local development and contributor onboarding. @@ -45,9 +127,8 @@ Start only shared dependencies: docker compose -f deploy/docker-compose.dev.yml up -d postgres valkey ``` -For day-to-day coding, contributors can still run the application services directly on the host and use Docker Compose only for PostgreSQL and Valkey. - -The Postgres schema is applied automatically on the first container start from `services/api/migrations/`. +For day-to-day coding, contributors can run the application services directly on the host +and use Docker Compose only for PostgreSQL and Valkey. ### Development service ports @@ -72,46 +153,10 @@ Remove the Postgres volume as well: docker compose -f deploy/docker-compose.dev.yml down -v ``` -## Production Compose - -Use [`docker-compose.prod.yml`](./docker-compose.prod.yml) as a self-hosting baseline for open-source deployments. - -The production compose includes PostgreSQL and Valkey because a public repository should offer a runnable end-to-end deployment path. It is still a single-host baseline rather than a universal recommendation. Teams using managed services can keep the same application images and point the runtime configuration at external infrastructure instead. - -Create a production environment file from the example: - -```bash -cp deploy/.env.production.example deploy/.env.production -``` - -Then run: - -**With MinIO (default self-hosted):** -```bash -docker compose \ - --env-file deploy/.env.production \ - -f deploy/docker-compose.prod.yml up -d --build -``` - -**With AWS S3 (suppress MinIO):** -```bash -# Set STORAGE_PROVIDER=s3 and real AWS credentials in .env.production -docker compose \ - --env-file deploy/.env.production \ - -f deploy/docker-compose.prod.yml up -d --build --scale minio=0 -``` - -This file is suitable as: - -- a self-hosting starting point; -- a CI/CD handoff artifact; -- a reference for container image names and required runtime configuration. - -By default, the web and API services are published to the host in the production compose. PostgreSQL, Valkey, and MinIO stay on the internal Compose network unless an operator intentionally exposes them. - ## E2E Compose -Use [`docker-compose.e2e.yml`](./docker-compose.e2e.yml) to spin up a full production-like stack with fixed, test-safe credentials for running end-to-end tests: +Use [`docker-compose.e2e.yml`](./docker-compose.e2e.yml) to spin up a full production-like +stack with fixed, test-safe credentials for running end-to-end tests: ```bash docker compose -f deploy/docker-compose.e2e.yml up -d --build --wait @@ -119,20 +164,3 @@ docker compose -f deploy/docker-compose.e2e.yml down -v ``` All secrets are intentionally weak and public — never use them outside a local E2E environment. - -## Object Storage - -All environments include MinIO, an S3-compatible object store, to support file attachments without requiring an AWS account. - -In production, MinIO runs by default. When using AWS S3, pass `--scale minio=0` to suppress the MinIO container: - -```bash -docker compose --env-file deploy/.env.production \ - -f deploy/docker-compose.prod.yml up -d --build --scale minio=0 -``` - -To switch to AWS S3: -1. Set `STORAGE_PROVIDER=s3` in `.env.production`. -2. Leave `STORAGE_ENDPOINT` empty (uses the default AWS regional endpoint) or set it explicitly. -3. Supply real `STORAGE_ACCESS_KEY_ID` and `STORAGE_SECRET_ACCESS_KEY`. -4. Add `--scale minio=0` to the startup command. \ No newline at end of file diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml index 7d503a1c..8d76d859 100644 --- a/deploy/docker-compose.prod.yml +++ b/deploy/docker-compose.prod.yml @@ -1,71 +1,71 @@ -# Production-oriented single-host compose example. +# Standalone production compose — no local source tree required. # -# This file is a self-hosting baseline for open-source users who want one -# deployable stack with the application and its stateful dependencies. Teams -# running managed PostgreSQL or Valkey can still use the same images -# and environment variables as a handoff into their own platform. +# Designed to be downloaded and run without cloning the repository. +# All application images are pulled from DockerHub. Database migrations are +# applied automatically by the API service at startup (embedded SQL files). # -# ── Pre-built images ────────────────────────────────────────────────────────── -# By default this file pulls the official pre-built images from DockerHub. -# To pin a specific release, set the IMAGE env vars: +# ── Quick start ─────────────────────────────────────────────────────────────── +# The recommended way to set this up is via the install script: # -# PACA_API_IMAGE=pacaai/paca-api:1.2.3 -# PACA_WEB_IMAGE=pacaai/paca-web:1.2.3 -# PACA_REALTIME_IMAGE=pacaai/paca-realtime:1.2.3 +# curl -fsSL https://github.com/Paca-AI/paca/releases/latest/download/install.sh -o install.sh +# bash install.sh # -# To build the images locally instead of pulling (e.g. with custom patches), -# append --build to the docker compose up command. +# ── Manual setup ────────────────────────────────────────────────────────────── +# 1. Download the nginx config alongside this file: +# mkdir -p nginx +# curl -fsSL https://github.com/Paca-AI/paca/releases/latest/download/gateway.conf -o nginx/gateway.conf # -# ── Object storage ──────────────────────────────────────────────────────────── -# MinIO runs by default (self-hosted). To use AWS S3 instead, scale MinIO to -# zero so it does not start, and supply real AWS credentials. +# 2. Create a .env file (see .env variables below). # -# Self-hosted with MinIO (default): -# cp deploy/.env.production.example deploy/.env.production -# docker compose \ -# --env-file deploy/.env.production \ -# -f deploy/docker-compose.prod.yml up -d +# 3. Start the stack: +# docker compose --env-file .env up -d # -# With AWS S3 (MinIO container suppressed): -# # Set STORAGE_PROVIDER=s3 and real AWS credentials in .env.production -# docker compose \ -# --env-file deploy/.env.production \ -# -f deploy/docker-compose.prod.yml up -d --scale minio=0 +# ── Image pinning ───────────────────────────────────────────────────────────── +# Set these in .env to pin a specific release: +# PACA_API_IMAGE=pacaai/paca-api:1.2.3 +# PACA_WEB_IMAGE=pacaai/paca-web:1.2.3 +# PACA_REALTIME_IMAGE=pacaai/paca-realtime:1.2.3 +# PACA_AI_AGENT_IMAGE=pacaai/paca-ai-agent:1.2.3 # -# ── Database ──────────────────────────────────────────────────────────────── -# PostgreSQL runs by default (self-hosted). +# ── External PostgreSQL ──────────────────────────────────────────────────────── +# Set DATABASE_URL to your managed connection string and add --scale postgres=0 +# to suppress the bundled container: +# docker compose --env-file .env up -d --scale postgres=0 # -# Self-hosted with PostgreSQL (default): -# # Set POSTGRES_PASSWORD or use the default -# docker compose \ -# --env-file deploy/.env.production \ -# -f deploy/docker-compose.prod.yml up -d +# ── AWS S3 instead of MinIO ─────────────────────────────────────────────────── +# Set STORAGE_PROVIDER=s3 with real AWS credentials and add --scale minio=0: +# docker compose --env-file .env up -d --scale minio=0 # -# With managed/external PostgreSQL (postgres container suppressed): -# # Set DATABASE_URL to your managed PostgreSQL connection string in .env.production -# docker compose \ -# --env-file deploy/.env.production \ -# -f deploy/docker-compose.prod.yml up -d --scale postgres=0 +# ── Skip AI agent ───────────────────────────────────────────────────────────── +# docker compose --env-file .env up -d --scale ai-agent=0 +# +# ── External / CDN-hosted web app (e.g. deployed to S3 + CloudFront) ────────── +# Suppress the web container and serve the SPA from your CDN instead. +# The gateway still handles /api/, /ws/, and /storage/ routes. +# docker compose --env-file .env up -d --scale web=0 -name: paca-prod +name: paca services: + # ── PostgreSQL ────────────────────────────────────────────────────────────── + # Suppress with --scale postgres=0 when using an external managed database. postgres: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB:-paca} POSTGRES_USER: ${POSTGRES_USER:-paca} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + # WARNING: override this with a strong password in your .env file. + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme} volumes: - postgres_data:/var/lib/postgresql/data - - ../services/api/migrations:/docker-entrypoint-initdb.d:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-paca} -d ${POSTGRES_DB:-paca}"] interval: 10s timeout: 5s retries: 10 + # ── Valkey (Redis-compatible cache / pub-sub) ──────────────────────────────── valkey: image: valkey/valkey:8-alpine restart: unless-stopped @@ -78,57 +78,69 @@ services: timeout: 5s retries: 10 + # ── MinIO (S3-compatible object store) ────────────────────────────────────── + # Suppress with --scale minio=0 when using AWS S3. + minio: + image: minio/minio:latest + restart: unless-stopped + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${STORAGE_ACCESS_KEY_ID:-minioadmin} + # WARNING: override this with a strong secret in your .env file. + MINIO_ROOT_PASSWORD: ${STORAGE_SECRET_ACCESS_KEY:-minioadmin} + volumes: + - minio_data:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 10s + timeout: 5s + retries: 10 + + # ── API (Go backend) ──────────────────────────────────────────────────────── api: image: ${PACA_API_IMAGE:-pacaai/paca-api:latest} - build: - context: ../services/api restart: unless-stopped environment: ENV: ${ENVIRONMENT:-production} PORT: 8080 - DATABASE_URL: ${DATABASE_URL:-postgres://${POSTGRES_USER:-paca}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-paca}?sslmode=disable} + # Defaults to the bundled postgres. Set DATABASE_URL explicitly when using + # an external database (and scale postgres to 0). + DATABASE_URL: ${DATABASE_URL:-postgres://${POSTGRES_USER:-paca}:${POSTGRES_PASSWORD:-changeme}@postgres:5432/${POSTGRES_DB:-paca}?sslmode=disable} REDIS_URL: ${REDIS_URL:-redis://valkey:6379/0} - JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET} + JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required — set it in your .env file} JWT_ACCESS_TTL: ${JWT_ACCESS_TTL:-15m} JWT_REFRESH_TTL: ${JWT_REFRESH_TTL:-168h} JWT_REFRESH_SESSION_TTL: ${JWT_REFRESH_SESSION_TTL:-24h} - COOKIE_SECURE: ${COOKIE_SECURE:-true} + COOKIE_SECURE: ${COOKIE_SECURE:-false} ADMIN_USERNAME: ${ADMIN_USERNAME:-admin} - ADMIN_PASSWORD: ${ADMIN_PASSWORD:?set ADMIN_PASSWORD} - # Object storage. - # Default: STORAGE_PROVIDER=minio — MinIO runs by default; no extra flags needed. - # AWS S3: set STORAGE_PROVIDER=s3 and real credentials; add --scale minio=0 to the startup command. + ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD is required — set it in your .env file} + # Object storage. Defaults to MinIO. Set STORAGE_PROVIDER=s3 for AWS S3. STORAGE_PROVIDER: ${STORAGE_PROVIDER:-minio} STORAGE_ENDPOINT: ${STORAGE_ENDPOINT:-minio:9000} - # Rewrite presigned URL host from the internal Docker hostname to the - # public gateway URL so browsers can reach the object store. STORAGE_PUBLIC_URL: ${STORAGE_PUBLIC_URL:-http://localhost/storage} STORAGE_REGION: ${STORAGE_REGION:-us-east-1} STORAGE_BUCKET: ${STORAGE_BUCKET:-paca} STORAGE_ACCESS_KEY_ID: ${STORAGE_ACCESS_KEY_ID:-minioadmin} STORAGE_SECRET_ACCESS_KEY: ${STORAGE_SECRET_ACCESS_KEY:-minioadmin} STORAGE_USE_SSL: ${STORAGE_USE_SSL:-false} - # Encryption key for sensitive values (plugin tokens/secrets). - # Must be a 64-char lowercase hex string (32 bytes). + # 64-char hex string for encrypting plugin secrets at rest. ENCRYPTION_KEY: ${ENCRYPTION_KEY:-} - # Public base URL used by plugins to generate callback/webhook URLs. + # Public base URL used by plugins for callback / webhook URLs. PUBLIC_URL: ${PUBLIC_URL:-} - # Plugin subsystem — load WASM binaries + migrations from the local plugin store. PLUGINS_WASM_DIR: /plugins PLUGINS_FRONTEND_DIR: /plugins-frontend PLUGINS_MCP_DIR: /plugins-mcp - # Pre-shared key for the AI agent service to authenticate with the API. - # Must match PACA_API_KEY in the ai-agent service. + # Pre-shared key matching PACA_API_KEY in the ai-agent service. AGENT_API_KEY: ${AGENT_API_KEY:-} depends_on: postgres: condition: service_healthy - required: false # skipped when --scale postgres=0 (managed PostgreSQL mode) + required: false valkey: condition: service_healthy minio: condition: service_healthy - required: false # skipped when --scale minio=0 (AWS S3 mode) + required: false healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/api/healthz"] interval: 10s @@ -136,32 +148,20 @@ services: retries: 10 start_period: 40s volumes: - # Backend plugin store — WASM binaries and SQL migrations only. - # Writable so the marketplace installer can download and extract artifacts. - # Never mounted to the gateway, so the WASM files are not publicly reachable. - # Layout: /plugins//backend.wasm + /plugins//migrations/ - backend_plugins:/plugins - # Frontend plugin store — built JS bundles and MCP bundles. - # Writable for API marketplace installs; also mounted read-only by the gateway. - # Layout: /plugins-frontend//assets/remoteEntry.js - frontend_plugins:/plugins-frontend - # MCP plugin store — self-contained ESM bundles served at /plugins-mcp/. - # Writable for API marketplace installs; also mounted read-only by the gateway. - # Layout: /plugins-mcp//mcp.js - mcp_plugins:/plugins-mcp + # ── Web (React SPA served via nginx) ──────────────────────────────────────── web: image: ${PACA_WEB_IMAGE:-pacaai/paca-web:latest} - build: - context: ../apps/web restart: unless-stopped depends_on: - api + # ── Realtime (Socket.IO event hub) ────────────────────────────────────────── realtime: image: ${PACA_REALTIME_IMAGE:-pacaai/paca-realtime:latest} - build: - context: ../services/realtime restart: unless-stopped environment: PORT: 3001 @@ -176,40 +176,38 @@ services: api: condition: service_healthy + # ── Gateway (nginx reverse proxy) ─────────────────────────────────────────── gateway: image: nginx:1.27-alpine restart: unless-stopped ports: - "${GATEWAY_PORT:-80}:80" volumes: + # gateway.conf must exist alongside this docker-compose.yml. - ./nginx/gateway.conf:/etc/nginx/conf.d/default.conf:ro - # Frontend plugin store — built JS bundles, served at /plugins/. - # Layout: /var/www/plugins//assets/remoteEntry.js - frontend_plugins:/var/www/plugins:ro - # MCP plugin store — self-contained ESM bundles, served at /plugins-mcp/. - # Layout: /var/www/plugins-mcp//mcp.js - mcp_plugins:/var/www/plugins-mcp:ro depends_on: - - api - - web - - realtime + api: + condition: service_started + web: + condition: service_started + required: false # skipped when --scale web=0 (external / CDN-hosted frontend) + realtime: + condition: service_started + # ── AI Agent (optional) ────────────────────────────────────────────────────── + # Suppress with --scale ai-agent=0 to skip this service. + # Requires Docker socket access on the host. ai-agent: image: ${PACA_AI_AGENT_IMAGE:-pacaai/paca-ai-agent:latest} - build: - # Build context is the paca/ root so the Dockerfile can build the MCP - # server (apps/mcp/) and the Python agent (services/ai-agent/) together. - context: .. - dockerfile: services/ai-agent/Dockerfile restart: unless-stopped environment: VALKEY_URL: ${REDIS_URL:-redis://valkey:6379/0} - DATABASE_URL: ${DATABASE_URL:-postgres://${POSTGRES_USER:-paca}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-paca}?sslmode=disable} - INTERNAL_API_KEY: ${INTERNAL_API_KEY:?set INTERNAL_API_KEY} + DATABASE_URL: ${DATABASE_URL:-postgres://${POSTGRES_USER:-paca}:${POSTGRES_PASSWORD:-changeme}@postgres:5432/${POSTGRES_DB:-paca}?sslmode=disable} + # Pre-shared key for authenticating requests to the AI agent service. + INTERNAL_API_KEY: ${INTERNAL_API_KEY:-} API_BASE_URL: http://api:8080 - # Gateway base URL — the MCP server uses this to resolve plugin MCP bundle - # URLs (e.g. /plugins-mcp//mcp.js). The gateway (nginx) serves these - # files, not the API service. GATEWAY_BASE_URL: http://gateway # Must match AGENT_API_KEY in the api service. PACA_API_KEY: ${AGENT_API_KEY:-} @@ -232,24 +230,6 @@ services: api: condition: service_healthy - # MinIO — S3-compatible object store. Runs by default for self-hosted deployments. - # When using AWS S3, suppress this container with --scale minio=0 and set - # STORAGE_PROVIDER=s3 with real AWS credentials in your env file. - minio: - image: minio/minio:latest - restart: unless-stopped - command: server /data --console-address ":9001" - environment: - MINIO_ROOT_USER: ${STORAGE_ACCESS_KEY_ID:-minioadmin} - MINIO_ROOT_PASSWORD: ${STORAGE_SECRET_ACCESS_KEY:-minioadmin} - volumes: - - minio_data:/data - healthcheck: - test: ["CMD", "mc", "ready", "local"] - interval: 10s - timeout: 5s - retries: 10 - volumes: postgres_data: valkey_data: diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 00000000..a83e32a1 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,565 @@ +#!/usr/bin/env bash +# Paca – interactive install script +# +# Downloads the pre-built release artifacts and walks you through setup. +# +# ── Recommended (interactive) ──────────────────────────────────────────────── +# curl -fsSL https://github.com/Paca-AI/paca/releases/latest/download/install.sh -o install.sh +# bash install.sh +# +# ── One-liner (non-interactive, all defaults + auto-generated secrets) ──────── +# bash <(curl -fsSL https://github.com/Paca-AI/paca/releases/latest/download/install.sh) +# +# ── Environment variable overrides ─────────────────────────────────────────── +# PACA_DIR Installation directory (default: ./paca) +# PACA_VERSION Release tag to install (default: latest) +# PACA_YES Skip prompts, use defaults (set to 1) + +set -euo pipefail + +# ── Colours ─────────────────────────────────────────────────────────────────── + +BOLD='\033[1m'; DIM='\033[2m' +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m' +RESET='\033[0m' + +info() { echo -e "${GREEN}✔${RESET} $*"; } +warn() { echo -e "${YELLOW}!${RESET} $*"; } +error() { echo -e "${RED}✖${RESET} $*" >&2; } +die() { error "$*"; exit 1; } +heading() { echo -e "\n${BOLD}${CYAN}── $* ${RESET}${DIM}$(printf '─%.0s' {1..40})${RESET}"; } +bold() { echo -e "${BOLD}$*${RESET}"; } + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +# head -c closes the pipe before tr finishes, causing tr to exit with SIGPIPE +# (status 141). pipefail would propagate that non-zero status and kill the +# script. Run each pipeline in a subshell with pipefail disabled so the exit +# code is taken from head (always 0) rather than tr. +rand_hex() { ( set +o pipefail; LC_ALL=C tr -dc 'a-f0-9' = ${#options[@]} )); then + warn "Invalid choice, using default (1)" + idx=0 + fi + _cref="${options[$idx]}" +} + +# yes_no VAR "Question" "y|n" +yes_no() { + local -n _yref="$1" + local question="$2" + local default="${3:-y}" + local answer="" + ask answer "$question" "$default" + case "${answer,,}" in + y|yes) _yref="yes" ;; + *) _yref="no" ;; + esac +} + +# download URL DEST +download() { + local url="$1" dest="$2" + if command -v curl &>/dev/null; then + curl -fsSL --retry 3 "$url" -o "$dest" + elif command -v wget &>/dev/null; then + wget -qO "$dest" "$url" + else + die "Neither curl nor wget found. Install one and retry." + fi +} + +# ── Version / URL resolution ────────────────────────────────────────────────── + +PACA_VERSION="${PACA_VERSION:-latest}" + +if [[ "$PACA_VERSION" == "latest" ]]; then + RELEASE_BASE="https://github.com/Paca-AI/paca/releases/latest/download" +else + RELEASE_BASE="https://github.com/Paca-AI/paca/releases/download/${PACA_VERSION}" +fi + +# Strip leading 'v' for Docker image tags (v1.2.3 → 1.2.3). +IMAGE_TAG="${PACA_VERSION#v}" + +# ── Preflight ───────────────────────────────────────────────────────────────── + +echo "" +bold "╔══════════════════════════════════════════════════════════╗" +bold "║ Paca – open-source AI-native project mgmt ║" +bold "╚══════════════════════════════════════════════════════════╝" +echo "" + +if ! command -v docker &>/dev/null; then + die "Docker is not installed. Get it at https://docs.docker.com/get-docker/" +fi +if ! docker info &>/dev/null 2>&1; then + die "Docker daemon is not running. Start Docker Desktop (or the daemon) and retry." +fi + +COMPOSE_CMD="" +if docker compose version &>/dev/null 2>&1; then + COMPOSE_CMD="docker compose" +elif command -v docker-compose &>/dev/null; then + COMPOSE_CMD="docker-compose" +else + die "Docker Compose not found. Install it from https://docs.docker.com/compose/install/" +fi + +info "Docker OK (compose: $COMPOSE_CMD)" + +# ── Installation directory ──────────────────────────────────────────────────── + +heading "Installation directory" + +PACA_DIR="${PACA_DIR:-./paca}" +ask PACA_DIR "Where should Paca be installed?" "$PACA_DIR" + +mkdir -p "${PACA_DIR}/nginx" +cd "${PACA_DIR}" +info "Working directory: $(pwd)" + +# ── Admin credentials ───────────────────────────────────────────────────────── + +heading "Admin account" + +ADMIN_USERNAME="${ADMIN_USERNAME:-admin}" +ask ADMIN_USERNAME "Admin username" "$ADMIN_USERNAME" + +_GENERATED_PW="$(rand_alnum 16)" +ADMIN_PASSWORD="${ADMIN_PASSWORD:-}" +ask_secret ADMIN_PASSWORD "Admin password (leave blank to auto-generate)" +if [[ -z "$ADMIN_PASSWORD" ]]; then + ADMIN_PASSWORD="$_GENERATED_PW" + ADMIN_PASSWORD_GENERATED=1 +else + ADMIN_PASSWORD_GENERATED=0 +fi + +# ── Encryption key ──────────────────────────────────────────────────────────── + +heading "Encryption key" + +echo " This key encrypts plugin secrets (OAuth tokens, API keys) stored in the" +echo " database. If you are connecting to an existing Paca database you MUST" +echo " supply the original key — a different key makes all existing encrypted" +echo " values permanently unreadable." +echo "" + +ENCRYPTION_KEY="" +ENCRYPTION_KEY_GENERATED=0 +ask_secret ENCRYPTION_KEY "Encryption key — 64-char hex (leave blank to generate)" + +if [[ -z "$ENCRYPTION_KEY" ]]; then + ENCRYPTION_KEY="$(rand_hex 32)" + ENCRYPTION_KEY_GENERATED=1 + info "Encryption key generated." +else + if [[ ! "$ENCRYPTION_KEY" =~ ^[a-f0-9]{64}$ ]]; then + die "Invalid encryption key: must be exactly 64 lowercase hex characters (32 bytes). Generate one with: openssl rand -hex 32" + fi + info "Using the provided encryption key." +fi + +# ── Database ────────────────────────────────────────────────────────────────── + +heading "Database" + +DB_CHOICE="" +ask_choice DB_CHOICE "How should Paca store data?" \ + "Bundled PostgreSQL container (recommended)" \ + "External / managed PostgreSQL (bring your own)" + +SCALE_POSTGRES="" +DATABASE_URL_OVERRIDE="" +POSTGRES_PASSWORD_VALUE="" + +if [[ "$DB_CHOICE" == *"External"* ]]; then + SCALE_POSTGRES="--scale postgres=0" + ask DATABASE_URL_OVERRIDE "PostgreSQL connection URL" "postgres://user:pass@host:5432/dbname" + # Set a placeholder so the compose's ${POSTGRES_PASSWORD:-changeme} default doesn't matter. + POSTGRES_PASSWORD_VALUE="not-used-external-db" + info "External PostgreSQL will be used." +else + POSTGRES_PASSWORD_VALUE="$(rand_alnum 20)" + info "A bundled PostgreSQL container will be started." +fi + +# ── Object storage ──────────────────────────────────────────────────────────── + +heading "Object storage" + +STORAGE_CHOICE="" +ask_choice STORAGE_CHOICE "Where should file attachments be stored?" \ + "Self-hosted MinIO (recommended, no cloud account needed)" \ + "AWS S3" + +SCALE_MINIO="" +STORAGE_PROVIDER="minio" +STORAGE_ENDPOINT="minio:9000" +STORAGE_USE_SSL="false" +STORAGE_REGION="us-east-1" +STORAGE_BUCKET="paca" +STORAGE_ACCESS_KEY_ID="$(rand_alnum 16)" +STORAGE_SECRET_ACCESS_KEY="$(rand_alnum 32)" + +if [[ "$STORAGE_CHOICE" == *"AWS"* ]]; then + SCALE_MINIO="--scale minio=0" + STORAGE_PROVIDER="s3" + STORAGE_ENDPOINT="" + STORAGE_USE_SSL="true" + + ask STORAGE_REGION "AWS region" "us-east-1" + ask STORAGE_BUCKET "S3 bucket name" "" + ask STORAGE_ACCESS_KEY_ID "AWS access key ID" "" + ask_secret STORAGE_SECRET_ACCESS_KEY "AWS secret access key" + + if [[ -z "$STORAGE_BUCKET" ]]; then + die "S3 bucket name is required." + fi + if [[ -z "$STORAGE_ACCESS_KEY_ID" || -z "$STORAGE_SECRET_ACCESS_KEY" ]]; then + die "AWS credentials are required." + fi + info "AWS S3 will be used (bucket: ${STORAGE_BUCKET}, region: ${STORAGE_REGION})." +else + info "Self-hosted MinIO will be started." +fi + +# ── Network ─────────────────────────────────────────────────────────────────── + +heading "Network" + +GATEWAY_PORT="80" +ask GATEWAY_PORT "Gateway port (the port Paca will be accessible on)" "80" + +# Derive a sensible default public URL from the port. +if [[ "$GATEWAY_PORT" == "80" ]]; then + _DEFAULT_PUBLIC_URL="http://localhost" +elif [[ "$GATEWAY_PORT" == "443" ]]; then + _DEFAULT_PUBLIC_URL="https://localhost" +else + _DEFAULT_PUBLIC_URL="http://localhost:${GATEWAY_PORT}" +fi + +PUBLIC_URL="" +ask PUBLIC_URL "Public URL (full URL where Paca will be accessible, no trailing slash)" "$_DEFAULT_PUBLIC_URL" +PUBLIC_URL="${PUBLIC_URL%/}" # strip trailing slash + +# Set COOKIE_SECURE based on whether the URL uses HTTPS. +if [[ "$PUBLIC_URL" == https://* ]]; then + COOKIE_SECURE="true" +else + COOKIE_SECURE="false" +fi + +# Compute storage public URL only for MinIO (S3 presigned URLs are self-contained). +if [[ "$STORAGE_PROVIDER" == "minio" ]]; then + STORAGE_PUBLIC_URL="${PUBLIC_URL}/storage" +else + STORAGE_PUBLIC_URL="" +fi + +# ── Web app ─────────────────────────────────────────────────────────────────── + +heading "Web application" + +WEB_CHOICE="" +ask_choice WEB_CHOICE "How do you want to serve the web app?" \ + "Bundled container (recommended – nginx serves the built React SPA)" \ + "External hosting (S3, CloudFront, Vercel, etc. – only API services run here)" + +SCALE_WEB="" +if [[ "$WEB_CHOICE" == *"External"* ]]; then + SCALE_WEB="--scale web=0" + echo "" + warn "The web container will be skipped." + warn "Build the SPA from source and deploy the dist/ folder to your CDN." + warn "Point your CDN's API proxy to: ${_DEFAULT_PUBLIC_URL:-http://localhost}/api" + echo "" + info "The gateway will still serve /api/, /ws/, and /storage/ routes." +else + info "Bundled web container will be started." +fi + +# ── AI Agent ────────────────────────────────────────────────────────────────── + +heading "AI Agent (optional)" + +echo " The AI agent enables autonomous task execution." +echo " It requires access to the Docker socket on the host machine." +echo "" + +INCLUDE_AI_AGENT="yes" +yes_no INCLUDE_AI_AGENT "Include the AI agent service?" "y" + +SCALE_AI_AGENT="" +AGENT_API_KEY="$(rand_hex 32)" +INTERNAL_API_KEY="$(rand_hex 32)" + +if [[ "$INCLUDE_AI_AGENT" == "no" ]]; then + SCALE_AI_AGENT="--scale ai-agent=0" + info "AI agent will be skipped." +else + info "AI agent will be included." +fi + +# ── Download release assets ─────────────────────────────────────────────────── + +heading "Downloading release assets" + +if [[ -f docker-compose.yml ]]; then + warn "docker-compose.yml already exists — skipping download." +else + info "Downloading docker-compose.yml..." + download "${RELEASE_BASE}/docker-compose.yml" docker-compose.yml +fi + +if [[ -f nginx/gateway.conf ]]; then + warn "nginx/gateway.conf already exists — skipping download." +else + info "Downloading nginx/gateway.conf..." + download "${RELEASE_BASE}/gateway.conf" nginx/gateway.conf +fi + +# ── Generate .env ───────────────────────────────────────────────────────────── + +heading "Generating .env" + +if [[ -f .env ]]; then + warn ".env already exists." + KEEP_ENV="yes" + yes_no KEEP_ENV "Keep existing .env?" "y" + if [[ "$KEEP_ENV" == "yes" ]]; then + warn "Keeping existing .env. Delete it and re-run to regenerate." + else + mv .env ".env.bak.$(date +%s)" + warn "Old .env backed up." + KEEP_ENV="no" + fi +else + KEEP_ENV="no" +fi + +if [[ "$KEEP_ENV" == "no" ]]; then + JWT_SECRET="$(rand_hex 32)" + + cat >.env < Date: Fri, 5 Jun 2026 04:45:27 +0000 Subject: [PATCH 2/2] feat: enhance installation and deployment documentation with interactive setup instructions and migration guidance --- deploy/README.md | 49 +++++++++++++++++-- deploy/docker-compose.prod.yml | 10 ++-- scripts/install.sh | 2 +- services/api/internal/bootstrap/app.go | 16 +++--- .../internal/platform/database/migrations.go | 5 +- 5 files changed, 63 insertions(+), 19 deletions(-) diff --git a/deploy/README.md b/deploy/README.md index 55fecc8e..400f32f2 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -53,11 +53,27 @@ curl -fsSL https://github.com/Paca-AI/paca/releases/latest/download/docker-compo curl -fsSL https://github.com/Paca-AI/paca/releases/latest/download/gateway.conf -o nginx/gateway.conf ``` -Create your environment file: +Download the example environment file and edit it: ```bash -cp /path/to/repo/deploy/.env.production.example .env -# Edit .env — set JWT_SECRET, ADMIN_PASSWORD, POSTGRES_PASSWORD, storage credentials, etc. +curl -fsSL https://github.com/Paca-AI/paca/releases/latest/download/docker-compose.yml -o docker-compose.yml +# Or use the .env.production.example from the repo as a reference: +# https://github.com/Paca-AI/paca/blob/master/deploy/.env.production.example +``` + +Create a `.env` with the required variables: + +```bash +# Required: generate with 'openssl rand -hex 32' +JWT_SECRET= +ADMIN_PASSWORD= +POSTGRES_PASSWORD= +# Required when using AI agent: generate with 'openssl rand -hex 32' +AGENT_API_KEY= +INTERNAL_API_KEY= +# Required for plugin secrets at rest: generate with 'openssl rand -hex 32' +ENCRYPTION_KEY=<64-char-hex> +PUBLIC_URL=http://your-domain-or-ip ``` Start the full stack (bundled PostgreSQL + MinIO): @@ -92,6 +108,33 @@ Flags can be combined: docker compose --env-file .env up -d --scale postgres=0 --scale minio=0 ``` +### Upgrading from an earlier installation + +The compose project was renamed from `paca-prod` to `paca` in this release. +Docker Compose namespaces volumes by project name, so existing volumes +(`paca-prod_postgres_data`, `paca-prod_minio_data`, etc.) are **not** automatically +attached to the new stack. To migrate: + +```bash +# 1. Stop the old stack (volumes are preserved on disk). +docker compose -p paca-prod --env-file .env down + +# 2. Rename each volume you want to keep. +docker volume create paca_postgres_data +docker run --rm \ + -v paca-prod_postgres_data:/from \ + -v paca_postgres_data:/to \ + alpine sh -c "cp -av /from/. /to/" +docker volume rm paca-prod_postgres_data + +# Repeat for minio_data, valkey_data, and plugin volumes as needed. + +# 3. Start the new stack. +docker compose --env-file .env up -d +``` + +If you are doing a fresh install (no data to keep), no migration is needed. + ### Pinning a release version Set the image variables in `.env` to lock to a specific release: diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml index 8d76d859..06f05d3d 100644 --- a/deploy/docker-compose.prod.yml +++ b/deploy/docker-compose.prod.yml @@ -1,8 +1,9 @@ # Standalone production compose — no local source tree required. # # Designed to be downloaded and run without cloning the repository. -# All application images are pulled from DockerHub. Database migrations are -# applied automatically by the API service at startup (embedded SQL files). +# All application images are pulled from DockerHub. Database schema migrations +# are embedded in the API binary and applied automatically on every startup +# using idempotent SQL (CREATE TABLE IF NOT EXISTS / INSERT … ON CONFLICT). # # ── Quick start ─────────────────────────────────────────────────────────────── # The recommended way to set this up is via the install script: @@ -111,7 +112,8 @@ services: JWT_ACCESS_TTL: ${JWT_ACCESS_TTL:-15m} JWT_REFRESH_TTL: ${JWT_REFRESH_TTL:-168h} JWT_REFRESH_SESSION_TTL: ${JWT_REFRESH_SESSION_TTL:-24h} - COOKIE_SECURE: ${COOKIE_SECURE:-false} + # Set to false only when serving over plain HTTP (no TLS termination). + COOKIE_SECURE: ${COOKIE_SECURE:-true} ADMIN_USERNAME: ${ADMIN_USERNAME:-admin} ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD is required — set it in your .env file} # Object storage. Defaults to MinIO. Set STORAGE_PROVIDER=s3 for AWS S3. @@ -206,6 +208,8 @@ services: VALKEY_URL: ${REDIS_URL:-redis://valkey:6379/0} DATABASE_URL: ${DATABASE_URL:-postgres://${POSTGRES_USER:-paca}:${POSTGRES_PASSWORD:-changeme}@postgres:5432/${POSTGRES_DB:-paca}?sslmode=disable} # Pre-shared key for authenticating requests to the AI agent service. + # WARNING: always set this to a strong random secret (e.g. openssl rand -hex 32). + # An empty key disables authentication on the agent service endpoint. INTERNAL_API_KEY: ${INTERNAL_API_KEY:-} API_BASE_URL: http://api:8080 GATEWAY_BASE_URL: http://gateway diff --git a/scripts/install.sh b/scripts/install.sh index a83e32a1..662add56 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -135,7 +135,7 @@ download() { if command -v curl &>/dev/null; then curl -fsSL --retry 3 "$url" -o "$dest" elif command -v wget &>/dev/null; then - wget -qO "$dest" "$url" + wget -q --tries=3 -O "$dest" "$url" else die "Neither curl nor wget found. Install one and retry." fi diff --git a/services/api/internal/bootstrap/app.go b/services/api/internal/bootstrap/app.go index 1484580f..eb915316 100644 --- a/services/api/internal/bootstrap/app.go +++ b/services/api/internal/bootstrap/app.go @@ -105,17 +105,13 @@ func New(cfg *config.Config) (*App, error) { refreshStore := redisRepo.NewRefreshTokenStore(redisClient) pluginRepo := pgRepo.NewPluginRepository(db) - // --- Schema migration (non-production only) ----------------------------- - // In development the embedded SQL migrations are run on every startup so - // that a fresh database is always in the correct state without requiring - // a manual migration step. All statements use CREATE TABLE IF NOT EXISTS - // / INSERT … ON CONFLICT so they are idempotent and safe to re-run. - if cfg.Env != "production" { - if err := database.RunMigrationsFS(db, migrations.FS); err != nil { - return nil, fmt.Errorf("bootstrap: auto-migrate: %w", err) - } - log.Info("schema migrations applied") + // --- Schema migration --------------------------------------------------- + // All statements use CREATE TABLE IF NOT EXISTS / INSERT … ON CONFLICT so + // they are idempotent and safe to re-run on every startup. + if err := database.RunMigrationsFS(db, migrations.FS); err != nil { + return nil, fmt.Errorf("bootstrap: auto-migrate: %w", err) } + log.Info("schema migrations applied") // --- Admin seeding ------------------------------------------------------- // seedDefaultRoles must run first so the ADMIN global role exists before diff --git a/services/api/internal/platform/database/migrations.go b/services/api/internal/platform/database/migrations.go index 35c16799..4771ae2d 100644 --- a/services/api/internal/platform/database/migrations.go +++ b/services/api/internal/platform/database/migrations.go @@ -39,8 +39,9 @@ func RunMigrations(db *gorm.DB, migrationsDir string) error { } // RunMigrationsFS executes all *.sql files found in the root of fsys (in -// lexicographic order) against db. Use this with an embedded FS for -// zero-configuration startup migration in non-production environments. +// lexicographic order) against db. All SQL files must be idempotent +// (CREATE TABLE IF NOT EXISTS, INSERT … ON CONFLICT, etc.) so the function +// is safe to call on every startup in any environment. func RunMigrationsFS(db *gorm.DB, fsys fs.FS) error { entries, err := fs.ReadDir(fsys, ".") if err != nil {