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:
parent
cc355b32d5
commit
f98cc61cf7
5 changed files with 247 additions and 151 deletions
|
|
@ -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
14
docker-compose.npm.yml
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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=<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 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).
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue