# Deployment guide Agmail runs as one Docker Compose stack on a **mail VPS** (port 25 open): Caddy (HTTPS), frontend, backend, PostgreSQL, MongoDB, and Postfix (inbound + outbound). The **main website** at `gentic.email` can stay on another host (e.g. GCP). Only the email stack lives on the mail VPS: | Hostname | Points to | Purpose | |----------|-----------|---------| | `gentic.email` | GCP / main site IP | Marketing / main app (unchanged) | | `email.gentic.email` | **Mail VPS public IP** | Agmail web UI (Caddy → frontend) | | `mail.gentic.email` | **Mail VPS public IP** | Postfix :25 (inbound + outbound) | | `@gentic.email` MX | `mail.gentic.email` | Delivers mail to the VPS | User addresses remain **`user@gentic.email`**. ## Architecture ```text Browser ──HTTPS──► email.gentic.email (Caddy :443) └──► frontend ──► backend ──SMTP──► smtp:25 (Postfix outbound) Internet ──SMTP:25──► mail.gentic.email (Postfix) ├── inbound → backend:2525 → GCS + MongoDB └── outbound ← backend (Docker mynetworks) ``` ## Quick start ```bash cp .env.example .env # Edit .env — see Environment section below docker compose up -d --build ``` Open **https://email.gentic.email** after DNS and TLS are ready. | Service | Host port | Purpose | |----------|-----------|---------| | caddy | 80, 443 | Public HTTPS entrypoint | | frontend | 127.0.0.1:8080 | Local debug only | | backend | 127.0.0.1:8000 | API / health (local) | | smtp | 25 | Inbound + outbound Postfix | ```bash curl -fsS http://127.0.0.1:8000/health docker compose ps docker compose logs -f caddy ``` ## Environment (`.env` at repo root) ### Required ```env JWT_SECRET= POSTGRES_PASSWORD="" MONGO_ROOT_PASSWORD="" GCS_RAW_MIME_BUCKET= GCS_PROJECT= GCP_CREDENTIALS_FILE=./midyear-karma-470610-i1-da5334d20acc.json CADDY_ACME_EMAIL=admin@gentic.email CORS_ORIGINS=["https://email.gentic.email"] ``` Quote values that contain `#`. `GCP_CREDENTIALS_FILE` is **relative to `deploy/app/`**, not the repo root. Outbound uses the Docker Postfix service by default (`SMTP_HOST=smtp`, `SMTP_PORT=25`, `SMTP_USE_TLS=false`). No Amazon SES credentials are required. ### Split-domain settings (mail VPS) ```env # User mailboxes are still @gentic.email PLATFORM_DOMAIN=gentic.email # Web UI on this host (Caddy terminates TLS here) APP_PUBLIC_HOST=email.gentic.email CADDY_ACME_EMAIL=admin@gentic.email # Postfix hostname (inbound + outbound) MAIL_HOSTNAME=mail.gentic.email MAIL_DOMAIN=gentic.email INBOUND_DOMAINS=gentic.email SMTP_PUBLIC_HOST=mail.gentic.email SMTP_HOST=smtp SMTP_PORT=25 SMTP_USE_TLS=false # Same-host Docker defaults (usually leave as-is) BACKEND_INGEST_HOST=backend BACKEND_INGEST_PORT=2525 INBOUND_ALLOWED_CIDRS=["172.16.0.0/12","127.0.0.0/8"] INBOUND_SMTP_BIND_IP=127.0.0.1 ``` ## Namecheap DNS changes Update DNS as follows (`` = public IP of the mail server). Generate the exact rows at **https://email.gentic.email/smtp-credentials**: ### Inbound | Type | Host | Value | Notes | |------|------|-------|-------| | **A** | `@` | main site IP | **Keep** — marketing site | | **A** | `email` | `` | Web UI | | **A** | `mail` | `` | Postfix | | **MX** | `@` | `10 mail.gentic.email` | Priority 10 | ### Outbound authentication | Type | Host | Value | Notes | |------|------|-------|-------| | **TXT** | `@` | `v=spf1 mx a ip4: ~all` | SPF for `@gentic.email` mail | | **TXT** | `_spf` | `v=spf1 ip4: ~all` | **Required** — company domains SPF-include this | | **TXT** | `agmail._domainkey` | app DKIM public key | From `/smtp-credentials` | | **TXT** | `_dmarc` | `v=DMARC1; p=quarantine; rua=mailto:dmarc@gentic.email` | One record only | Remove any old Amazon SES Easy DKIM CNAMEs and `include:amazonses.com` from SPF. ### Firewall / security group | Port | Source | Purpose | |------|--------|---------| | 443 | `0.0.0.0/0` | HTTPS web UI | | 80 | `0.0.0.0/0` | HTTP → HTTPS redirect (Caddy) | | 25 | `0.0.0.0/0` | Inbound mail (+ outbound egress) | Do **not** expose 5432, 27017, 8000, or 2525 publicly. Confirm **egress** on port 25 is allowed (required for delivery to Gmail/Outlook): ```bash nc -vz -w 5 gmail-smtp-in.l.google.com 25 ``` ### Reverse DNS (PTR) — required for Gmail/Outlook Major receivers expect your VPS IP reverse DNS to match `mail.gentic.email`. ```bash dig +short -x YOUR_VPS_IP # Good: mail.gentic.email. ``` Set PTR in your VPS provider panel (is\*hosting: client area → network / rDNS, or open a support ticket) to `mail.gentic.email`. ### STARTTLS on Postfix (port 25) Gmail/Zoho use STARTTLS. Postfix generates a cert on first boot. After rebuild: ```bash printf 'EHLO test\r\nSTARTTLS\r\n' | nc mail.gentic.email 25 # expect: 220 Ready to start TLS (not 454 TLS not available) ``` ## Caddy / TLS Caddy obtains Let's Encrypt certificates automatically when: 1. `APP_PUBLIC_HOST=email.gentic.email` resolves to this server's public IP 2. Ports 80 and 443 are open 3. `CADDY_ACME_EMAIL` is set in `.env` Config: `deploy/caddy/Caddyfile` → reverse-proxies to `frontend:80`. ## Smoke test ```bash # HTTPS UI curl -fsSI https://email.gentic.email/health # Inbound mail (account must exist) swaks \ --server mail.gentic.email:25 \ --from sender@example.com \ --to existing-user@gentic.email \ --header "Subject: inbound smoke test" \ --body "Verify delivery" # Outbound queue after sending from the UI docker exec agmail-smtp postqueue -p curl -fsS http://127.0.0.1:8000/health ``` ## Operations ```bash docker compose logs -f caddy backend smtp docker compose restart caddy docker exec agmail-smtp postqueue -p ``` Persistent volumes: `postgres_data`, `mongo_data`, `smtp_queue`, `caddy_data`. ## TLS troubleshooting (`SSL_ERROR_INTERNAL_ERROR_ALERT`) This means Caddy is listening on 443 but **does not have a valid certificate** (Let's Encrypt issuance failed or left bad state in `caddy_data`). On the mail server: ```bash cd /opt/apps/agentic_email # 1. Confirm .env has a real contact email (use your Gmail if unsure) grep CADDY_ACME_EMAIL .env grep APP_PUBLIC_HOST .env # APP_PUBLIC_HOST=email.gentic.email # CADDY_ACME_EMAIL=you@gmail.com # 2. Check Caddy logs for ACME errors docker compose logs caddy --tail 80 # 3. Reset Caddy cert storage and recreate docker compose stop caddy docker volume rm agentic_email_caddy_data 2>/dev/null || docker volume rm email_caddy_data docker compose up -d --build caddy docker compose logs -f caddy ``` Wait 30–60 seconds, then test: ```bash curl -fsSI https://email.gentic.email/ ``` Requirements for Let's Encrypt: - `email.gentic.email` A record → this server's public IP - Firewall allows **inbound TCP 80 and 443** from `0.0.0.0/0` - `CADDY_ACME_EMAIL` set in `.env` If logs show rate limits, wait an hour or use a staging CA temporarily. ## API reference - `GET /health` — service checks (also proxied at `/health` on the web host) - `POST /api/v1/dns-setup/preview` — DNS generator (HTTP Basic Auth)