# Deploying via Portainer End-to-end deploy steps for a fresh Portainer-managed host. Targets the standard cm-whatsapp-bot pair of images published by `scripts/publish.sh`. ## 0. Prerequisites - Portainer 2.x running on the target host (CE or EE both fine). - A Postgres reachable from that host (the `wabot` database with the pgcrypto / pg_trgm extensions enabled — run migrations from any machine that can reach the DB before the stack is brought up). - A pull credential for `gitea.04080616.xyz` — a Gitea personal access token with the `read:packages` scope. Generate one in Gitea → User Settings → Applications. - A reverse proxy / Cloudflare Tunnel pointing at `http://:` if the deploy needs to be reachable on the public domain (e.g. `wabot.04080616.xyz`). ## 1. Add the registry to Portainer Portainer → **Registries** → **+ Add registry** → Custom registry. | Field | Value | |---------------|-----------------------------| | Name | `gitea.04080616.xyz` | | Registry URL | `gitea.04080616.xyz` | | Authentication | enabled | | Username | your Gitea username | | Password | the read:packages PAT | Save. The registry must show as connected before continuing — if the test pull fails, the stack will hang on `pull` later. ## 2. Push the images (on your dev machine) ```bash # Login once (sudo path matches scripts/dev.sh by default) sudo docker login gitea.04080616.xyz # Push :latest. Tag explicitly with DOCKER_IMAGE_TAG=v1.x.y if you # want pinned-tag deploys (recommended for prod — never deploy # `latest` if you can avoid it; tag versions per release). NO_SUDO=1 ./scripts/publish.sh latest ``` `publish.sh` builds + pushes both images: - `gitea.04080616.xyz/yiekheng/cm-whatsapp-bot:` - `gitea.04080616.xyz/yiekheng/cm-whatsapp-web:` ## 3. Create the Portainer stack Portainer → **Stacks** → **+ Add stack**. **Name:** `cm-whatsapp-bot` **Build method:** "Web editor" or "Repository". Either is fine — "Repository" pointing at this repo's `master` and the file `docker-compose.portainer.yml` is the cleanest path because future deploys are just "Pull and redeploy" inside Portainer. **Web editor path:** copy the contents of [`docker-compose.portainer.yml`](../docker-compose.portainer.yml) into the editor verbatim. **Repository path:** | Field | Value | |------------------|-------------------------------------------------------------| | Repository URL | http://192.168.0.215:3000/yiekheng/cm_whatsapp_bot_v1.git | | Reference | refs/heads/master | | Compose path | docker-compose.portainer.yml | | Authentication | enabled (same Gitea PAT as step 1) | | Auto-update | optional — enabled lets Portainer redeploy on every push | ## 4. Set environment variables In the same stack form, scroll to **Environment variables** and add: | Key | Value | |---------------------------|------------------------------------------------| | `DATABASE_URL` | `postgres://wabot:PASS@192.168.0.210:5432/wabot` | | `AUTH_SECRET` | output of `scripts/gen_auth_secret.sh` | | `WEB_PORT` | host port (e.g. `9000`) | | `DOCKER_IMAGE_TAG` | `latest` (or a pinned `v1.x.y`) | | `OPERATOR_TOKEN_VERSION` | `1` (bump only when you want to invalidate every existing session) | | `BOT_LOG_LEVEL` | `info` | Optional tuning (defaults are fine for most installs): | Key | Default | When to bump | |---------------------------|---------|--------------| | `BOT_FIRE_CONCURRENCY` | `8` | More accounts firing in parallel | | `BOT_GROUP_CONCURRENCY` | `3` | More groups per fire — but careful with WhatsApp rate caps | | `BOT_MAX_SEND_PER_MINUTE` | `40` | Aged accounts can push toward 60 | ## 5. Run database migrations The stack does NOT auto-migrate on boot. Apply migrations from any machine that can reach the same Postgres: ```bash DATABASE_URL='postgres://...' \ ./scripts/db.sh migrate ``` If the journal is non-monotonic, the migrate runner refuses with a clear error and prints which `_journal.json` entry to bump (the guard added in commit 47d7c53 + the CI test in `apps/web/src/test/drizzle-journal-monotonic.test.ts`). Then seed the bootstrap operator + set its password: ```bash DATABASE_URL='postgres://...' SEED_OPERATOR_USERNAME=admin \ ./scripts/db.sh seed DATABASE_URL='postgres://...' \ ./scripts/set-password.sh admin # reads the password from stdin ``` ## 6. Deploy the stack In Portainer → click **Deploy the stack**. Watch the container list in **Containers**: - `cmbot-bot` should show *running, healthy* within ~20 s. - `cmbot-web` should show *running, healthy* within ~30 s (Next.js cold boot is the bottleneck). If a container shows *unhealthy*, check **Logs**: | Symptom | Likely cause | |----------------------------------------------|--------------| | `column "email" does not exist` | Migrations weren't applied. Run step 5. | | `Server is not configured for sign-in` | `AUTH_SECRET` blank or missing. Set it in stack env. | | `pg-boss: queue policy ...standard` | Harmless first-boot log; the bot force-flips it. | | `Stream Errored (restart required)` (Baileys) | Upstream noise; ignore unless pairing fails. | ## 7. First sign-in Visit `https:///login`, sign in as `admin` with the password set in step 5, and walk the [`docs/runbook.md`](runbook.md) smoke checklist before declaring the deploy good. ## 8. Future redeploys Two paths depending on how you set up step 3: **Web editor flow:** 1. Run `scripts/publish.sh ` on your dev machine. 2. In Portainer → Stack → "Update the stack" → "Re-pull image and redeploy". **Repository flow:** 1. Run `scripts/publish.sh `. 2. Commit any compose / env changes to master. 3. Portainer → Stack → "Pull and redeploy". (If auto-update is on, skip this — Portainer redeploys on every push.) Always pin a tag (`v1.4.2`) instead of `latest` for production — makes rollback a one-field stack edit instead of a republish. ## Rolling back In Portainer → Stack → set `DOCKER_IMAGE_TAG=v1.4.1` (or whatever the previous good tag was) → Re-pull and redeploy. The cmbot-* data volumes (sessions, media) are preserved across image swaps, so a rollback doesn't lose pairings or uploaded media. If the schema also rolled back, run the corresponding `down` SQL by hand — drizzle's migrator only goes forward, by design.