Installation & configuration
The easiest way to run PulseDeck is Docker — one command, nothing to configure. That’s all most self-hosters need, and it’s covered first.
The sections after it — manual setup, production, and the environment reference — are optional. You only need them if you’re running without Docker or deploying to production. PulseDeck itself only requires PostgreSQL (Redis is optional, for multi-replica realtime), and the API applies its own migrations on startup, so there’s never a separate migration step.
Run with Docker (recommended)
Section titled “Run with Docker (recommended)”You need Docker, plus make and
openssl (preinstalled on macOS/Linux).
git clone https://github.com/me-shaon/pulsedeck.git pulsedeckcd pulsedeckmake setup # writes .env and generates strong secrets (idempotent)docker compose up # or `make up` to build + run detachedThis starts the web app, the API, and PostgreSQL together. Open http://localhost:3000, create your account and workspace, and you’re running — then head to the Quickstart to connect an agent.
Manual setup (without Docker)
Section titled “Manual setup (without Docker)”Prefer not to use Docker, or working on PulseDeck’s own code? Run it directly.
Prerequisites
Section titled “Prerequisites”- Node.js 22+
- pnpm 9.15+ —
corepack enable && corepack prepare pnpm@9.15.0 --activate - PostgreSQL 16+ — running and reachable
1. Install dependencies
Section titled “1. Install dependencies”git clone https://github.com/me-shaon/pulsedeck.git pulsedeckcd pulsedeckpnpm install --frozen-lockfile2. Create the database
Section titled “2. Create the database”psql -U postgres -c "CREATE USER pulsedeck WITH PASSWORD 'pulsedeck';"psql -U postgres -c "CREATE DATABASE pulsedeck OWNER pulsedeck;"# Postgres 15+ locks down the public schema — grant it explicitly:psql -U postgres -d pulsedeck -c "GRANT ALL ON SCHEMA public TO pulsedeck;"3. Configure the API
Section titled “3. Configure the API”The API reads apps/api/.env (real environment variables always win). The two
required values:
cat > apps/api/.env <<'EOF'DATABASE_URL=postgres://pulsedeck:pulsedeck@localhost:5432/pulsedeckAUTH_SECRET=replace-with-32+-random-chars # e.g. `openssl rand -base64 48`EOF4. Run it
Section titled “4. Run it”pnpm devThis starts the API on :3001 and the web dev server on :3000 (Vite
proxies /api → the API, keeping cookies first-party). Migrations run on API
start. Open http://localhost:3000.
Production
Section titled “Production”pnpm buildThis emits the API bundle to apps/api/dist and the static web bundle to
apps/web/dist.
Start the API
Section titled “Start the API”It applies pending migrations, then listens:
cd apps/apiNODE_ENV=production \DATABASE_URL=postgres://pulsedeck:pulsedeck@localhost:5432/pulsedeck \AUTH_SECRET=your-32+-char-secret \BETTER_AUTH_URL=https://your.domain \PORT=3001 \node dist/index.jsKeep it alive with a process manager (systemd, pm2, …).
Serve the web bundle
Section titled “Serve the web bundle”apps/web/dist is static files. The SPA calls /api on its own origin, so put
a reverse proxy in front that serves the static files and forwards /api to
the API.
server { listen 80; server_name your.domain; root /path/to/pulsedeck/apps/web/dist; index index.html;
location /api/ { proxy_pass http://127.0.0.1:3001; # keeps the /api prefix proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Server-Sent Events (live updates): flush immediately, don't time out. proxy_set_header Connection ''; proxy_buffering off; proxy_read_timeout 1h; }
location / { try_files $uri $uri/ /index.html; # SPA client-side routing }}Caddy alternative:
your.domain { root * /path/to/pulsedeck/apps/web/dist handle /api/* { reverse_proxy 127.0.0.1:3001 } handle { try_files {path} /index.html file_server }}Put TLS in front (Caddy does this automatically; for nginx use certbot) and set
BETTER_AUTH_URL=https://your.domain to match.
Secrets
Section titled “Secrets”PulseDeck fails fast on weak secrets so a known value can never reach production.
POSTGRES_PASSWORD— the one place the database secret lives. The Docker stack builds the API’sDATABASE_URLfrom it (hostpostgres), so you set it once. WithNODE_ENV=productionthe API refuses to boot on a default likepulsedeck,postgres, orpassword.AUTH_SECRET— signs auth sessions; min 32 chars. WithNODE_ENV=productionthe API rejects placeholder values (change-me,insecure,dev-only, …). Rotating it invalidates existing sessions (users re-login); no data is lost.
make setup generates both for you. For a non-Docker or managed setup, generate
them by hand — openssl rand -base64 48 for AUTH_SECRET, openssl rand -hex 24 for POSTGRES_PASSWORD.
The bundled Postgres no longer publishes a host port in the production compose
file (it’s reachable only on the internal Docker network). The dev overlay
(make dev) re-exposes it on 127.0.0.1:${POSTGRES_PORT} for local tooling.
Environment reference
Section titled “Environment reference”All options below are read by the API. Only POSTGRES_PASSWORD (Docker) or
DATABASE_URL (non-Docker) and AUTH_SECRET are required; everything else has
a sensible default.
| Variable | Purpose | Default |
|---|---|---|
POSTGRES_PASSWORD | Password for the bundled Postgres; the Docker stack builds DATABASE_URL from it. Production rejects defaults. | (required for Docker) |
DATABASE_URL | PostgreSQL connection string. Migrations run automatically on startup. Commented out for Docker (built from POSTGRES_PASSWORD). | built from POSTGRES_PASSWORD |
AUTH_SECRET | Secret for signing auth sessions. Min 32 chars; openssl rand -base64 48. Production rejects placeholders. | (required) |
NODE_ENV | development, test, or production. Production enables the weak-secret guards above. | development |
BETTER_AUTH_URL | Public origin users hit (for OAuth callbacks + request validation). No trailing slash. | inferred from request |
TRUST_PROXY | Reverse-proxy trust: true/false, a hop count (1), or an IP/CIDR. Set to 1 behind the bundled nginx so rate limits key off the real client IP. | false |
DEPLOYMENT_MODE | self-host or cloud. Drives the defaults for signup, billing, accounts, and retention. | self-host |
SIGNUP_MODE | setup (first-run wizard only), open (public sign-up), or invite (invite-only). | setup (self-host) |
BILLING_ENABLED | Whether billing routes/UI render. | false (self-host) |
RETENTION_DAYS | Days to keep reports. 0 = keep forever. >0 enables periodic deletion. | 0 |
RETENTION_SWEEP_INTERVAL_MS | How often the retention sweep runs (ms, min 1000). Only matters when RETENTION_DAYS > 0. | 3600000 (1h) |
INGEST_RATE_LIMIT | Max ingestion requests per window, per source (API key). | 120 |
INGEST_RATE_WINDOW | Rate-limit window, in ms or an ms-style string (e.g. "1 minute"). | 60000 |
AUTH_RATE_LIMIT | Max attempts per window on sensitive auth endpoints (sign-in/up, password reset), per client IP. Raise it if many users share one NAT’d IP. | 20 |
AUTH_RATE_WINDOW | Auth rate-limit window, in ms or an ms-style string. | 60000 |
REDIS_URL | Optional. Backs multi-replica SSE fan-out and the auth/ingest rate-limit store. Omit for single-instance (both work in-memory per node). | (unset) |
GITHUB_CLIENT_ID | Optional GitHub OAuth client id. Both GitHub vars must be set together. | (unset) |
GITHUB_CLIENT_SECRET | Optional GitHub OAuth client secret. | (unset) |
EMAIL_PROVIDER | Set to smtp to deliver password-reset + invite emails. Unset = nothing sent; invite URLs returned in the API response. See Accounts & password reset. | (unset) |
EMAIL_FROM | From-address for sent email, e.g. "PulseDeck <no-reply@yourdomain.com>". | (unset) |
SMTP_HOST | SMTP server host. The trigger that activates delivery (with EMAIL_PROVIDER=smtp). | (unset) |
SMTP_PORT | SMTP port. 587 for STARTTLS; 465 with SMTP_SECURE=true. | 587 |
SMTP_USER / SMTP_PASS | Optional SMTP auth credentials. | (unset) |
SMTP_SECURE | true for implicit TLS (port 465); false/unset → STARTTLS. | false |
WEBHOOK_ALLOW_PRIVATE_IPS | Allow webhook URLs that resolve to loopback/private ranges (reach internal Slack/services). SSRF defense when off. | true (self-host) |
WEBHOOK_MAX_ATTEMPTS | Per-delivery retry ceiling before a webhook gives up. | 5 |
BOOTSTRAP_EMAIL | Optional. Seeds the first admin headlessly (skips the /setup wizard) — for IaC. | (unset) |
BOOTSTRAP_PASSWORD | Password for the bootstrapped admin. Idempotent (no-op if a user exists). | (unset) |
PORT | Port the API listens on. | 3001 |
WEB_PORT | Docker Compose host port for the web app. | 3000 |
API_PORT | Docker Compose host port for the API. | 3001 |
POSTGRES_PORT | Docker Compose host port for Postgres. | 5432 |