# Portainer-ready stack. Pulls cm-whatsapp-{web,bot} from # gitea.04080616.xyz/yiekheng instead of building from source — drop # this file into a Portainer "Stack" (Repository or Web editor) and # fill the env vars in the Portainer UI. # # Differences vs docker-compose.base.yml: # - No `build:` blocks (Portainer pulls only). # - Named volumes (cmbot-data, cmbot-sessions, cmbot-media) instead # of host bind-mounts so the operator doesn't need shell access # to manage persistent state. # - Ports section on `web` so the operator can route a reverse # proxy / Cloudflare Tunnel directly at the container. # - `restart: unless-stopped` on both services. # # Required env vars (set in Portainer → Stack → Environment variables): # DATABASE_URL postgres://USER:PASS@HOST:5432/wabot # AUTH_SECRET 32-byte random hex (use scripts/gen_auth_secret.sh # on any machine and copy the output) # WEB_PORT host port for the web container (default 9000) # # Optional: # DOCKER_IMAGE_TAG registry tag to deploy (default: latest) # OPERATOR_TOKEN_VERSION session-cookie kill switch (default: 1) # BOT_FIRE_CONCURRENCY pg-boss workers (default: 8) # BOT_GROUP_CONCURRENCY per-account parallel sends (default: 3) # BOT_MAX_SEND_PER_MINUTE per-account token-bucket rate (default: 40) # BOT_LOG_LEVEL pino log level (default: info) # # Registry auth: Portainer needs a pull credential for # gitea.04080616.xyz before you start the stack: # Portainer → Registries → Add registry # Name: gitea.04080616.xyz # URL: gitea.04080616.xyz # Username: # Token: # After adding, edit each service in the stack and set "Registry" to # the one you just added so the pull resolves. services: bot: image: gitea.04080616.xyz/yiekheng/cm-whatsapp-bot:${DOCKER_IMAGE_TAG:-latest} container_name: cmbot-bot restart: unless-stopped environment: NODE_ENV: production DATABASE_URL: ${DATABASE_URL} DATA_DIR: /data SESSIONS_DIR: /data/sessions MEDIA_DIR: /data/media BOT_HEALTH_PORT: 8081 BOT_LOG_LEVEL: ${BOT_LOG_LEVEL:-info} BOT_FIRE_CONCURRENCY: ${BOT_FIRE_CONCURRENCY:-8} BOT_GROUP_CONCURRENCY: ${BOT_GROUP_CONCURRENCY:-3} BOT_MAX_SEND_PER_MINUTE: ${BOT_MAX_SEND_PER_MINUTE:-40} volumes: - cmbot-sessions:/data/sessions - cmbot-media:/data/media healthcheck: test: - "CMD-SHELL" - "wget -qO- --timeout=2 http://127.0.0.1:8081/health >/dev/null || exit 1" interval: 30s timeout: 5s retries: 3 start_period: 20s networks: - cmbot web: image: gitea.04080616.xyz/yiekheng/cm-whatsapp-web:${DOCKER_IMAGE_TAG:-latest} container_name: cmbot-web restart: unless-stopped depends_on: - bot environment: NODE_ENV: production DATABASE_URL: ${DATABASE_URL} DATA_DIR: /data MEDIA_DIR: /data/media WEB_PORT: 3000 AUTH_SECRET: ${AUTH_SECRET} OPERATOR_TOKEN_VERSION: ${OPERATOR_TOKEN_VERSION:-1} volumes: # Web reads media from the same persistent volume the bot wrote. - cmbot-media:/data/media:ro ports: # Maps the Next.js port (3000 inside the container) to whatever # WEB_PORT the operator set. The reverse proxy / Cloudflare Tunnel # in front of this host points at :${WEB_PORT}. - "${WEB_PORT:-9000}:3000" healthcheck: test: - "CMD-SHELL" - "wget -qO- --timeout=2 http://127.0.0.1:3000/api/health >/dev/null || exit 1" interval: 30s timeout: 5s retries: 3 start_period: 30s networks: - cmbot volumes: cmbot-sessions: name: cmbot-sessions cmbot-media: name: cmbot-media networks: cmbot: driver: bridge name: cmbot