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
# docker-compose.yml
services:
app: # PayWarden gateway
postgres: # PostgreSQL 16
redis: # Redis 7Quick start
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 appFirst 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:
docker compose logs -f appEnvironment file
The app container reads from .env in the project root. Never commit this file.
# .env.example shows all available variables
cat .env.examplePersistent data
By default, Docker volumes are used for PostgreSQL and Redis data:
volumes:
postgres_data:
redis_data:Data persists across container restarts. To fully reset:
docker compose down -v # ⚠️ deletes all dataVault 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:
# docker-compose.prod.yml
services:
app:
volumes:
- ./vault:/app/vaultVault 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 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
# docker-compose.prod.yml
services:
app:
restart: always
environment:
- NODE_ENV=production
- LOG_LEVEL=warn
volumes:
- ./vault:/app/vault# 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 -dMigrations 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
services:
app:
deploy:
resources:
limits:
cpus: '1'
memory: 512MHealth check
PayWarden exposes three health endpoints (see API reference):
# 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
docker compose pull
# Re-run migrations in case the new image ships schema changes
docker compose --profile tools run --rm migrate
docker compose up -dBackup
Back up these items regularly:
| Item | Location | Priority |
|---|---|---|
| Mnemonic phrase | Written offline | 🔴 Critical |
VAULT_KEY | Password manager / HSM | 🔴 Critical |
| PostgreSQL data | docker compose exec postgres pg_dump | 🟡 Important |
.env | Encrypted backup | 🟡 Important |
# 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