From cc355b32d5efa66ff1d83f40703ec63f8ae3a375 Mon Sep 17 00:00:00 2001 From: Jonny Nguyen Date: Fri, 26 Jun 2026 11:18:12 -0700 Subject: [PATCH] Update .env.example with detailed setup instructions, modify DATABASE_URL for Docker, and add new environment variables. Enhance package.json with Docker commands for easier deployment. Expand self-host hardening documentation to include Docker deployment on Ubuntu/VPS. --- .dockerignore | 15 +++ .env.example | 29 +++-- Dockerfile | 48 +++++++++ deploy/Caddyfile | 7 ++ docker-compose.yml | 58 ++++++++++ docs/deploy-ubuntu-docker.md | 202 +++++++++++++++++++++++++++++++++++ docs/self-host-hardening.md | 4 + package.json | 2 + scripts/docker-entrypoint.sh | 12 +++ 9 files changed, 369 insertions(+), 8 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 deploy/Caddyfile create mode 100644 docker-compose.yml create mode 100644 docs/deploy-ubuntu-docker.md create mode 100755 scripts/docker-entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..42b8814 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +node_modules +**/node_modules +**/dist +.env +.env.* +!.env.example +.git +.cursor +*.db +*.db-journal +apps/api/src/generated +coverage +docs +**/*.md +!README.md diff --git a/.env.example b/.env.example index 6c08ab2..98a8033 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,24 @@ -DATABASE_URL="file:./prisma/dev.db" -ENCRYPTION_KEY="<64-char-hex>" +# Copy to .env on your server: cp .env.example .env +# Generate secrets: +# openssl rand -hex 32 # ENCRYPTION_KEY (64 hex chars — NEVER change after first deploy) +# openssl rand -hex 24 # API_KEY -# Free AI via OpenRouter — sign up at https://openrouter.ai/keys -AI_API_KEY="sk-or-v1-..." +# Persistent SQLite (Docker Compose sets this automatically) +DATABASE_URL="file:/data/warmbox.db" + +# Required — encrypts mailbox passwords in the database +ENCRYPTION_KEY="" + +# Required when exposed to the internet — dashboard login uses this +API_KEY="" + +# Public URL users and OAuth providers see (no trailing slash) +PUBLIC_BASE_URL="https://warmbox.yourdomain.com" + +PORT=3000 +NODE_ENV=production + +# AI content generation (OpenRouter recommended) +AI_API_KEY="" AI_BASE_URL="https://openrouter.ai/api/v1" AI_MODEL="openrouter/free" - -API_KEY="" -PORT=3000 -NODE_ENV=development diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..663afb6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +# Warmbox — single-container production image (API + dashboard + cron jobs) +FROM node:22-bookworm-slim AS base +RUN apt-get update \ + && apt-get install -y --no-install-recommends openssl python3 make g++ \ + && rm -rf /var/lib/apt/lists/* + +FROM base AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +COPY apps/api/package.json apps/api/ +COPY apps/web/package.json apps/web/ +COPY packages/shared/package.json packages/shared/ +RUN npm ci + +FROM deps AS build +COPY . . +ENV DATABASE_URL=file:/tmp/build.db +RUN npx prisma generate +RUN npm run build + +FROM base AS runner +WORKDIR /app +ENV NODE_ENV=production + +RUN useradd --create-home --uid 10001 warmbox + +COPY --from=build /app/package.json /app/package-lock.json ./ +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/apps/api/dist ./apps/api/dist +COPY --from=build /app/apps/api/package.json ./apps/api/package.json +COPY --from=build /app/apps/web/dist ./apps/web/dist +COPY --from=build /app/packages/shared/dist ./packages/shared/dist +COPY --from=build /app/packages/shared/package.json ./packages/shared/package.json +COPY --from=build /app/prisma ./prisma +COPY --from=build /app/prisma.config.ts ./prisma.config.ts +COPY scripts/docker-entrypoint.sh /entrypoint.sh + +RUN chmod +x /entrypoint.sh \ + && mkdir -p /data \ + && chown -R warmbox:warmbox /app /data + +USER warmbox +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \ + CMD node -e "fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/deploy/Caddyfile b/deploy/Caddyfile new file mode 100644 index 0000000..a7a3730 --- /dev/null +++ b/deploy/Caddyfile @@ -0,0 +1,7 @@ +# Replace warmbox.yourdomain.com with your hostname. +# Caddy obtains Let's Encrypt certificates automatically. + +warmbox.yourdomain.com { + encode gzip + reverse_proxy warmbox:3000 +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..11c1326 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,58 @@ +services: + warmbox: + build: . + image: warmbox:local + restart: unless-stopped + env_file: .env + environment: + DATABASE_URL: file:/data/warmbox.db + 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" + 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 + + # Optional HTTPS reverse proxy — requires a public DNS name pointing at this host. + # Start with: docker compose --profile caddy up -d + 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 + + # 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} + +volumes: + warmbox-data: + caddy-data: + caddy-config: diff --git a/docs/deploy-ubuntu-docker.md b/docs/deploy-ubuntu-docker.md new file mode 100644 index 0000000..cf5b6a7 --- /dev/null +++ b/docs/deploy-ubuntu-docker.md @@ -0,0 +1,202 @@ +# 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 + +**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. + +--- + +## Quick comparison + +| 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 | + +--- + +## 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 + +```bash +cp .env.example .env +nano .env +``` + +Set these **before first start**: + +```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 +``` + +**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. + +--- + +## 3. Choose how the world reaches your server + +### Option A — Cloudflare Tunnel (recommended for home) + +No port forwarding. Works behind CGNAT. + +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) + +```bash +# In docker-compose.yml change ports to "3000:3000" +docker compose up -d --build +``` + +Use `http://SERVER_LAN_IP:3000` — not suitable for OAuth or remote access. + +--- + +## 4. First login + +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) + +--- + +## 5. Update OAuth redirect URIs + +Replace `localhost` with your public URL in: + +| 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. + +--- + +## 6. Migrate local data (optional) + +If you already configured mailboxes and campaigns locally: + +```bash +# On your Mac — stop local dev server first +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 +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 +``` + +Copy the **same** `ENCRYPTION_KEY` and `API_KEY` (or set a new API_KEY and use that to log in). + +--- + +## 7. Operations + +```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 +``` + +### 30-day warmup checklist + +- [ ] 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 + +--- + +## 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`. + +--- + +## Troubleshooting + +| 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 | + +See also [self-host-hardening.md](./self-host-hardening.md) for operator workflows. diff --git a/docs/self-host-hardening.md b/docs/self-host-hardening.md index 20c3319..5331ef3 100644 --- a/docs/self-host-hardening.md +++ b/docs/self-host-hardening.md @@ -21,6 +21,10 @@ npm run dev # API :3000, dashboard :5173 npm run build && npm run start ``` +### Run on Ubuntu / VPS (Docker) + +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). + ## Operator workflows (no curl required) 1. **Mailboxes** — add, edit credentials, test connection, DNS check diff --git a/package.json b/package.json index a4ad6e5..dc66378 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "dev": "concurrently -n api,web -c blue,green \"npm run dev -w @warmbox/api\" \"npm run dev -w @warmbox/web\"", "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:logs": "docker compose logs -f warmbox", "db:migrate": "prisma migrate dev", "db:generate": "prisma generate", "db:push": "prisma db push", diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh new file mode 100755 index 0000000..28fb5a2 --- /dev/null +++ b/scripts/docker-entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +mkdir -p /data + +cd /app + +echo "Running database migrations..." +npx prisma migrate deploy + +echo "Starting Warmbox API..." +exec node apps/api/dist/index.js