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.

This commit is contained in:
Jonny Nguyen 2026-06-26 12:32:42 -07:00
parent cc355b32d5
commit f98cc61cf7
5 changed files with 247 additions and 151 deletions

View file

@ -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
}

14
docker-compose.npm.yml Normal file
View file

@ -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

View file

@ -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

View file

@ -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 | ~$56/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=<openssl rand -hex 24>
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 NPMs 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).

View file

@ -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",