Files
multica/docker-compose.selfhost.yml
Bohan Jiang 8db619c1cd fix(email): wire SMTP_EHLO_NAME through self-host config + docs [MUL-2984] (#3749)
* fix(email): wire SMTP_EHLO_NAME through self-host config + docs

Follow-up to #3679, which added SMTP_EHLO_NAME in code but never exposed
it to operators.

- docker-compose.selfhost.yml: pass SMTP_EHLO_NAME through to the backend
  container. The compose env block is an explicit allowlist, so without
  this the override set in .env was silently dropped and never reached
  the process — making the escape hatch unusable on the docker path.
- Document the var alongside its SMTP_* siblings: .env.example,
  SELF_HOSTING_ADVANCED.md, environment-variables.mdx, auth-setup.mdx,
  and self-host-quickstart.mdx (the last two with a strict-relay example).
- email.go: log when os.Hostname() fails instead of silently falling back
  to net/smtp's lazy "localhost" — the exact greeting strict relays reject.
- Add TestNewEmailService_EHLOName covering the env override, trimming,
  and the hostname fallback.

MUL-2984

Co-authored-by: multica-agent <github@multica.ai>

* fix(email): gate EHLO resolution to SMTP mode + sync docs to zh/ja/ko

Addresses review nits on this PR:

- email.go: resolve smtpEHLOName only when SMTP_HOST is set, so the
  Resend / DEV-stdout paths never call os.Hostname() or emit its
  failure log. The EHLO name is only ever used on the SMTP send path.
- docs: add SMTP_EHLO_NAME to the zh/ja/ko variants of
  environment-variables, self-host-quickstart, and auth-setup, in sync
  with the English docs updated earlier in this PR.

Note: the ja/ko self-host-quickstart and auth-setup pages were already
missing the port-465 implicit-TLS example (pre-existing i18n drift from
an earlier SMTP_TLS change, unrelated to this PR); the new EHLO block is
inserted at the correct logical anchor regardless. A full ja/ko re-sync
is left as a separate follow-up.

MUL-2984

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-04 14:44:55 +08:00

108 lines
4.5 KiB
YAML

# Self-hosting Docker Compose — starts PostgreSQL, backend, and frontend.
#
# Services bind to 127.0.0.1 only. For cross-machine or public access, front
# them with a reverse proxy (Caddy / nginx / Cloudflare Tunnel) that terminates
# TLS and forwards to 127.0.0.1:8080 (backend) and 127.0.0.1:3000 (frontend).
# Do NOT change these bindings to 0.0.0.0 — Docker bypasses host firewalls
# (UFW/iptables) by default, so the raw ports would be exposed to the internet
# with the default JWT_SECRET and Postgres credentials. See:
# apps/docs/content/docs/self-host-quickstart.mdx
#
# Usage:
# cp .env.example .env
# # Edit .env — change JWT_SECRET at minimum
# docker compose -f docker-compose.selfhost.yml up -d
#
# Frontend: http://localhost:${FRONTEND_PORT:-3000}
# Backend: http://localhost:${BACKEND_PORT:-${API_PORT:-${SERVER_PORT:-${PORT:-8080}}}}
name: multica
services:
postgres:
image: pgvector/pgvector:pg17
environment:
POSTGRES_DB: ${POSTGRES_DB:-multica}
POSTGRES_USER: ${POSTGRES_USER:-multica}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-multica}
volumes:
- pgdata:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-multica} -d ${POSTGRES_DB:-multica}"]
interval: 5s
timeout: 5s
retries: 5
backend:
image: ${MULTICA_BACKEND_IMAGE:-ghcr.io/multica-ai/multica-backend}:${MULTICA_IMAGE_TAG:-latest}
depends_on:
postgres:
condition: service_healthy
ports:
- "127.0.0.1:${BACKEND_PORT:-${API_PORT:-${SERVER_PORT:-${PORT:-8080}}}}:8080"
volumes:
- backend_uploads:/app/data/uploads
environment:
DATABASE_URL: postgres://${POSTGRES_USER:-multica}:${POSTGRES_PASSWORD:-multica}@postgres:5432/${POSTGRES_DB:-multica}?sslmode=disable
PORT: "8080"
METRICS_ADDR: ${METRICS_ADDR:-}
JWT_SECRET: ${JWT_SECRET:-change-me-in-production}
FRONTEND_ORIGIN: ${FRONTEND_ORIGIN:-http://localhost:${FRONTEND_PORT:-3000}}
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-}
RESEND_API_KEY: ${RESEND_API_KEY:-}
RESEND_FROM_EMAIL: ${RESEND_FROM_EMAIL:-noreply@multica.ai}
SMTP_HOST: ${SMTP_HOST:-}
SMTP_PORT: ${SMTP_PORT:-25}
SMTP_USERNAME: ${SMTP_USERNAME:-}
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
SMTP_TLS: ${SMTP_TLS:-}
SMTP_TLS_INSECURE: ${SMTP_TLS_INSECURE:-false}
SMTP_EHLO_NAME: ${SMTP_EHLO_NAME:-}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI:-http://localhost:${FRONTEND_PORT:-3000}/auth/callback}
S3_BUCKET: ${S3_BUCKET:-}
S3_REGION: ${S3_REGION:-us-west-2}
CLOUDFRONT_DOMAIN: ${CLOUDFRONT_DOMAIN:-}
CLOUDFRONT_KEY_PAIR_ID: ${CLOUDFRONT_KEY_PAIR_ID:-}
CLOUDFRONT_PRIVATE_KEY: ${CLOUDFRONT_PRIVATE_KEY:-}
COOKIE_DOMAIN: ${COOKIE_DOMAIN:-}
APP_ENV: ${APP_ENV:-production}
MULTICA_DEV_VERIFICATION_CODE: ${MULTICA_DEV_VERIFICATION_CODE:-}
MULTICA_APP_URL: ${MULTICA_APP_URL:-http://localhost:${FRONTEND_PORT:-3000}}
ALLOW_SIGNUP: ${ALLOW_SIGNUP:-true}
ALLOWED_EMAILS: ${ALLOWED_EMAILS:-}
ALLOWED_EMAIL_DOMAINS: ${ALLOWED_EMAIL_DOMAINS:-}
DISABLE_WORKSPACE_CREATION: ${DISABLE_WORKSPACE_CREATION:-}
GITHUB_APP_SLUG: ${GITHUB_APP_SLUG:-}
GITHUB_WEBHOOK_SECRET: ${GITHUB_WEBHOOK_SECRET:-}
# Public URL the API is reachable at from the open internet, no
# trailing slash. Used to mint absolute webhook URLs for autopilot
# webhook triggers. Leave unset behind a same-origin reverse proxy
# (e.g. plain localhost dev); the frontend will compose the URL
# from window.origin + webhook_path in that case. Headers are
# intentionally NOT used to derive this value, to avoid Host /
# X-Forwarded-Host spoofing on misconfigured proxies.
MULTICA_PUBLIC_URL: ${MULTICA_PUBLIC_URL:-}
# Comma-separated CIDRs whose source IP is allowed to set
# X-Forwarded-For / X-Real-IP for the webhook per-IP rate limiter.
# Empty default = headers ignored, RemoteAddr used. Set e.g.
# "127.0.0.1/32" when running behind a same-host reverse proxy.
MULTICA_TRUSTED_PROXIES: ${MULTICA_TRUSTED_PROXIES:-}
restart: unless-stopped
frontend:
image: ${MULTICA_WEB_IMAGE:-ghcr.io/multica-ai/multica-web}:${MULTICA_IMAGE_TAG:-latest}
depends_on:
- backend
ports:
- "127.0.0.1:${FRONTEND_PORT:-3000}:3000"
environment:
HOSTNAME: "0.0.0.0"
restart: unless-stopped
volumes:
pgdata:
backend_uploads: