Skip to main content

16 - Docker Deployment

Production deployment with Docker and Docker Compose.

The repository ships with a working docker-compose.yml at the project root that defines five services — postgres, redis, api, admin, and an optional minio plus nginx for the full stack. Use that file as the source of truth for ports, environment variables, and healthchecks; the snippets in this guide are illustrations only.

Host with no registry access? This guide assumes the host can pull images. If yours cannot and you carry image tarballs across instead, see 22 - Air-gapped Deploy — it covers building, verifying and gating that path, and the failure modes specific to it.


Quick Deploy​

# 1. Clone the repository
git clone https://github.com/ribato22/MultiWA.git
cd MultiWA

# 2. Copy the Docker environment template and review it
cp .env.docker .env
# Edit .env: set strong values for JWT_SECRET, JWT_REFRESH_SECRET, ENCRYPTION_KEY,
# DB_PASSWORD, NEXT_PUBLIC_API_URL, and CORS_ORIGINS.

# 3. Start the stack
docker compose up -d

# 4. (Optional) Bring up the full stack with S3 storage and Nginx reverse proxy
docker compose --profile full up -d

Default exposed endpoints once the stack is up:

ServiceURL
Admin Dashboardhttp://localhost:3001
APIhttp://localhost:3333/api/v1
Swagger UIhttp://localhost:3333/api/docs
PostgreSQLlocalhost:5432 (mapped from postgres:5432)
Redislocalhost:6379 (mapped from redis:6379)

The API container listens on 3333 because docker-compose.yml sets API_PORT=3333. The Admin container listens on 3001.


Service Definitions (excerpt from docker-compose.yml)​

services:
api:
build:
context: .
dockerfile: Dockerfile
target: api
container_name: multiwa-api
environment:
NODE_ENV: production
DATABASE_URL: postgresql://multiwa:${DB_PASSWORD:-multiwa123}@postgres:5432/${DB_NAME:-multiwa}?schema=public
REDIS_URL: redis://redis:6379
API_PORT: 3333
API_HOST: 0.0.0.0
JWT_SECRET: ${JWT_SECRET:-change-this-to-a-random-secret}
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:-change-this-refresh-secret}
ENCRYPTION_KEY: ${ENCRYPTION_KEY:-}
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3001,http://localhost:3333}
WHATSAPP_ENGINE: ${DEFAULT_ENGINE:-whatsapp-web-js}
SESSIONS_DIR: /data/sessions
ports:
- "${API_PORT:-3333}:3333"
volumes:
- sessions_data:/data/sessions
depends_on:
postgres: { condition: service_healthy }
redis: { condition: service_healthy }
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3333/api/docs"]

admin:
build:
context: .
dockerfile: Dockerfile
target: admin
args:
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:3333}
container_name: multiwa-admin
environment:
NODE_ENV: production
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:3333}
ports:
- "${ADMIN_PORT:-3001}:3001"

postgres:
image: postgres:16-alpine
container_name: multiwa-postgres

redis:
image: redis:7-alpine
container_name: multiwa-redis

NEXT_PUBLIC_API_URL is baked into the admin client bundle at build time. If you change its value, you must rebuild the admin image: docker compose build --no-cache admin && docker compose up -d --no-deps --force-recreate admin.


Required Environment Variables​

