This directory contains deployment assets for two distinct use cases:
- contributor-friendly local development;
- production container deployment for self-hosters.
| File | Description |
|---|---|
docker-compose.dev.yml |
Local development stack: PostgreSQL, Valkey, MinIO, and optional app containers |
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 manual production deployments |
Service container definitions live with each service:
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 Caddyfile, walks you
through configuration interactively (database, storage, networking/HTTPS, AI agent),
generates a .env with strong random secrets, and starts the stack.
curl -fsSL https://github.com/Paca-AI/paca/releases/latest/download/install.sh -o install.sh
bash install.shThe 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 |
| HTTPS | Enabled by default — Let's Encrypt for a real domain, Caddy's local CA otherwise; can be disabled for plain HTTP |
| AI Agent | Enabled by default; can be skipped to reduce resource usage |
Download the two required files from the latest release:
mkdir -p paca/caddy && 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/Caddyfile -o caddy/CaddyfileDownload the example environment file and edit it:
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.exampleCreate a .env with the required variables:
# Required: generate with 'openssl rand -hex 32'
JWT_SECRET=<strong-random-secret>
ADMIN_PASSWORD=<strong-password>
POSTGRES_PASSWORD=<strong-random-password>
# Required when using AI agent: generate with 'openssl rand -hex 32'
AGENT_API_KEY=<strong-random-secret>
INTERNAL_API_KEY=<strong-random-secret>
# Required for plugin secrets at rest: generate with 'openssl rand -hex 32'
ENCRYPTION_KEY=<64-char-hex>
PUBLIC_URL=http://your-domain-or-ipStart the full stack (bundled PostgreSQL + MinIO):
docker compose --env-file .env up -dWith HTTPS — set SITE_ADDRESS to any concrete domain or IP address and Caddy
handles certificates automatically, choosing the right kind for what you give it:
# In .env: set SITE_ADDRESS to your domain/IP, and PUBLIC_URL/COOKIE_SECURE to match.
SITE_ADDRESS=paca.example.com
PUBLIC_URL=https://paca.example.com
COOKIE_SECURE=truedocker compose --env-file .env up -d- A real domain name with DNS already pointed here gets a trusted Let's Encrypt certificate, renewed automatically. Ports 80 and 443 must both be reachable from the internet for the ACME challenge to succeed.
- An IP address,
localhost,*.localhost, or anything else that isn't a publicly resolvable domain gets a certificate from Caddy's own local certificate authority instead — traffic is still encrypted, but browsers will show a trust warning since that CA isn't publicly trusted.
Either way, certificates persist in the caddy_data volume across restarts.
Without SITE_ADDRESS (or set to a bare port like :80), the gateway serves plain
HTTP — the simplest option, and the right one when another proxy or load balancer in
front of this server already terminates TLS.
With external PostgreSQL (suppress the bundled container):
# Set DATABASE_URL in .env to your managed connection string.
docker compose --env-file .env up -d --scale postgres=0With AWS S3 (suppress MinIO):
# Set STORAGE_PROVIDER=s3 and real AWS credentials in .env.
docker compose --env-file .env up -d --scale minio=0Without the AI agent:
docker compose --env-file .env up -d --scale ai-agent=0Flags can be combined:
docker compose --env-file .env up -d --scale postgres=0 --scale minio=0Recommended: upgrade script. From the directory where your docker-compose.yml and
.env live, run the same upgrade script published with each release. It backs up
docker-compose.yml, caddy/Caddyfile, and .env before overwriting them, refreshes
the compose file and Caddyfile, re-pins image versions when you request a specific
release, then pulls and restarts the stack:
curl -fsSL https://github.com/Paca-AI/paca/releases/latest/download/upgrade.sh -o upgrade.sh
bash upgrade.shPin to a specific release instead of latest:
PACA_VERSION=v1.2.3 bash upgrade.shPass through any --scale flags you used originally:
bash upgrade.sh --scale web=0 --scale minio=0Manual: pull the latest images and restart the stack — this is enough when
docker-compose.yml and the Caddyfile haven't changed shape since your last upgrade:
docker compose pull
docker compose --env-file .env up -dDatabase migrations run automatically on API startup — no manual steps are required.
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:
# 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 -dIf you are doing a fresh install (no data to keep), no migration is needed.
Set the image variables in .env to lock to 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.3A db-backup container runs alongside the stack and writes a gzip-compressed
pg_dump on a cron schedule you control, pruning dumps older than the
configured retention period. It works against the bundled postgres container
or an external DATABASE_URL.
Configure it in .env:
BACKUP_ENABLED=true # set to false to turn off backups permanently
BACKUP_DIR=./backups # host directory dumps are written to
BACKUP_CRON=0 2 * * * # standard 5-field cron syntax, default 02:00 daily
BACKUP_RETENTION_DAYS=7 # dumps older than this are deleted
# TZ=America/New_York # interpret BACKUP_CRON in this zone instead of UTCBACKUP_DIR is bind-mounted into the container, so it must be a path (relative
to wherever you run docker compose, or absolute) — not a bare name.
BACKUP_CRON accepts any standard cron expression, e.g. */30 * * * * (every
30 minutes) or 0 2 * * 0 (weekly, Sunday at 02:00). The install script prompts
for all four; existing installs get them backfilled by upgrade.sh with these
same defaults.
BACKUP_ENABLED is the persisted record of whether the service should run at
all — --scale db-backup=0 only suppresses it for a single up invocation,
so upgrade.sh reads BACKUP_ENABLED back on every upgrade to decide whether
to re-apply that scale flag automatically, rather than guessing from
DATABASE_URL alone. Set it (not just the scale flag) if you want a "no" to
backups during install to stay a "no" on every future upgrade.
Scheduling is handled by crond inside the container, which blocks until the
next due minute rather than polling, and the container is capped at 0.5 CPU /
256MB (see deploy.resources.limits on the service) — so it stays effectively
idle (well under 1MB RSS, 0% CPU observed) between runs and can't compete for
host resources during the brief dump window either. Raise the memory limit in
docker-compose.yml directly if you have an unusually large database.
Dumps are written by the container's root user, so deleting or moving them
directly on the host may require sudo.
Restore (bundled PostgreSQL container):
gunzip -c backups/paca-<timestamp>.sql.gz | docker compose exec -T postgres psql -U ${POSTGRES_USER:-paca} -d ${POSTGRES_DB:-paca}Restore (external PostgreSQL, using DATABASE_URL):
gunzip -c backups/paca-<timestamp>.sql.gz | psql "$DATABASE_URL"Disable automated backups (e.g. if a managed database already handles this):
docker compose --env-file .env up -d --scale db-backup=0To keep it disabled across future upgrade.sh runs as well, also set
BACKUP_ENABLED=false in .env — otherwise upgrade.sh has no record of the
choice and may re-enable it on the next upgrade.
Use docker-compose.dev.yml for local development and contributor onboarding.
When exposing the stack through a tunnel or reverse proxy, copy the example env file and set the public host:
cp deploy/.env.dev.example deploy/.env.dev
# Edit PUBLIC_HOST and VITE_ALLOWED_HOST in deploy/.env.dev
docker compose --env-file deploy/.env.dev -f deploy/docker-compose.dev.yml up -dStart the full local stack in containers (no tunnel, plain localhost):
docker compose -f deploy/docker-compose.dev.yml up -dOpen http://localhost:3000 once all services are healthy.
Start only shared dependencies:
docker compose -f deploy/docker-compose.dev.yml up -d postgres valkeyFor day-to-day coding, contributors can run the application services directly on the host and use Docker Compose only for PostgreSQL and Valkey.
| Service | Port | Notes |
|---|---|---|
| Gateway (Caddy) | 3000 | Main entry point — http://localhost:3000 |
| PostgreSQL | 5432 | Local database for development |
| Valkey | 6379 | Local cache / event streams |
| API | 8080 (internal) | Routed via gateway at /api/ |
| Web | 3000 (internal) | Routed via gateway at / |
| MinIO S3 API | 9000 | Local object store (S3-compatible) |
| MinIO Console | 9001 | MinIO web UI (credentials: minioadmin / minioadmin) |
docker-compose.dev.yml includes the same db-backup service as production
(see Database backups above), pointed at the local
postgres container by default. It starts automatically with the rest of the
stack and writes dumps to deploy/backups/.
To test it without waiting for the cron schedule, trigger a dump on demand:
docker compose -f deploy/docker-compose.dev.yml exec db-backup run-backup.sh
ls deploy/backups/To test retention pruning, backdate a dummy file past the retention window and re-run the script:
docker compose -f deploy/docker-compose.dev.yml exec db-backup \
sh -c 'touch -t "$(date -d "@$(($(date +%s)-10*86400))" +%Y%m%d%H%M)" /backups/paca-fake.sql.gz'
docker compose -f deploy/docker-compose.dev.yml exec db-backup run-backup.sh
ls deploy/backups/ # paca-fake.sql.gz should be goneTo watch the cron schedule itself fire, override BACKUP_CRON in
deploy/.env.dev (e.g. */2 * * * * for every 2 minutes) and tail the logs:
docker compose --env-file deploy/.env.dev -f deploy/docker-compose.dev.yml up -d db-backup
docker compose -f deploy/docker-compose.dev.yml logs -f db-backupRestore a dump into the local dev database:
gunzip -c deploy/backups/paca-<timestamp>.sql.gz | docker compose -f deploy/docker-compose.dev.yml exec -T postgres psql -U paca -d pacaStop the development stack:
docker compose -f deploy/docker-compose.dev.yml downRemove the Postgres volume as well:
docker compose -f deploy/docker-compose.dev.yml down -vUse docker-compose.e2e.yml to spin up a full production-like
stack with fixed, test-safe credentials for running end-to-end tests:
docker compose -f deploy/docker-compose.e2e.yml up -d --build --wait
docker compose -f deploy/docker-compose.e2e.yml down -vAll secrets are intentionally weak and public — never use them outside a local E2E environment.