Self-host PulseWatch with Docker
PulseWatch is fully self-hostable. This post is a copy-paste guide to running the whole stack on your own machine or server with Docker, using the published image ghcr.io/optomake-srl/pulsewatch:latest. By the end you will have the app and a Postgres database on a shared Docker network, a health check you can probe, and notes for running it in production.
Prerequisites
- Docker Engine 24+ and the Docker CLI (docker --version).
- A host with at least 1 GB RAM and 2 vCPU (2 GB+ recommended for production).
- A domain name and a reverse proxy (Caddy, nginx or Traefik) if you want HTTPS in production.
- Postgres 16+ - either the bundled container below or a managed database.
Pick a strong random value for every secret. You can generate one with:
openssl rand -hex 32
Environment variables
These are read by the app at startup. PORT, DATABASE_URL, SECRET_KEY, BETTER_AUTH_SECRET and BETTER_AUTH_URL are required.
| Variable | Required | Example / default | Notes |
|---|---|---|---|
PORT |
yes | 10000 | HTTP port the app listens on. Keep 10000 unless you change the mapping. |
DATABASE_URL |
yes | postgres://pulsewatch:change-me@pulsewatch-db:5432/pulsewatch | Postgres connection string. Use the service name as host on the shared network. |
SECRET_KEY |
yes | <random 32+ chars> | App secret used for signing. Generate with openssl rand. |
BETTER_AUTH_SECRET |
yes | <random 32+ chars> | Secret for Better Auth sessions and tokens. |
BETTER_AUTH_URL |
yes | https://pulsewatch.example.com | Public URL of the app. Must match what users open in the browser; used in auth callbacks. |
NODE_ENV |
no | production | Set to production for production deployments. |
Optional integrations
- OAuth: GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET (only if you enable those providers).
- Sentry: SENTRY_DSN for error reporting.
- PostHog: NEXT_PUBLIC_POSTHOG_KEY and NEXT_PUBLIC_POSTHOG_HOST for analytics.
2. Run the database
This starts Postgres 16 on the shared network, persists data in a named volume, and exposes port 5432 only on localhost.
docker run -d --name pulsewatch-db --network pulsewatch -e POSTGRES_USER=pulsewatch -e POSTGRES_PASSWORD=change-me -e POSTGRES_DB=pulsewatch -v pulsewatch-pg:/var/lib/postgresql/data -p 127.0.0.1:5432:5432 --restart unless-stopped postgres:16-alpine
3. Run PulseWatch
Wait a few seconds for Postgres to be ready, then start the app on the same network. Replace the secrets and the public URL with your own.
docker run -d --name pulsewatch --network pulsewatch -p 10000:10000 -e PORT=10000 -e DATABASE_URL="postgres://pulsewatch:change-me@pulsewatch-db:5432/pulsewatch" -e SECRET_KEY="replace-with-openssl-rand-hex-32" -e BETTER_AUTH_SECRET="replace-with-openssl-rand-hex-32" -e BETTER_AUTH_URL="https://pulsewatch.example.com" -e NODE_ENV="production" --restart unless-stopped ghcr.io/optomake-srl/pulsewatch:latest
4. Health check
The app exposes /health on port 10000. Probe it to confirm everything is up:
curl -fsS http://localhost:10000/health
A 200 OK means the app and its database connection are healthy. Point your reverse proxy at port 10000 and set BETTER_AUTH_URL to the public HTTPS URL.
Production guidance
- Managed Postgres: for production, point
DATABASE_URLat a managed database (Neon, Supabase, RDS, etc.) and remove the bundledpulsewatch-dbcontainer. Back up with your provider's tooling orpg_dump. - Remove bundled db: if you keep the container, restrict port 5432 to localhost (as above) and never expose it publicly.
- Watchtower: auto-update the app container when a new image is published:
docker run -d --name watchtower --restart unless-stopped -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --cleanup --interval 3600 pulsewatch
Docker Swarm build note: the published image is already multi-arch. If you build your own image for Swarm, build for both architectures and push to your registry: docker buildx build --platform linux/amd64,linux/arm64 -t your-registry/pulsewatch:latest --push . Reference the image (not a local build) in your stack file.
Optional: run a worker (probe)
PulseWatch Probe lets you report region-level check results from your own host. It ships as a separate image:
docker run -d --name pulsewatch-probe --restart unless-stopped -e PULSEWATCH_URL=https://pulsewatch.example.com -e REGION=eu ghcr.io/optomake-srl/pulsewatch-probe:latest
The probe prints a pairing URL and code in its logs; approve it while signed in at /pair-probe and it starts reporting. See the PulseWatch Probe post for pairing details.
The PulseWatch Team
💬 Comments
Questions, corrections or thoughts on this post? Join the thread below.