# Deploy Warmbox on Ubuntu (Docker) Warmbox is a **long-running worker** (cron dispatcher, rescuer, SMTP/IMAP). It needs: - A persistent database (SQLite volume) - Outbound SMTP/IMAP from the server - A public HTTPS URL for the dashboard and OAuth callbacks **Not suitable:** Netlify, GitHub Pages, Vercel — no background jobs, no SQLite, no mail sockets. --- ## Your setup (`warmbox.jonnyn.com`) You already have: - DNS **A** record → home public IP - **Nginx Proxy Manager** (NPM) on Docker network `npm_network` **Recommended for you: Option B — Nginx Proxy Manager** (simplest; reuses what you have). Use **Option A — Cloudflare Tunnel** only if you want HTTPS without opening ports 80/443, or if your ISP blocks inbound traffic. Do **not** run Caddy (Option C) alongside NPM — both want ports 80 and 443. --- ## 1. Prepare the server ```bash cd ~/warmbox # or wherever you cloned the repo cp .env.example .env nano .env ``` Required in `.env`: ```bash ENCRYPTION_KEY=<64 hex chars — openssl rand -hex 32> API_KEY= PUBLIC_BASE_URL=https://warmbox.jonnyn.com AI_API_KEY=sk-or-v1-... ``` Keep `ENCRYPTION_KEY` forever. If migrating from your Mac, copy the same key and `prisma/dev.db`. --- ## 2. Fix: “port 3000 already allocated” Warmbox listens on **port 3000 inside its container**. That does **not** conflict with `homepage` using host port `3000` — unless you publish Warmbox to the host with a `ports:` mapping. **NPM and Cloudflare Tunnel only need Docker network access** (`warmbox:3000`). No host port is required. ### Verify your compose file on the server ```bash cd /srv/docker-configs/warmbox grep -n ports docker-compose.yml docker-compose.npm.yml 2>/dev/null docker compose -f docker-compose.yml -f docker-compose.npm.yml config | grep -A2 published ``` - **Correct:** no `published` / `ports` for the `warmbox` service (empty grep for warmbox ports). - **Wrong:** lines like `"3000:3000"` or `"127.0.0.1:3000:3000"` under `warmbox` — **delete the entire `ports:` block** for that service. Pull the latest repo (or remove `ports:` manually), then: ```bash docker compose -f docker-compose.yml -f docker-compose.npm.yml down docker compose -f docker-compose.yml -f docker-compose.npm.yml up -d --build ``` ### Optional: direct LAN access on a different host port Only if you want to hit Warmbox without NPM (debugging). This uses host **3001**, not 3000: ```yaml ports: - "127.0.0.1:3001:3000" ``` NPM still uses forward port **3000** (container port), not 3001. ### Find what uses host port 3000 ```bash docker ps --format '{{.Names}} {{.Ports}}' | grep '0.0.0.0:3000' ``` In your case `homepage` owns host 3000 — leave it alone; Warmbox does not need that port. --- ## Option B — Nginx Proxy Manager (recommended for your home server) Traffic path: ```text Internet → warmbox.jonnyn.com (DNS A → home IP) → NPM :443 → warmbox:3000 (Docker) ``` ### Step 1 — Start Warmbox on `npm_network` ```bash docker compose -f docker-compose.yml -f docker-compose.npm.yml up -d --build ``` Verify: ```bash docker compose -f docker-compose.yml -f docker-compose.npm.yml ps docker compose -f docker-compose.yml -f docker-compose.npm.yml logs -f warmbox curl -s http://warmbox:3000/health # run from any container on npm_network, or skip ``` ### Step 2 — NPM proxy host In Nginx Proxy Manager UI: | Field | Value | |--------|--------| | Domain names | `warmbox.jonnyn.com` | | Scheme | `http` | | Forward hostname / IP | `warmbox` | | Forward port | `3000` (container port — not your host port 3000 used by homepage) | | Cache assets | off | | Block common exploits | on | | Websockets support | on | **SSL** tab: - Request a new SSL Certificate (Let's Encrypt) - Force SSL + HTTP/2 If you use **Cloudflare orange cloud** (proxied) on the A record, set Cloudflare SSL mode to **Full** or **Full (strict)** so NPM’s certificate works. ### Step 3 — Login 1. Open `https://warmbox.jonnyn.com` 2. API key = your `API_KEY` from `.env` 3. Leave **Base URL** blank ### Step 4 — OAuth redirect URIs | Integration | Redirect URI | |-------------|----------------| | Google Postmaster | `https://warmbox.jonnyn.com/api/integrations/google/callback` | | Microsoft Outlook | `https://warmbox.jonnyn.com/api/integrations/microsoft/callback` | --- ## Option A — Cloudflare Tunnel Use this if you prefer **no inbound ports** on your router (tunnel only). Traffic path: ```text Browser → Cloudflare edge → cloudflared (Docker) → warmbox:3000 ``` ### Important DNS note For tunnel-only access, let the **tunnel** own the hostname. In Cloudflare Zero Trust → your tunnel → **Public Hostname**: | Field | Value | |--------|--------| | Subdomain | `warmbox` | | Domain | `jonnyn.com` | | Type | HTTP | | URL | `warmbox:3000` | Cloudflare usually creates a CNAME for you. You can **remove the A record** for `warmbox` if everything goes through the tunnel (avoids split routing). ### Step 1 — `.env` ```bash CLOUDFLARE_TUNNEL_TOKEN=eyJh... # from Zero Trust → Tunnels → your tunnel → Docker install PUBLIC_BASE_URL=https://warmbox.jonnyn.com ``` ### Step 2 — Start (no NPM override file) ```bash docker compose --profile cloudflare up -d --build ``` `cloudflared` and `warmbox` share the `warmbox_internal` network — hostname `warmbox` resolves inside that network. ### Step 3 — Verify ```bash docker compose --profile cloudflare ps docker compose --profile cloudflare logs -f cloudflared docker compose --profile cloudflare logs -f warmbox curl -s https://warmbox.jonnyn.com/health ``` ### Tunnel + NPM together? Pick **one** public entry for `warmbox.jonnyn.com`: - **NPM only:** A record → home IP, no tunnel hostname for the same subdomain. - **Tunnel only:** tunnel hostname, no A record to home IP for that subdomain. Running both for the same hostname causes confusing SSL or routing issues. --- ## Option C — Caddy (standalone, no NPM) Only if NPM is **not** using 80/443 on this host. 1. Edit `deploy/Caddyfile` — set `warmbox.jonnyn.com` 2. DNS A record → home public IP; forward router 80/443 to the server 3. Start: ```bash docker compose --profile caddy up -d --build ``` **Skip this** if Nginx Proxy Manager is already on the server. --- ## Migrate local database (optional) ```bash # Mac scp prisma/dev.db user@home-server:/tmp/warmbox.db # Server — stack stopped docker compose -f docker-compose.yml -f docker-compose.npm.yml down docker run --rm -v warmbox_warmbox-data:/data -v /tmp:/backup alpine \ cp /backup/warmbox.db /data/warmbox.db docker compose -f docker-compose.yml -f docker-compose.npm.yml up -d ``` Use the **same** `ENCRYPTION_KEY` as on your Mac. --- ## Day-to-day commands **NPM deployment:** ```bash docker compose -f docker-compose.yml -f docker-compose.npm.yml logs -f warmbox docker compose -f docker-compose.yml -f docker-compose.npm.yml up -d --build ``` **Cloudflare deployment:** ```bash docker compose --profile cloudflare logs -f warmbox docker compose --profile cloudflare up -d --build ``` **Backup:** ```bash docker compose exec warmbox sh -c 'cp /data/warmbox.db /data/warmbox-$(date +%F).db' ``` --- ## Troubleshooting | Symptom | Fix | |---------|-----| | `port 3000 already allocated` | Use updated compose (no host ports); `docker compose down`; find conflict with `docker ps \| grep 3000` | | NPM 502 Bad Gateway | Warmbox not on `npm_network`; wrong forward host (`warmbox` not `localhost`); check `docker compose logs warmbox` | | Tunnel 502 / error | Public hostname URL must be `http://warmbox:3000`; both containers on `warmbox_internal`; check `logs cloudflared` | | Login fails | `API_KEY` in `.env` must match login; restart after `.env` change | | OAuth redirect error | `PUBLIC_BASE_URL=https://warmbox.jonnyn.com` exactly; update Google/Microsoft redirect URIs | | SSL errors with Cloudflare proxy | Cloudflare SSL mode **Full** or **Full (strict)** when NPM terminates TLS | See also [self-host-hardening.md](./self-host-hardening.md).