Skip to content

Docker Compose Deployment

PayWarden is designed to run as a single docker compose up command. This page covers deployment options from local dev to production.

Default stack

yaml
# docker-compose.yml
services:
  app:       # PayWarden gateway
  postgres:  # PostgreSQL 16
  redis:     # Redis 7

Quick start

bash
git clone https://github.com/paywarden/paywarden
cd paywarden
cp .env.example .env
# Edit .env with your values

# 1. Bring up postgres + redis first
docker compose up -d postgres redis

# 2. Run Drizzle migrations against the empty database (host-side, one-shot).
#    The default compose file does not ship a `migrate` service — see
#    `docker-compose.prod.yml` if you need a containerized migrate runner.
pnpm install
pnpm db:migrate

# 3. Start the app
docker compose up -d app

First boot needs migrations

The app container does NOT run Drizzle migrations on startup. Running docker compose up -d against an empty database without the migrate step will cause the app to crash on first query. See Production deployment below for the same sequence in a prod override.

Check logs:

bash
docker compose logs -f app

Environment file

The app container reads from .env in the project root. Never commit this file.

bash
# .env.example shows all available variables
cat .env.example

Persistent data

By default, Docker volumes are used for PostgreSQL and Redis data:

yaml
volumes:
  postgres_data:
  redis_data:

Data persists across container restarts. To fully reset:

bash
docker compose down -v   # ⚠️ deletes all data

Vault directory

Your encrypted vault is stored inside the container at /app/vault/ (a directory containing seed.enc, not a single file). In production, mount it as a volume so it persists across image updates:

yaml
# docker-compose.prod.yml
services:
  app:
    volumes:
      - ./vault:/app/vault

Vault isolation is per-instance

The vault/ directory is encrypted with VAULT_KEY. If you run multiple PayWarden instances (e.g. dev + prod on the same host, or a blue/green deployment), each instance MUST use its own vault/ path and its own VAULT_KEY. Sharing a vault/ across instances with different VAULT_KEY values will cause AES-GCM decryption to fail on startup and the app will refuse to boot.

Production deployment

1. Use a reverse proxy

Run Nginx or Caddy in front of PayWarden:

nginx
# Nginx example
server {
    listen 443 ssl;
    server_name pay.yourdomain.com;

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

With Caddy (automatic HTTPS):

pay.yourdomain.com {
    reverse_proxy localhost:3000
}

2. Use docker compose override

yaml
# docker-compose.prod.yml
services:
  app:
    restart: always
    environment:
      - NODE_ENV=production
      - LOG_LEVEL=warn
    volumes:
      - ./vault:/app/vault
bash
# 1. Start postgres + redis first
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d postgres redis

# 2. Run database migrations explicitly (one-shot profile, not automatic on app startup)
docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile tools run --rm migrate

# 3. Start the full stack (app + reverse proxy)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Migrations are NOT automatic

The app container does not run Drizzle migrations on startup. You must run the dedicated migrate profile before starting the app, and again after every upgrade that ships schema changes. Running docker compose up -d without the migrate step against an empty database will cause the app to crash on first query.

3. Resource limits

yaml
services:
  app:
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 512M

Health check

PayWarden exposes three health endpoints (see API reference):

bash
# Liveness — always 200 while the process is up. Used by Docker HEALTHCHECK.
curl http://localhost:3000/api/v1/health/live
# {"status":"alive"}

# Readiness — public, minimal. Returns 200 ok or 503 degraded after
# exercising DB / Redis / TronGrid / wallet / watcher checks server-side.
# The response body is intentionally minimal; no infra details are leaked.
curl http://localhost:3000/api/v1/health
# {"status":"ok"}

Detailed breakdowns (per-dependency status, current block, sweep mode) are only available on the admin-authenticated GET /api/v1/admin/health endpoint.

Docker Compose's built-in healthcheck targets /api/v1/health/live so a first-deploy bootstrap (wallet not yet initialized, migrations pending) does not flap the container into a restart loop.

Updating

bash
docker compose pull
# Re-run migrations in case the new image ships schema changes
docker compose --profile tools run --rm migrate
docker compose up -d

Backup

Back up these items regularly:

ItemLocationPriority
Mnemonic phraseWritten offline🔴 Critical
VAULT_KEYPassword manager / HSM🔴 Critical
PostgreSQL datadocker compose exec postgres pg_dump🟡 Important
.envEncrypted backup🟡 Important
bash
# PostgreSQL backup
docker compose exec postgres pg_dump -U postgres paywarden > backup_$(date +%Y%m%d).sql

# Restore
docker compose exec -T postgres psql -U postgres paywarden < backup_20250101.sql

Released under the BSL 1.1 License.