From f98cc61cf7204996f63a8769768780c229002041 Mon Sep 17 00:00:00 2001 From: Jonny Nguyen Date: Fri, 26 Jun 2026 12:32:42 -0700 Subject: [PATCH] Add Docker Compose configuration for Warmbox with Nginx Proxy Manager integration. Introduce new npm-specific compose file and update existing services to utilize Docker networks without host port exposure. Enhance documentation for deployment instructions and clarify setup for Cloudflare Tunnel and Nginx Proxy Manager. --- deploy/Caddyfile | 6 +- docker-compose.npm.yml | 14 ++ docker-compose.yml | 46 ++--- docs/deploy-ubuntu-docker.md | 330 +++++++++++++++++++++-------------- package.json | 2 + 5 files changed, 247 insertions(+), 151 deletions(-) create mode 100644 docker-compose.npm.yml diff --git a/deploy/Caddyfile b/deploy/Caddyfile index a7a3730..2f2fc21 100644 --- a/deploy/Caddyfile +++ b/deploy/Caddyfile @@ -1,7 +1,7 @@ -# Replace warmbox.yourdomain.com with your hostname. -# Caddy obtains Let's Encrypt certificates automatically. +# 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.yourdomain.com { +warmbox.jonnyn.com { encode gzip reverse_proxy warmbox:3000 } diff --git a/docker-compose.npm.yml b/docker-compose.npm.yml new file mode 100644 index 0000000..0d99994 --- /dev/null +++ b/docker-compose.npm.yml @@ -0,0 +1,14 @@ +# 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 11c1326..616a7c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,19 +9,32 @@ services: NODE_ENV: production volumes: - warmbox-data:/data - ports: - # Bind to localhost only when using the Caddy profile (recommended). - # For LAN testing without Caddy, change to "3000:3000". - - "127.0.0.1:3000:3000" + # 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"] 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 - # Optional HTTPS reverse proxy — requires a public DNS name pointing at this host. - # Start with: docker compose --profile caddy up -d + # 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 @@ -36,23 +49,14 @@ services: - ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro - caddy-data:/data - caddy-config:/config - - # Optional Cloudflare Tunnel — HTTPS without opening router ports (great for home servers). - # 1. Create a tunnel in Cloudflare Zero Trust → copy token - # 2. Set CLOUDFLARE_TUNNEL_TOKEN in .env - # 3. docker compose --profile cloudflare up -d - 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 volumes: warmbox-data: caddy-data: caddy-config: + +networks: + warmbox_internal: + name: warmbox_internal diff --git a/docs/deploy-ubuntu-docker.md b/docs/deploy-ubuntu-docker.md index cf5b6a7..d1547dd 100644 --- a/docs/deploy-ubuntu-docker.md +++ b/docs/deploy-ubuntu-docker.md @@ -6,186 +6,261 @@ Warmbox is a **long-running worker** (cron dispatcher, rescuer, SMTP/IMAP). It n - Outbound SMTP/IMAP from the server - A public HTTPS URL for the dashboard and OAuth callbacks -**Recommended for your case:** Ubuntu home server + Docker + **Cloudflare Tunnel** (free HTTPS, no router port forwarding). - -**Not suitable:** Netlify, GitHub Pages, Vercel static — no background jobs, no SQLite, no mail sockets. +**Not suitable:** Netlify, GitHub Pages, Vercel — no background jobs, no SQLite, no mail sockets. --- -## Quick comparison +## Your setup (`warmbox.jonnyn.com`) -| Option | Best for | Pros | Cons | -|--------|----------|------|------| -| **Ubuntu + Docker + Cloudflare Tunnel** | Home server (your preference) | Free, persistent, full control | You maintain the box | -| Ubuntu + Docker + Caddy | Home server with public IP + domain | Simple TLS | Open ports 80/443 on router | -| Hetzner / DigitalOcean VPS + same Docker | No home server | Always on, static IP | ~$5–6/mo | -| Railway / Fly.io | Managed container | Easy deploy | Cost, less control for 30-day test | +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 Ubuntu server - -```bash -# SSH into your home server -sudo apt update && sudo apt upgrade -y -sudo apt install -y git docker.io docker-compose-v2 -sudo usermod -aG docker $USER -# log out and back in so docker group applies -``` - -Clone the repo: - -```bash -git clone https://github.com/YOUR_ORG/warmbox.git -cd warmbox -``` - ---- - -## 2. Configure environment +## 1. Prepare the server ```bash +cd ~/warmbox # or wherever you cloned the repo cp .env.example .env nano .env ``` -Set these **before first start**: +Required in `.env`: ```bash -# Generate once — keep forever (changing it breaks encrypted mailbox passwords) -openssl rand -hex 32 # → ENCRYPTION_KEY - -# Dashboard login password -openssl rand -hex 24 # → API_KEY - -PUBLIC_BASE_URL=https://warmbox.yourdomain.com # your real public URL -AI_API_KEY=sk-or-v1-... # OpenRouter or compatible +ENCRYPTION_KEY=<64 hex chars — openssl rand -hex 32> +API_KEY= +PUBLIC_BASE_URL=https://warmbox.jonnyn.com +AI_API_KEY=sk-or-v1-... ``` -**Migrating from local:** copy your existing `prisma/dev.db` to the server **and** use the **same** `ENCRYPTION_KEY` from your local `.env`. Otherwise mailbox credentials cannot be decrypted. +Keep `ENCRYPTION_KEY` forever. If migrating from your Mac, copy the same key and `prisma/dev.db`. --- -## 3. Choose how the world reaches your server +## 2. Fix: “port 3000 already allocated” -### Option A — Cloudflare Tunnel (recommended for home) +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. -No port forwarding. Works behind CGNAT. +**NPM and Cloudflare Tunnel only need Docker network access** (`warmbox:3000`). No host port is required. -1. [Cloudflare Zero Trust](https://one.dash.cloudflare.com/) → **Networks** → **Tunnels** → Create tunnel -2. Name it `warmbox`, install connector via **Docker** — copy the token -3. In the tunnel **Public Hostname** tab, route `warmbox.yourdomain.com` → `http://warmbox:3000` (Docker network) - If cloudflared runs in the same compose file, use hostname `warmbox` and port `3000`. -4. Add to `.env`: - - ```bash - CLOUDFLARE_TUNNEL_TOKEN=eyJh... - PUBLIC_BASE_URL=https://warmbox.yourdomain.com - ``` - -5. Start: - - ```bash - docker compose --profile cloudflare up -d --build - ``` - -### Option B — Caddy on a public IP - -1. Point DNS `warmbox.yourdomain.com` → your home public IP -2. Forward router ports **80** and **443** to the Ubuntu server -3. Edit `deploy/Caddyfile` with your domain -4. Start: - - ```bash - docker compose --profile caddy up -d --build - ``` - -### Option C — LAN / direct port (testing only) +### Verify your compose file on the server ```bash -# In docker-compose.yml change ports to "3000:3000" -docker compose up -d --build +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 ``` -Use `http://SERVER_LAN_IP:3000` — not suitable for OAuth or remote access. +- **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. --- -## 4. First login +## Option B — Nginx Proxy Manager (recommended for your home server) -1. Open `https://warmbox.yourdomain.com` -2. Login with your `API_KEY` -3. Leave **Base URL** blank (dashboard and API are same origin in production) +Traffic path: ---- +```text +Internet → warmbox.jonnyn.com (DNS A → home IP) → NPM :443 → warmbox:3000 (Docker) +``` -## 5. Update OAuth redirect URIs +### Step 1 — Start Warmbox on `npm_network` -Replace `localhost` with your public URL in: +```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.yourdomain.com/api/integrations/google/callback` | -| Microsoft Outlook | `https://warmbox.yourdomain.com/api/integrations/microsoft/callback` | - -Re-connect integrations in **Settings** after deploy. +| Google Postmaster | `https://warmbox.jonnyn.com/api/integrations/google/callback` | +| Microsoft Outlook | `https://warmbox.jonnyn.com/api/integrations/microsoft/callback` | --- -## 6. Migrate local data (optional) +## Option A — Cloudflare Tunnel -If you already configured mailboxes and campaigns locally: +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 -# On your Mac — stop local dev server first +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 -# On Ubuntu — before first start OR with stack stopped: -docker compose down -docker volume create warmbox_warmbox-data 2>/dev/null || true +# 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 --profile cloudflare up -d +docker compose -f docker-compose.yml -f docker-compose.npm.yml up -d ``` -Copy the **same** `ENCRYPTION_KEY` and `API_KEY` (or set a new API_KEY and use that to log in). +Use the **same** `ENCRYPTION_KEY` as on your Mac. --- -## 7. Operations +## Day-to-day commands + +**NPM deployment:** ```bash -# Logs -docker compose logs -f warmbox - -# Restart after code pull -git pull -docker compose --profile cloudflare up -d --build - -# Backup database (run daily via cron) -docker compose exec warmbox sh -c 'cp /data/warmbox.db /data/warmbox-$(date +%F).db' - -# Health check -curl -s https://warmbox.yourdomain.com/health +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 ``` -### 30-day warmup checklist +**Cloudflare deployment:** -- [ ] Container `restart: unless-stopped` (default in compose) -- [ ] `warmbox-data` volume backed up -- [ ] `ENCRYPTION_KEY` saved in your password manager -- [ ] Campaign started and status **active** -- [ ] Settings → Jobs shows dispatcher/rescuer running (or check logs every few hours day 1) -- [ ] OAuth redirect URIs updated if using Postmaster / Microsoft +```bash +docker compose --profile cloudflare logs -f warmbox +docker compose --profile cloudflare up -d --build +``` ---- +**Backup:** -## 8. Security notes (single-tenant MVP) - -- **Always set `API_KEY`** when exposed to the internet. -- Do not commit `.env` to git. -- Restrict dashboard access further with Cloudflare Access (optional) or VPN if desired. -- Mailbox app passwords live encrypted in SQLite — protect the volume and `ENCRYPTION_KEY`. +```bash +docker compose exec warmbox sh -c 'cp /data/warmbox.db /data/warmbox-$(date +%F).db' +``` --- @@ -193,10 +268,11 @@ curl -s https://warmbox.yourdomain.com/health | Symptom | Fix | |---------|-----| -| Login fails | Check `API_KEY` in `.env` matches what you enter; restart container after `.env` change | -| OAuth redirect error | `PUBLIC_BASE_URL` must match browser URL; update provider redirect URI | -| Jobs not sending | `docker compose logs warmbox` — verify cron lines; confirm SMTP/IMAP from server IP isn't blocked | -| `ENCRYPTION_KEY` error on decrypt | Wrong key vs database — restore original key or re-add mailboxes | -| Container unhealthy | `docker compose logs warmbox` — migration or port conflict | +| `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) for operator workflows. +See also [self-host-hardening.md](./self-host-hardening.md). diff --git a/package.json b/package.json index dc66378..8d19e47 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "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",