Deploy a WhatsApp Bulk Messaging System with Docker
Standing up a self-hosted WhatsApp bulk messaging system that charges nothing per message is easier than it sounds with Docker: you run a database schema, fill in a single .env file and type docker compose up. This guide walks through every step in order, from requirements to QR pairing, from a production deployment with Dokploy to troubleshooting. The system is QR-based (whatsapp-web.js), not the official Business API — so there is no application, no template approval and no per-conversation fee.
1. Before you start: requirements
You will need the following in place before deploying:
| Component | Recommendation | Why |
|---|---|---|
| VPS (Linux) | 2 GB RAM, 1–2 vCPU, 10–20 GB disk | The worker runs headless Chromium; 1 GB is not enough |
| Docker + Compose | Docker Engine 24+ with the compose plugin | Every service runs as a container |
| Supabase project | Free tier | Holds persistent records (messages/session) |
| Phone | WhatsApp installed, active line | Pairs the session via QR |
To understand how this architecture works and why hosting it on your own server pays off, see self-hosted WhatsApp messaging system.
2. What Docker Compose brings up
A single docker compose up starts four services. Each is deliberately separated so the rate limit lives in exactly one place — the worker.
| Container | Job |
|---|---|
| web (React/nginx) | Panel UI, composing sends, scanning the QR |
| server (Express :3200) | Validates requests and queues jobs; never sends itself |
| redis (BullMQ) | Job queue and scheduled sends; persistent via AOF |
| whatsapp-worker (Node 20 + Chromium) | The only sender; applies anti-ban timing, concurrency 1 |
The server never sending messages is a key design choice: even 100 requests/second to the panel cannot push WhatsApp traffic past human speed. For more on the WhatsApp Web protocol the worker uses, see what is whatsapp-web.js.
3. Step 1 — Set up the database schema
First create a free Supabase project. Open its SQL Editor, paste the schema file from the repo and run it. This creates the notification_messages and whatsapp_web_sessions tables.
# Clone the repo onto your server git clone https://github.com/user/whatsapp-oto-message.git cd whatsapp-oto-message # Paste the contents of supabase/migrations/001_init.sql # into Supabase > SQL Editor > New query and click Run
Then, from the Supabase settings, note the Project URL and the service_role key; you will add them to .env in the next step.
4. Step 2 — Fill in the .env file
Copy the example file and generate the secrets strongly. Using openssl for random secrets is far safer than guessable passwords.
cp .env.example .env # Generate strong secrets openssl rand -hex 32 # for REDIS_PASSWORD openssl rand -hex 32 # for WA_CONTROL_SECRET
The core fields to fill in .env:
REDIS_PASSWORD=your_generated_32_byte_value WA_CONTROL_SECRET=your_generated_32_byte_value SUPABASE_URL=https://xxxx.supabase.co SUPABASE_SERVICE_ROLE_KEY=eyJ... TZ=Europe/Istanbul
Do not skip TZ. The anti-ban window (05:00–23:00) is based on local time. Without TZ=Europe/Istanbul the container stays on UTC and sends can drift by hours. For how scheduling works, see scheduling WhatsApp messages.
5. Step 3 — Start the services
One command builds every image and starts the containers in the background:
docker compose up -d --build # Check the status of the services docker compose ps # Follow the worker logs (the QR and sends show up here) docker compose logs -f whatsapp-worker
The first build can take a few minutes because of Chromium's dependencies. All four services should read running in docker compose ps. If you want to trigger sends from an external app over an HTTP API, HTTP API integration covers the send endpoint and its authentication.
6. Step 4 — Scan the QR code
Once the containers are up, open the panel in a browser and go to the Connection page. The QR code produced by the worker appears there.
- On your phone, open WhatsApp > Settings > Linked Devices.
- Tap Link a Device and scan the QR in the panel.
- The session is stored via
LocalAuthon a persistent volume, so you won't need to rescan even if the container restarts.
Your computer does not need to stay on — the system runs on the server. Your phone only needs to reach the internet roughly every two weeks to keep the session alive.
7. Going to production with Dokploy
docker compose up is fine for development, but in production you want a domain, a TLS certificate and automatic restarts. A self-hosted PaaS panel like Dokploy manages the same compose file and makes this simple:
- Define separate domains for the site and the panel (e.g.
dispatch-wa.comandpanel.dispatch-wa.com). - Attach the services to a shared
dokploy-network. - Get automatic HTTPS via Let's Encrypt.
- Define persistent volumes for Redis data and the WhatsApp session so a redeploy never loses the session.
Ethics and law: Deploy this only to message people who have consented to hear from you. Unsolicited bulk messaging violates both WhatsApp's terms and data-protection law (GDPR in the EU, KVKK and electronic-communication rules in Turkey); every marketing message needs an opt-out line (e.g. "reply STOP"). See consent-compliant messaging.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Worker keeps restarting | Not enough RAM for Chromium | Move to at least 2 GB RAM; add swap |
| Messages go out at the wrong time | TZ unset (UTC) | Add TZ=Europe/Istanbul to .env and restart |
| QR not showing / blank | Worker not ready yet | Wait and watch docker compose logs -f whatsapp-worker |
| Queue never drains | Wrong REDIS_PASSWORD | server and worker must share the password; align .env |
| Session drops on every restart | LocalAuth volume not persistent | Make the worker volume a named volume in compose |
When you run the system for the first time, test with a small list. The rate ceiling is already safe, but if you want to ramp up gradually on a fresh number, apply a number warm-up approach.
Frequently asked questions
What server do I need for the deployment?
Because headless Chromium needs memory, a VPS with at least 2 GB RAM is recommended. On 1 GB servers the worker can crash while Chromium launches. 1–2 vCPU and 10–20 GB disk are enough.
How many containers does Docker Compose bring up?
Four: web (React/nginx), server (Express :3200), redis and whatsapp-worker (Node 20 + Chromium). Persistent data lives in an external free Supabase project.
Why does TZ matter?
The anti-ban window of 05:00–23:00 is local time. Without TZ=Europe/Istanbul the server stays on UTC and messages go out at unexpected hours.
How do I run it in production?
Manage the same compose file through a panel like Dokploy to set separate domains, TLS and automatic restarts. The QR session and Redis data live on persistent volumes.
Finish the deploy, start sending
Schema, .env and docker compose up — three steps on your own server. Open the panel and scan the QR.