Deploy a WhatsApp Bulk Messaging System with Docker

≈ 9 min read

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:

ComponentRecommendationWhy
VPS (Linux)2 GB RAM, 1–2 vCPU, 10–20 GB diskThe worker runs headless Chromium; 1 GB is not enough
Docker + ComposeDocker Engine 24+ with the compose pluginEvery service runs as a container
Supabase projectFree tierHolds persistent records (messages/session)
PhoneWhatsApp installed, active linePairs 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.

ContainerJob
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.

  1. On your phone, open WhatsApp > Settings > Linked Devices.
  2. Tap Link a Device and scan the QR in the panel.
  3. The session is stored via LocalAuth on 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.com and panel.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

SymptomLikely causeFix
Worker keeps restartingNot enough RAM for ChromiumMove to at least 2 GB RAM; add swap
Messages go out at the wrong timeTZ unset (UTC)Add TZ=Europe/Istanbul to .env and restart
QR not showing / blankWorker not ready yetWait and watch docker compose logs -f whatsapp-worker
Queue never drainsWrong REDIS_PASSWORDserver and worker must share the password; align .env
Session drops on every restartLocalAuth volume not persistentMake 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.

Open the panel →