VariableDescriptionNotes
DB_PASSWORDPostgreSQL passwordSet a strong value before first up -d; it becomes the password baked into the volume.
JWT_SECRETSecret for JWT access tokensopenssl rand -base64 64
JWT_REFRESH_SECRETSecret for JWT refresh tokensIndependent of JWT_SECRET
ENCRYPTION_KEYEncrypts sensitive fields at restopenssl rand -hex 32
NEXT_PUBLIC_API_URLPublic URL the browser uses to reach the APIMust match the origin users hit (e.g., https://api.example.com); build-time
CORS_ORIGINSComma-separated allowed originsInclude the admin origin (e.g., https://admin.example.com)
DEFAULT_ENGINEwhatsapp-web-js or baileysDefaults to whatsapp-web-js

Optional S3-compatible media storage (MinIO or external):

VariableDescription
S3_ENDPOINTe.g., http://minio:9000 or https://s3.amazonaws.com
S3_ACCESS_KEY / S3_SECRET_KEYStorage credentials
S3_BUCKETDefaults to multiwa-media

The full reference is in 18-configuration-reference.md.


Reverse Proxy (Nginx)​

Single public origin serving the Admin UI, with API calls reverse-proxied under /api:

server {
listen 443 ssl http2;
server_name multiwa.example.com;

ssl_certificate /etc/letsencrypt/live/multiwa.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/multiwa.example.com/privkey.pem;

# Admin (Next.js)
location / {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
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;
}

# API + Swagger
location /api/ {
proxy_pass http://127.0.0.1:3333;
proxy_http_version 1.1;
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;
}

# Socket.IO (WebSocket upgrade)
location /socket.io/ {
proxy_pass http://127.0.0.1:3333;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}

If you put the Admin on a different subdomain than the API (e.g., admin.example.com and api.example.com), update NEXT_PUBLIC_API_URL and CORS_ORIGINS accordingly, then rebuild the admin image so the new API URL is baked into the client bundle.


Health Checks​

# API (matches the compose healthcheck)
curl -fsSI http://localhost:3333/api/docs

# PostgreSQL
docker exec multiwa-postgres pg_isready -U multiwa

# Redis
docker exec multiwa-redis redis-cli ping
# Expected: PONG

# Admin
curl -fsSI http://localhost:3001/

# Worker logs (last 40 lines)
docker logs --tail 40 multiwa-worker

The compose file ships container-level healthchecks for the API (/api/docs), PostgreSQL (pg_isready), and Redis. docker ps shows (healthy) once they pass.


Backup and Restore​

# Dump the database from inside the container
docker exec -t multiwa-postgres pg_dump -U multiwa multiwa > multiwa-backup-$(date -u +%Y%m%d).sql

# Restore (assumes an empty target database)
cat multiwa-backup-YYYYMMDD.sql | docker exec -i multiwa-postgres psql -U multiwa -d multiwa

WhatsApp session files live in the sessions_data named volume and should be backed up alongside the database. They contain device authentication material and must be protected like a secret.


Production Checklist​

Before exposing the deployment to real users, confirm:

  • Strong, unique values for JWT_SECRET, JWT_REFRESH_SECRET, ENCRYPTION_KEY, and DB_PASSWORD (no defaults from .env.docker).
  • NODE_ENV=production is set in the API container.
  • CORS_ORIGINS only lists the real public origins; localhost removed.
  • NEXT_PUBLIC_API_URL matches the URL the browser will use, and the admin image has been rebuilt after any change.
  • TLS is terminated in front of the stack (Nginx, Traefik, Caddy, or a managed load balancer).
  • Database and Redis are not exposed to the public internet.
  • Daily database backups are scheduled and tested (restore at least once).
  • sessions_data volume is backed up and access-controlled.
  • Webhook delivery uses HTTPS endpoints and HMAC secrets where applicable.
  • Logs and metrics are shipped somewhere persistent (Loki, ELK, CloudWatch, etc.).

Troubleshooting​

SymptomLikely causeFix
Bind for 0.0.0.0:3333 failed: port is already allocatedAnother process or container holds the portStop the conflicting process, or override API_PORT / ADMIN_PORT in .env.
multiwa-api keeps restartingJWT_SECRET missing or DB URL wrongdocker logs multiwa-api and fix the env, then docker compose up -d.
Admin loads but every API call is Network ErrorNEXT_PUBLIC_API_URL mismatched, or CORS blockingVerify NEXT_PUBLIC_API_URL in .env, rebuild admin (docker compose build --no-cache admin), and confirm the value appears in CORS_ORIGINS.
Admin shows localhost:3333 from a browser on another machineBuild-time NEXT_PUBLIC_API_URL was left as the defaultSet NEXT_PUBLIC_API_URL to a reachable URL, then rebuild the admin image.
WhatsApp keeps asking for the QR code on every restartsessions_data volume not persistent or wrong mountConfirm the volume is declared (docker volume inspect multiwa_sessions_data) and not pruned.
pg_isready failsPostgres container unhealthy or wrong credentialsCheck docker logs multiwa-postgres; common causes are DB_PASSWORD changed after the volume was initialized.
redis-cli ping returns no PONGRedis not started or different passworddocker logs multiwa-redis; if you set a Redis password ensure REDIS_URL uses it.
curl http://localhost:3333/api/docs returns 404Wrong port, or the API is still bootingWait for MultiWA Gateway API running in the API logs; verify port mapping with docker port multiwa-api.

You can also use these as a quick sanity probe:

docker exec multiwa-api sh -c 'echo NODE_ENV=$NODE_ENV'   # expect: production
docker exec multiwa-api sh -c 'echo PORT=$API_PORT' # expect: 3333

← n8n Integration · Documentation Index · Development Guide →