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.

This commit is contained in:
Jonny Nguyen 2026-06-26 11:18:12 -07:00
parent 807722dc3e
commit cc355b32d5
9 changed files with 369 additions and 8 deletions

15
.dockerignore Normal file
View file

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

View file

@ -1,11 +1,24 @@
DATABASE_URL="file:./prisma/dev.db" # Copy to .env on your server: cp .env.example .env
ENCRYPTION_KEY="<64-char-hex>" # 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 # Persistent SQLite (Docker Compose sets this automatically)
AI_API_KEY="sk-or-v1-..." 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_BASE_URL="https://openrouter.ai/api/v1"
AI_MODEL="openrouter/free" AI_MODEL="openrouter/free"
API_KEY=""
PORT=3000
NODE_ENV=development

48
Dockerfile Normal file
View file

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

7
deploy/Caddyfile Normal file
View file

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

58
docker-compose.yml Normal file
View file

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

View file

@ -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 | ~$56/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.

View file

@ -21,6 +21,10 @@ npm run dev # API :3000, dashboard :5173
npm run build && npm run start 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) ## Operator workflows (no curl required)
1. **Mailboxes** — add, edit credentials, test connection, DNS check 1. **Mailboxes** — add, edit credentials, test connection, DNS check

View file

@ -11,6 +11,8 @@
"dev": "concurrently -n api,web -c blue,green \"npm run dev -w @warmbox/api\" \"npm run dev -w @warmbox/web\"", "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", "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", "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:migrate": "prisma migrate dev",
"db:generate": "prisma generate", "db:generate": "prisma generate",
"db:push": "prisma db push", "db:push": "prisma db push",

12
scripts/docker-entrypoint.sh Executable file
View file

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