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:
parent
807722dc3e
commit
cc355b32d5
9 changed files with 369 additions and 8 deletions
15
.dockerignore
Normal file
15
.dockerignore
Normal 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
|
||||||
29
.env.example
29
.env.example
|
|
@ -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
48
Dockerfile
Normal 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
7
deploy/Caddyfile
Normal 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
58
docker-compose.yml
Normal 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:
|
||||||
202
docs/deploy-ubuntu-docker.md
Normal file
202
docs/deploy-ubuntu-docker.md
Normal 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 | ~$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.
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
12
scripts/docker-entrypoint.sh
Executable 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
|
||||||
Loading…
Reference in a new issue