From ac09760771890a8df65b413c39bcebbb26bd8346 Mon Sep 17 00:00:00 2001 From: Jonny Nguyen Date: Fri, 26 Jun 2026 13:00:32 -0700 Subject: [PATCH] Update .env.example with new PUBLIC_BASE_URL, remove unused docker-compose.npm.yml and Caddyfile, and adjust docker-compose.yml to simplify network configuration. Revise deployment documentation to reflect changes in setup and clarify Nginx Proxy Manager integration. --- .env.example | 2 +- deploy/Caddyfile | 7 - docker-compose.npm.yml | 14 -- docker-compose.yml | 45 +---- docs/deploy-ubuntu-docker.md | 337 +++++++++++++++-------------------- docs/self-host-hardening.md | 4 +- package.json | 2 - 7 files changed, 149 insertions(+), 262 deletions(-) delete mode 100644 deploy/Caddyfile delete mode 100644 docker-compose.npm.yml diff --git a/.env.example b/.env.example index 98a8033..26146f6 100644 --- a/.env.example +++ b/.env.example @@ -13,7 +13,7 @@ ENCRYPTION_KEY="" API_KEY="" # Public URL users and OAuth providers see (no trailing slash) -PUBLIC_BASE_URL="https://warmbox.yourdomain.com" +PUBLIC_BASE_URL="https://warmbox.jonnyn.com" PORT=3000 NODE_ENV=production diff --git a/deploy/Caddyfile b/deploy/Caddyfile deleted file mode 100644 index 2f2fc21..0000000 --- a/deploy/Caddyfile +++ /dev/null @@ -1,7 +0,0 @@ -# Replace warmbox.yourdomain.com with your hostname (e.g. warmbox.jonnyn.com). -# Only use this profile if Nginx Proxy Manager is NOT already bound to 80/443. - -warmbox.jonnyn.com { - encode gzip - reverse_proxy warmbox:3000 -} diff --git a/docker-compose.npm.yml b/docker-compose.npm.yml deleted file mode 100644 index 0d99994..0000000 --- a/docker-compose.npm.yml +++ /dev/null @@ -1,14 +0,0 @@ -# Use with: docker compose -f docker-compose.yml -f docker-compose.npm.yml up -d --build -# -# Attaches Warmbox to your existing Nginx Proxy Manager network so NPM can proxy -# warmbox.jonnyn.com → http://warmbox:3000 - -services: - warmbox: - networks: - - warmbox_internal - - npm_network - -networks: - npm_network: - external: true diff --git a/docker-compose.yml b/docker-compose.yml index 616a7c2..d4ce78d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,7 @@ services: warmbox: build: . image: warmbox:local + container_name: warmbox restart: unless-stopped env_file: .env environment: @@ -9,54 +10,18 @@ services: NODE_ENV: production volumes: - warmbox-data:/data - # No host ports — reach Warmbox via Docker network only (NPM or Cloudflare Tunnel). - # For LAN debugging only, temporarily add: ports: ["127.0.0.1:3001:3000"] + # No host ports — NPM reaches this container on npm_network at warmbox:3000 healthcheck: test: ['CMD', 'node', '-e', "fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 30s timeout: 5s retries: 3 start_period: 40s - networks: - - warmbox_internal - - # Cloudflare Tunnel connector — shares warmbox_internal with the app. - cloudflared: - profiles: [cloudflare] - image: cloudflare/cloudflared:latest - restart: unless-stopped - depends_on: - warmbox: - condition: service_healthy - command: tunnel run - environment: - TUNNEL_TOKEN: ${CLOUDFLARE_TUNNEL_TOKEN} - networks: - - warmbox_internal - - # Standalone Caddy — only if you do NOT already run Nginx Proxy Manager on 80/443. - caddy: - profiles: [caddy] - image: caddy:2-alpine - restart: unless-stopped - depends_on: - warmbox: - condition: service_healthy - ports: - - '80:80' - - '443:443' - volumes: - - ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro - - caddy-data:/data - - caddy-config:/config - networks: - - warmbox_internal volumes: warmbox-data: - caddy-data: - caddy-config: networks: - warmbox_internal: - name: warmbox_internal + default: + external: true + name: npm_network diff --git a/docs/deploy-ubuntu-docker.md b/docs/deploy-ubuntu-docker.md index d1547dd..3e6ceec 100644 --- a/docs/deploy-ubuntu-docker.md +++ b/docs/deploy-ubuntu-docker.md @@ -1,146 +1,134 @@ -# Deploy Warmbox on Ubuntu (Docker) +# Deploy Warmbox with Nginx Proxy Manager (Docker) -Warmbox is a **long-running worker** (cron dispatcher, rescuer, SMTP/IMAP). It needs: +Warmbox runs as one container on your existing **`npm_network`**. Nginx Proxy Manager (NPM) terminates HTTPS and forwards to `http://warmbox:3000`. -- A persistent database (SQLite volume) -- Outbound SMTP/IMAP from the server -- A public HTTPS URL for the dashboard and OAuth callbacks +```text +Internet → warmbox.jonnyn.com (DNS) → NPM :443 → warmbox:3000 (Docker network) +``` -**Not suitable:** Netlify, GitHub Pages, Vercel — no background jobs, no SQLite, no mail sockets. +No host port is published. Warmbox can run alongside `homepage` on host port 3000 — they do not conflict. --- -## Your setup (`warmbox.jonnyn.com`) +## Prerequisites -You already have: +- Ubuntu server with Docker + Compose v2 +- [Nginx Proxy Manager](https://nginxproxymanager.com/) already running on Docker network **`npm_network`** +- DNS **A** record: `warmbox.jonnyn.com` → your home public IP +- Router forwards **80** and **443** to NPM (if not already) -- 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 +Confirm NPM’s network name: ```bash -cd ~/warmbox # or wherever you cloned the repo +docker network ls | grep npm +docker inspect --format '{{json .NetworkSettings.Networks}}' | jq +``` + +If your network is not `npm_network`, edit `networks.default.name` in `docker-compose.yml`. + +--- + +## 1. Clone and configure + +```bash +cd /srv/docker-configs/warmbox # or your path +git pull # get latest docker-compose.yml + cp .env.example .env nano .env ``` -Required in `.env`: +Required variables: ```bash -ENCRYPTION_KEY=<64 hex chars — openssl rand -hex 32> -API_KEY= +ENCRYPTION_KEY= # never change after first deploy +API_KEY= # dashboard login 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`. +**Migrating from your Mac:** use the **same** `ENCRYPTION_KEY` and copy `prisma/dev.db` (see §6). --- -## 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 +## 2. Start Warmbox ```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. +# Stop any old stack (including previous compose overrides) +docker compose down -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 +# Build and start — joins npm_network automatically +docker compose 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 +docker compose ps +docker compose logs -f warmbox ``` -### Step 2 — NPM proxy host +Warmbox should be **healthy**. Check it is on `npm_network`: -In Nginx Proxy Manager UI: +```bash +docker inspect warmbox --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' +# should include: npm_network +``` -| Field | Value | -|--------|--------| +Test from another container on the same network (e.g. NPM): + +```bash +docker exec -it wget -qO- http://warmbox:3000/health +# expect: {"status":"ok"} +``` + +--- + +## 3. Nginx Proxy Manager — proxy host + +Open NPM UI (usually `http://:81`). + +**Hosts → Proxy Hosts → Add Proxy Host** + +| Tab / 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) | +| Forward port | `3000` | | Cache assets | off | | Block common exploits | on | | Websockets support | on | **SSL** tab: -- Request a new SSL Certificate (Let's Encrypt) -- Force SSL + HTTP/2 +- SSL Certificate: Request a new Let's Encrypt certificate +- Force SSL: on +- HTTP/2: on -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. +If Cloudflare proxies your A record (orange cloud), set Cloudflare SSL mode to **Full** or **Full (strict)**. -### Step 3 — Login +Save, then test: + +```bash +curl -s https://warmbox.jonnyn.com/health +``` + +--- + +## 4. Dashboard login 1. Open `https://warmbox.jonnyn.com` -2. API key = your `API_KEY` from `.env` -3. Leave **Base URL** blank +2. Enter `API_KEY` from `.env` +3. Leave **Base URL** empty (same origin in production) -### Step 4 — OAuth redirect URIs +--- + +## 5. OAuth redirect URIs + +Update in Google Cloud / Azure if you use these integrations: | Integration | Redirect URI | |-------------|----------------| @@ -149,116 +137,35 @@ If you use **Cloudflare orange cloud** (proxied) on the A record, set Cloudflare --- -## 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` +## 6. Migrate local database (optional) ```bash -CLOUDFLARE_TUNNEL_TOKEN=eyJh... # from Zero Trust → Tunnels → your tunnel → Docker install -PUBLIC_BASE_URL=https://warmbox.jonnyn.com -``` +# On your Mac +scp prisma/dev.db jonnyn@burke:/tmp/warmbox.db -### 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 +# On server +cd /srv/docker-configs/warmbox +docker compose 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 +docker compose up -d ``` -Use the **same** `ENCRYPTION_KEY` as on your Mac. +Use the same `ENCRYPTION_KEY` as on your Mac. --- -## Day-to-day commands - -**NPM deployment:** +## 7. Day-to-day commands ```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 -``` +cd /srv/docker-configs/warmbox -**Cloudflare deployment:** +docker compose logs -f warmbox # live logs +docker compose up -d --build # rebuild after git pull +docker compose down # stop +docker compose restart warmbox # restart app only -```bash -docker compose --profile cloudflare logs -f warmbox -docker compose --profile cloudflare up -d --build -``` - -**Backup:** - -```bash +# Backup SQLite docker compose exec warmbox sh -c 'cp /data/warmbox.db /data/warmbox-$(date +%F).db' ``` @@ -266,13 +173,51 @@ docker compose exec warmbox sh -c 'cp /data/warmbox.db /data/warmbox-$(date +%F) ## 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 | +### `port 3000 already allocated` + +Your `docker-compose.yml` must **not** have a `ports:` section under `warmbox`: + +```bash +grep ports docker-compose.yml +# should return nothing (or only comments) +``` + +Warmbox uses port 3000 **inside** the container only. `homepage` on host `0.0.0.0:3000` is unrelated. + +### NPM 502 Bad Gateway + +1. Warmbox running and healthy: `docker compose ps` +2. On `npm_network`: `docker inspect warmbox --format '{{json .NetworkSettings.Networks}}'` +3. NPM forward host is `warmbox` (not `localhost`, not `127.0.0.1`) +4. NPM forward port is `3000` +5. Logs: `docker compose logs warmbox` + +### `network npm_network not found` + +Create it or fix the name to match NPM: + +```bash +docker network ls +# edit docker-compose.yml → networks.default.name +``` + +### Login fails + +- `API_KEY` in `.env` must match what you type +- Restart after `.env` change: `docker compose up -d --force-recreate` + +### Container unhealthy + +```bash +docker compose logs warmbox +``` + +Common causes: missing `ENCRYPTION_KEY`, bad `DATABASE_URL`, migration error. + +--- + +## Why only `npm_network`? + +NPM and Warmbox must share a Docker network so NPM can resolve the hostname `warmbox`. A separate `warmbox_internal` network is unnecessary for this setup — one external network is enough. See also [self-host-hardening.md](./self-host-hardening.md). diff --git a/docs/self-host-hardening.md b/docs/self-host-hardening.md index 5331ef3..0bbd31a 100644 --- a/docs/self-host-hardening.md +++ b/docs/self-host-hardening.md @@ -21,9 +21,9 @@ npm run dev # API :3000, dashboard :5173 npm run build && npm run start ``` -### Run on Ubuntu / VPS (Docker) +### Run on Ubuntu / VPS (Docker + Nginx Proxy Manager) -For a **30-day live test** without keeping your laptop on, see **[deploy-ubuntu-docker.md](./deploy-ubuntu-docker.md)** (Docker Compose + Cloudflare Tunnel or Caddy). +For a **30-day live test** without keeping your laptop on, see **[deploy-ubuntu-docker.md](./deploy-ubuntu-docker.md)**. ## Operator workflows (no curl required) diff --git a/package.json b/package.json index 8d19e47..dc66378 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,6 @@ "build": "npm run build -w @warmbox/shared && npm run build -w @warmbox/api && npm run build -w @warmbox/web", "start": "npm run start -w @warmbox/api", "docker:up": "docker compose up -d --build", - "docker:up:npm": "docker compose -f docker-compose.yml -f docker-compose.npm.yml up -d --build", - "docker:up:cloudflare": "docker compose --profile cloudflare up -d --build", "docker:logs": "docker compose logs -f warmbox", "db:migrate": "prisma migrate dev", "db:generate": "prisma generate",