Skip to main content
Version: v0.7.6

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.

Prerequisites
  • A 64-bit Linux, macOS, or Windows host (linux/amd64 or linux/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

RequirementDocker ComposeFrom source
RuntimeDocker Engine + Compose v2Node.js 22 LTS, npm
WhatsApp engineBundled (whatsapp-web.js default)Bundled; whatsapp-web.js needs a Chromium binary, baileys needs none
ChromiumIncluded in the imageAuto-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
Which engine, and why RAM matters

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.

OpenWA ships two Compose files. Use the one that matches your goal:

FileWhat it isDatabaseUse it when
docker-compose.dev.ymlSingle-container quick start (production image, local SQLite, bind-mounted ./data)SQLiteYou want OpenWA up in one command
docker-compose.ymlCore API plus optional backing services behind Compose profilesSQLite (default), PostgreSQL optionalYou 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:

SurfaceURL
Dashboardhttp://localhost:2785
REST APIhttp://localhost:2785/api
Swagger UIhttp://localhost:2785/api/docs
This is a smoke test, not hot reload

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
ProfileAddsSelected by
postgresPostgreSQL 16DATABASE_TYPE=postgres
redisRedis 7 cache/queueREDIS_ENABLED=true
minioMinIO, S3-compatible storageSTORAGE_TYPE=s3
fullAll 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.

PostgreSQL, Redis, and S3 ship no default credentials

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:

VariableDefaultPurpose
API_PORT2785Host 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_KEYemptySet a strong key, or let OpenWA generate one on first boot
DATABASE_TYPEsqlitesqlite or postgres
STORAGE_TYPElocallocal or s3
ENGINE_TYPEunsetPin 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.

Do not expose plain HTTP to the internet

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

Only 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:

SurfaceURL
Dashboard (Vite, hot reload)http://localhost:2886
REST APIhttp://localhost:2785/api
Swagger UIhttp://localhost:2785/api/docs
Dev dashboard port differs

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.

Chromium for the default engine

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

SymptomCauseFix
curl: connection refused on 2785Container still building, or port not boundWait for the build to finish; check docker compose ps and docker compose logs openwa-api
/api/health/ready returns 503A database isn't reachable, or the app is shutting downInspect details in the body; for PostgreSQL, confirm DATABASE_PASSWORD is set and the postgres profile is up
PostgreSQL or MinIO container exits immediatelyMissing credentialsSet DATABASE_PASSWORD / S3_ACCESS_KEY / S3_SECRET_KEY in .env — these images refuse empty secrets
Session stuck at "authenticating" after scanning the QRSlow first boot (WSL2, low-resource host)Raise WWEBJS_AUTH_TIMEOUT_MS (for example 120000) in .env
whatsapp-web.js engine fails to launch a browserNo Chromium found (source install)Set PUPPETEER_EXECUTABLE_PATH, or switch to ENGINE_TYPE=baileys
Port 2785 already in useAnother process holds the portOn 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