Install OpenWA
This guide installs a running OpenWA instance — the REST API plus the bundled web dashboard — and verifies it with a health check. Pick one of two paths:
- Docker Compose — recommended. One command, no Node toolchain, Chromium bundled in the image.
- From source — when you want hot reload or to hack on OpenWA itself. Needs Node.js 22.
Both paths serve everything on port 2785: the API under /api, the dashboard at /,
and interactive Swagger docs at /api/docs.
- A 64-bit Linux, macOS, or Windows host (
linux/amd64orlinux/arm64). - A phone with WhatsApp installed — you scan a QR code to connect a session later.
- For the Docker path: Docker Engine 20.10+ with the Compose v2 plugin (
docker compose). - For the source path: Node.js 22 LTS and npm.
Choose your path
Prerequisites in detail
| Requirement | Docker Compose | From source |
|---|---|---|
| Runtime | Docker Engine + Compose v2 | Node.js 22 LTS, npm |
| WhatsApp engine | Bundled (whatsapp-web.js default) | Bundled; whatsapp-web.js needs a Chromium binary, baileys needs none |
| Chromium | Included in the image | Auto-downloaded by Puppeteer, or point PUPPETEER_EXECUTABLE_PATH at a system one |
| RAM | ~2 GB for the default engine (a headless Chromium per session) | Same |
| Disk | ~2 GB for the image + session data | ~2 GB for node_modules + session data |
OpenWA ships two WhatsApp engines, selected by ENGINE_TYPE. The default
whatsapp-web.js drives a headless Chromium per session (heavier, ~2 GB
RAM guideline). baileys is a WebSocket client with no browser (much
lighter). You can switch later in Configuration; leave it
unset for now to use the default.
Option A: Docker Compose (recommended)
OpenWA ships two Compose files. Use the one that matches your goal:
| File | What it is | Database | Use it when |
|---|---|---|---|
docker-compose.dev.yml | Single-container quick start (production image, local SQLite, bind-mounted ./data) | SQLite | You want OpenWA up in one command |
docker-compose.yml | Core API plus optional backing services behind Compose profiles | SQLite (default), PostgreSQL optional | You want PostgreSQL, Redis, or S3-compatible storage |
Neither file runs a separate dashboard container — the dashboard SPA is built into the API image and served by the same process on port 2785.
Quick start (single container)
Clone the repository and bring up the quick-start stack. It builds the production image and
runs it against a local SQLite database with ./data bind-mounted for persistence:
git clone https://github.com/rmyndharis/OpenWA.git
cd OpenWA
docker compose -f docker-compose.dev.yml up -d
The first build takes a few minutes (it installs Chromium). When it finishes, open:
| Surface | URL |
|---|---|
| Dashboard | http://localhost:2785 |
| REST API | http://localhost:2785/api |
| Swagger UI | http://localhost:2785/api/docs |
Despite the dev in its name, docker-compose.dev.yml runs the same production image — there
is no source mount and no live reload. For a hot-reload development loop, use
Option B: From source.
Full stack (PostgreSQL, Redis, storage)
The main docker-compose.yml runs the core API on SQLite and local storage by default, and
gates optional backing services behind Compose profiles:
# Core only — SQLite, local storage
docker compose up -d
# Add PostgreSQL
docker compose --profile postgres up -d
# Everything — PostgreSQL, Redis, and MinIO (S3-compatible storage)
docker compose --profile full up -d
| Profile | Adds | Selected by |
|---|---|---|
postgres | PostgreSQL 16 | DATABASE_TYPE=postgres |
redis | Redis 7 cache/queue | REDIS_ENABLED=true |
minio | MinIO, S3-compatible storage | STORAGE_TYPE=s3 |
full | All of the above | — |
Starting a profile only launches the container. You still point OpenWA at it with the matching environment variable — see Configuration for the full set.
The postgres and minio images refuse to start with an empty password, and OpenWA's
production boot guard rejects empty or placeholder secrets. Set DATABASE_PASSWORD (for
PostgreSQL) and S3_ACCESS_KEY / S3_SECRET_KEY (for MinIO) in your .env before starting
those profiles.
Set your environment
The Compose files read configuration from a .env file in the project root. Copy the template
and edit it before your first real start:
cp .env.example .env
For local testing you can leave the defaults. The most relevant knobs:
| Variable | Default | Purpose |
|---|---|---|
API_PORT | 2785 | Host port the API and dashboard bind to. Honored only by docker-compose.yml; the quick-start docker-compose.dev.yml hardcodes the host port 2785 and ignores this variable |
API_MASTER_KEY | empty | Set a strong key, or let OpenWA generate one on first boot |
DATABASE_TYPE | sqlite | sqlite or postgres |
STORAGE_TYPE | local | local or s3 |
ENGINE_TYPE | unset | Pin whatsapp-web.js or baileys; unset uses the dashboard default |
If you do not set API_MASTER_KEY, OpenWA generates a random admin key on first boot and prints
it in the startup banner (also written to data/.api-key). You need that key for every
authenticated request, so grab it now:
docker compose -f docker-compose.dev.yml logs openwa | grep -i "api key"
The quick-start stack's service is named openwa (its container is openwa-api), so you must
pass both -f docker-compose.dev.yml and the openwa service name. On the full
docker-compose.yml stack the service is openwa-api, so there you run
docker compose logs openwa-api | grep -i "api key" instead.
Bind address and TLS
Both Compose files bind the API to 127.0.0.1 (localhost only) and serve plain HTTP.
The API key travels in the X-API-Key header in cleartext over plain HTTP. For any
internet-facing deployment, put your own TLS reverse proxy (nginx, Caddy, a cloud load
balancer, or a Kubernetes Ingress) in front and terminate TLS there. See
Deployment for the full setup.
To reach the quick-start stack from another machine on a trusted network, set BIND_HOST=0.0.0.0
in your .env — but only behind that TLS proxy.
BIND_HOST only applies to docker-compose.dev.ymlOnly the quick-start docker-compose.dev.yml reads BIND_HOST. The full docker-compose.yml
hardcodes its port mapping to 127.0.0.1:${API_PORT:-2785}:2785 and ignores BIND_HOST, so
setting it there has no effect. To expose that stack, edit the host side of its port mapping (and
keep it behind the TLS proxy).
Using Podman instead of Docker?
Rootless Podman works if you start its socket and point DOCKER_HOST at it before running
docker compose:
systemctl --user start podman.socket
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock
Add the export line to your shell profile (~/.bashrc, ~/.zshrc) to make it permanent.
Now skip ahead to Verify the install.
Option B: From source
Use this path for a hot-reload development loop or to modify OpenWA. It requires Node.js 22 LTS.
git clone https://github.com/rmyndharis/OpenWA.git
cd OpenWA
# Install dependencies — the postinstall hook also installs the dashboard's dependencies
npm install
OpenWA auto-generates its config on first run, so you can start without writing a .env file.
Run with hot reload
npm run dev
This starts the API on port 2785 and the dashboard on the Vite dev server at port 2886 with hot reload:
| Surface | URL |
|---|---|
| Dashboard (Vite, hot reload) | http://localhost:2886 |
| REST API | http://localhost:2785/api |
| Swagger UI | http://localhost:2785/api/docs |
In source-dev mode the dashboard runs separately on 2886. In every other mode (both Docker files, and the production build below) the dashboard is served by the API on 2785.
Run a production-style build locally
To serve the bundled dashboard from the API on a single port, build the backend and the dashboard, then start:
npm run build # nest build — compiles the backend to dist/ only
npm run dashboard:build # builds the dashboard SPA into dashboard/dist
npm run start:prod
npm run build runs nest build and produces the backend in dist/ — it does not build the
dashboard. The API only serves the SPA when dashboard/dist/index.html exists, which
npm run dashboard:build (equivalently cd dashboard && npm run build) creates. Skip that step
on a clean checkout and you get a working API at http://localhost:2785/api but a blank
dashboard at http://localhost:2785.
With both builds done, the dashboard is at http://localhost:2785, the same as the Docker path.
The default whatsapp-web.js engine needs a Chromium or Chrome binary. Puppeteer downloads one
during npm install; if it is not found at runtime, set PUPPETEER_EXECUTABLE_PATH to a system
binary (for example /usr/bin/chromium). To avoid Chromium entirely, set ENGINE_TYPE=baileys.
Verify the install
The health endpoints are public — no API key required — and live under the /api prefix.
Run a basic check:
curl http://localhost:2785/api/health
A healthy instance returns 200 OK:
{
"status": "ok",
"timestamp": "2026-06-26T10:15:00.000Z",
"version": "0.7.6"
}
Then confirm OpenWA is ready to serve traffic. The readiness probe checks that both the auth/audit and data databases respond:
curl -i http://localhost:2785/api/health/ready
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "ok",
"details": {
"mainDatabase": { "status": "up" },
"dataDatabase": { "status": "up" }
}
}
If readiness returns 200, OpenWA is up and you can connect a WhatsApp account.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
curl: connection refused on 2785 | Container still building, or port not bound | Wait for the build to finish; check docker compose ps and docker compose logs openwa-api |
/api/health/ready returns 503 | A database isn't reachable, or the app is shutting down | Inspect details in the body; for PostgreSQL, confirm DATABASE_PASSWORD is set and the postgres profile is up |
| PostgreSQL or MinIO container exits immediately | Missing credentials | Set DATABASE_PASSWORD / S3_ACCESS_KEY / S3_SECRET_KEY in .env — these images refuse empty secrets |
| Session stuck at "authenticating" after scanning the QR | Slow first boot (WSL2, low-resource host) | Raise WWEBJS_AUTH_TIMEOUT_MS (for example 120000) in .env |
whatsapp-web.js engine fails to launch a browser | No Chromium found (source install) | Set PUPPETEER_EXECUTABLE_PATH, or switch to ENGINE_TYPE=baileys |
| Port 2785 already in use | Another process holds the port | On docker-compose.yml, change API_PORT in .env. On the quick-start docker-compose.dev.yml (which ignores API_PORT), edit the host side of the '${BIND_HOST:-127.0.0.1}:2785:2785' mapping. From source, set PORT |
For anything not covered here, see Troubleshooting.
Next steps
- Configuration — set your API key, database, storage, and engine.
- Connect a session — scan the QR code and authenticate a WhatsApp account.
- Quick start — send your first message end to end.