services: api: build: context: . target: api restart: unless-stopped environment: PORT: 3001 NODE_ENV: production DATABASE_URL: ${DATABASE_URL} REDIS_URL: ${REDIS_URL} ENCRYPTION_KEY: ${ENCRYPTION_KEY} GATEWAY_URL: http://gateway:3002 GATEWAY_TOKEN: ${GATEWAY_TOKEN} UPLOADX_TOKEN: ${UPLOADX_TOKEN} UPLOADX_URL: ${UPLOADX_URL} PUBLIC_URL: https://api.wapi.crafter.run # Audit request/response bodies. They carry message text and recipient numbers, redacted # of credentials but not of content, so set this to "off" for a metadata-only trail. AUDIT_BODIES: ${AUDIT_BODIES:-on} expose: - "3001" depends_on: - gateway networks: - dokploy-network # STATEFUL. One replica, always. A WhatsApp session is a WebSocket owned by exactly one # process, and two live sockets on one session escalates to a WhatsApp restriction # (see PLAN.md section 2). No published port and no domain: this RPC surface is protected # only by GATEWAY_TOKEN and the internal network. gateway: build: context: . target: gateway restart: unless-stopped deploy: replicas: 1 environment: PORT: 3002 NODE_ENV: production LOG_LEVEL: info DATABASE_URL: ${DATABASE_URL} REDIS_URL: ${REDIS_URL} GATEWAY_TOKEN: ${GATEWAY_TOKEN} expose: - "3002" networks: - dokploy-network # Stateless: redeploys freely, unlike the gateway. Consumes the gateway's Redis channel # and delivers webhooks with retry/backoff via BullMQ. webhook-worker: build: context: . target: webhook-worker restart: unless-stopped environment: NODE_ENV: production LOG_LEVEL: info DATABASE_URL: ${DATABASE_URL} REDIS_URL: ${REDIS_URL} depends_on: - gateway networks: - dokploy-network # Dashboard. Stateless, redeploys freely. # HOSTNAME=0.0.0.0 is mandatory: without it Next binds 127.0.0.1 inside the container and # Traefik answers 404 (documented in the vps skill). web: build: context: . target: web args: NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY} restart: unless-stopped environment: NODE_ENV: production HOSTNAME: "0.0.0.0" PORT: "3000" DATABASE_URL: ${DATABASE_URL} REDIS_URL: ${REDIS_URL} ENCRYPTION_KEY: ${ENCRYPTION_KEY} GATEWAY_URL: http://gateway:3002 GATEWAY_TOKEN: ${GATEWAY_TOKEN} CLERK_SECRET_KEY: ${CLERK_SECRET_KEY} NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY} # Page renders take the internal hop: no TLS, no public round-trip, no dependency on # outbound DNS from inside the container. The doctor deliberately uses the public URL, # because the edge is part of what it is checking. API_INTERNAL_URL: http://api:3001 API_PUBLIC_URL: https://api.wapi.crafter.run # Where the CLI is told to send people to approve a login. Derived from the request it would # be the container's bind address, which is not a URL anybody can open. WEB_PUBLIC_URL: https://wapi.crafter.run expose: - "3000" depends_on: - gateway - api networks: - dokploy-network # Nightly pg_dump. Postgres is the whole product: lose baileys_creds and every session # re-pairs, which tonight proved is not theoretical. pg_dump must match the server major # version, so this pins postgres:18 to match the managed instance. # # Honest limitation: the volume lives on this host. That covers a bad migration, an # accidental DROP, or Dokploy recreating the database container -- but not losing the box. # Set BACKUP_S3_BUCKET to also ship off-host. backup: build: context: . target: backup restart: unless-stopped environment: DATABASE_URL: ${DATABASE_URL} BACKUP_DIR: /backups BACKUP_KEEP: "7" BACKUP_INTERVAL_SECONDS: "86400" volumes: - wapi-backups:/backups networks: - dokploy-network volumes: wapi-backups: networks: dokploy-network: external: true