From 1f964eefe2fed6451bf64e11f2668b8d057524eb Mon Sep 17 00:00:00 2001 From: LanQin Date: Mon, 15 Jun 2026 00:57:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(deploy):=20=E5=A2=9E=E5=8A=A0=E5=8D=95?= =?UTF-8?q?=E5=AE=B9=E5=99=A8=E9=83=A8=E7=BD=B2=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 `deploy/all-in-one` 构建与启动配置,将 API、Web、Nginx、Postfix、Dovecot、OpenDKIM 集成到一个容器中。 - 调整 `deploy/docker-compose.yml` 默认指向单容器部署,并补充分体调试编排 `deploy/docker-compose.stack.yml`。 - 更新 `README.md`、`deploy/README.md` 和 `.env.example`,同步说明新的部署方式与本地投递配置。 --- README.md | 23 ++++++++-- deploy/.env.example | 2 +- deploy/README.md | 38 ++++++++++++++-- deploy/all-in-one/Dockerfile | 42 +++++++++++++++++ deploy/all-in-one/entrypoint.sh | 59 ++++++++++++++++++++++++ deploy/all-in-one/nginx.conf | 23 ++++++++++ deploy/all-in-one/supervisord.conf | 55 ++++++++++++++++++++++ deploy/docker-compose.stack.yml | 73 ++++++++++++++++++++++++++++++ deploy/docker-compose.yml | 55 ++-------------------- 9 files changed, 310 insertions(+), 60 deletions(-) create mode 100644 deploy/all-in-one/Dockerfile create mode 100644 deploy/all-in-one/entrypoint.sh create mode 100644 deploy/all-in-one/nginx.conf create mode 100644 deploy/all-in-one/supervisord.conf create mode 100644 deploy/docker-compose.stack.yml diff --git a/README.md b/README.md index 3b1b044..9f30263 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # LanQin Email -LanQin Email 是一个自建邮箱 Webmail MVP:React/Vite + shadcn 风格组件前端,Go + SQLite 后端,部署层预留 Postfix/Dovecot/OpenDKIM 集成。 +LanQin Email 是一个自建邮箱 Webmail MVP:React/Vite + shadcn 风格组件前端,Go + SQLite 后端,默认使用单容器集成 Postfix、Dovecot、OpenDKIM。 ## 快速开发 @@ -44,7 +44,24 @@ npm run check:shadcn ## Docker 部署 -`deploy/docker-compose.yml` 提供 Linux 单机部署骨架:API、Web、Postfix、Dovecot、OpenDKIM、Nginx。真实公网收发前需要正确配置 MX/SPF/DKIM/DMARC,并确认云厂商开放 25/587/993 端口。 +默认单容器部署: + +```bash +cd deploy +cp .env.example .env +docker compose up -d --build +``` + +这个容器内部集成:API、Web、Nginx、Postfix、Dovecot、OpenDKIM。 + +如需多容器调试版: + +```bash +cd deploy +docker compose -f docker-compose.stack.yml up -d --build +``` + +真实公网收发前需要正确配置 MX/SPF/DKIM/DMARC,并确认云厂商开放 25/587/993 端口。 ## V1 能力 @@ -57,6 +74,6 @@ npm run check:shadcn ## 当前收发说明 - 本地开发:系统内邮箱互发可直接使用;未配置 `LANQIN_SMTP_HOST` 时,外部收件人不会真正投递到公网。 -- 服务器部署:`deploy/.env.example` 默认使用 `LANQIN_SMTP_HOST=postfix`,发件会交给 Postfix。 +- 服务器部署:`deploy/.env.example` 默认使用 `LANQIN_SMTP_HOST=127.0.0.1`,发件会交给同容器内 Postfix。 - 收件同步:Postfix/Dovecot 收到的 Maildir 邮件会由 API 的 Maildir worker 同步到 SQLite 后展示在 Webmail。 - Maildir worker 通过 `LANQIN_MAILDIR_ROOT` 和 `LANQIN_MAILDIR_SCAN_SECONDS` 控制,默认服务器路径为 `/var/mail/vhosts`。 diff --git a/deploy/.env.example b/deploy/.env.example index d6bb4eb..214c56e 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -8,7 +8,7 @@ LANQIN_ADDR=:8080 LANQIN_ALLOW_INSECURE_HTTP=false LANQIN_OPEN_REGISTRATION=false LANQIN_TWO_FACTOR_ENABLED=false -LANQIN_SMTP_HOST=postfix +LANQIN_SMTP_HOST=127.0.0.1 LANQIN_SMTP_PORT=25 LANQIN_SMTP_REQUIRE_TLS=false LANQIN_MAILDIR_ROOT=/var/mail/vhosts diff --git a/deploy/README.md b/deploy/README.md index 81c0ce0..aa85f5b 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,6 +1,6 @@ # LanQin Email Docker 部署说明 -## 启动 +## 默认:单容器部署 ```bash cd deploy @@ -9,9 +9,39 @@ cp .env.example .env docker compose up -d --build ``` +默认只启动一个业务容器: + +```text +lanqin-email +``` + +容器内部包含: + +- Go API +- Web 静态站点 +- Nginx +- Postfix +- Dovecot +- OpenDKIM + +常用日志: + +```bash +docker compose logs -f lanqin-email +``` + +## 可选:多容器调试部署 + +如果需要分别查看 Postfix / Dovecot / OpenDKIM 日志,可以使用保留的 stack 编排: + +```bash +cd deploy +docker compose -f docker-compose.stack.yml up -d --build +``` + ## DNS -进入 Web 管理后台后,在“DNS 记录”面板查看每个域名需要配置的: +进入 Web 管理后台后,在域名管理中查看每个域名需要配置的: - MX - SPF TXT @@ -30,6 +60,6 @@ docker compose up -d --build ## 生产注意 -- 替换 Dovecot 示例自签证书,建议在 Nginx 或边缘负载均衡终止 HTTPS。 +- 建议在服务器或边缘网关配置 HTTPS。 - 云厂商通常默认封禁 25 端口,需要单独申请解封。 -- SQLite 适合 V1 单机部署;多节点部署前迁移到 PostgreSQL,并把 Postfix/Dovecot maps 改为 PostgreSQL。 +- SQLite 适合 V1 单机部署;多节点部署前迁移到 PostgreSQL,并把 Postfix/Dovecot maps 改为 PostgreSQL。 diff --git a/deploy/all-in-one/Dockerfile b/deploy/all-in-one/Dockerfile new file mode 100644 index 0000000..c8d9620 --- /dev/null +++ b/deploy/all-in-one/Dockerfile @@ -0,0 +1,42 @@ +FROM golang:1.22-bookworm AS api-build +WORKDIR /src/apps/api +COPY apps/api/go.mod apps/api/go.sum ./ +RUN go mod download +COPY apps/api ./ +RUN CGO_ENABLED=0 GOOS=linux go build -o /out/lanqin-api ./cmd/server + +FROM node:20-bookworm-slim AS web-build +WORKDIR /src/apps/web +COPY apps/web/package.json apps/web/package-lock.json* ./ +RUN npm ci || npm install +COPY apps/web ./ +RUN npm run build + +FROM debian:bookworm-slim +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates tzdata sqlite3 supervisor nginx \ + postfix postfix-sqlite \ + dovecot-core dovecot-imapd dovecot-lmtpd dovecot-sqlite ssl-cert \ + opendkim opendkim-tools \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=api-build /out/lanqin-api /usr/local/bin/lanqin-api +COPY --from=web-build /src/apps/web/dist /usr/share/nginx/html + +COPY deploy/all-in-one/supervisord.conf /etc/supervisor/conf.d/lanqin.conf +COPY deploy/all-in-one/nginx.conf /etc/nginx/sites-enabled/default +COPY deploy/all-in-one/entrypoint.sh /entrypoint.sh + +COPY deploy/postfix/main.cf /etc/postfix/main.cf +COPY deploy/postfix/master.cf /etc/postfix/master.cf +COPY deploy/postfix/sqlite-*.cf /etc/postfix/ +COPY deploy/dovecot/dovecot.conf /etc/dovecot/dovecot.conf +COPY deploy/dovecot/dovecot-sql.conf.ext /etc/dovecot/dovecot-sql.conf.ext +COPY deploy/opendkim/opendkim.conf /etc/opendkim.conf +COPY deploy/opendkim/TrustedHosts /etc/opendkim/TrustedHosts + +RUN chmod +x /entrypoint.sh && mkdir -p /run/nginx /var/mail/vhosts /data /etc/opendkim/keys + +EXPOSE 80 443 25 587 993 +CMD ["/entrypoint.sh"] diff --git a/deploy/all-in-one/entrypoint.sh b/deploy/all-in-one/entrypoint.sh new file mode 100644 index 0000000..9732b76 --- /dev/null +++ b/deploy/all-in-one/entrypoint.sh @@ -0,0 +1,59 @@ +#!/bin/sh +set -eu + +: "${LANQIN_PUBLIC_HOSTNAME:=mail.example.com}" +: "${LANQIN_DATA_DIR:=/data}" +: "${LANQIN_DB_PATH:=/data/lanqin.db}" +: "${LANQIN_ADDR:=127.0.0.1:8080}" +: "${LANQIN_SMTP_HOST:=127.0.0.1}" +: "${LANQIN_SMTP_PORT:=25}" +: "${LANQIN_MAILDIR_ROOT:=/var/mail/vhosts}" + +export LANQIN_DATA_DIR LANQIN_DB_PATH LANQIN_ADDR LANQIN_SMTP_HOST LANQIN_SMTP_PORT LANQIN_MAILDIR_ROOT + +addgroup --system --gid 5000 vmail 2>/dev/null || true +adduser --system --uid 5000 --gid 5000 --home /var/mail/vhosts --no-create-home vmail 2>/dev/null || true +mkdir -p /data /var/mail/vhosts /etc/opendkim/keys /var/spool/postfix /var/run/dovecot +chown -R 5000:5000 /var/mail/vhosts + +postconf -e "myhostname = ${LANQIN_PUBLIC_HOSTNAME}" +postconf -e "myorigin = ${LANQIN_PUBLIC_HOSTNAME}" +postconf -e "virtual_transport = lmtp:inet:127.0.0.1:24" +postconf -e "smtpd_sasl_path = inet:127.0.0.1:12345" +postconf -e "smtpd_milters = inet:127.0.0.1:8891" +postconf -e "non_smtpd_milters = inet:127.0.0.1:8891" + +# OpenDKIM keys are generated after API seed/migrations create the SQLite DB. +/usr/local/bin/lanqin-api >/tmp/lanqin-api-bootstrap.log 2>&1 & +bootstrap_pid=$! +for i in $(seq 1 60); do + if [ -f "$LANQIN_DB_PATH" ]; then + users_count="$(sqlite3 "$LANQIN_DB_PATH" "SELECT COALESCE(COUNT(1),0) FROM users;" 2>/dev/null || echo 0)" + domains_count="$(sqlite3 "$LANQIN_DB_PATH" "SELECT COALESCE(COUNT(1),0) FROM domains;" 2>/dev/null || echo 0)" + if [ "${users_count:-0}" -gt 0 ] && [ "${domains_count:-0}" -gt 0 ]; then + break + fi + fi + sleep 1 +done +kill "$bootstrap_pid" 2>/dev/null || true +wait "$bootstrap_pid" 2>/dev/null || true + +: > /etc/opendkim/KeyTable +: > /etc/opendkim/SigningTable +if [ -f "$LANQIN_DB_PATH" ]; then + sqlite3 -separator '|' "$LANQIN_DB_PATH" "SELECT name, dkim_selector, dkim_private_key FROM domains WHERE status='active';" | while IFS='|' read -r domain selector private_key; do + [ -n "$domain" ] || continue + dir="/etc/opendkim/keys/$domain" + mkdir -p "$dir" + keyfile="$dir/$selector.private" + printf '%s' "$private_key" | base64 -d > "$keyfile" + chmod 600 "$keyfile" + echo "$selector._domainkey.$domain $domain:$selector:$keyfile" >> /etc/opendkim/KeyTable + echo "*@${domain} ${selector}._domainkey.${domain}" >> /etc/opendkim/SigningTable + done +fi +chown -R opendkim:opendkim /etc/opendkim + +postfix check +exec /usr/bin/supervisord -c /etc/supervisor/conf.d/lanqin.conf diff --git a/deploy/all-in-one/nginx.conf b/deploy/all-in-one/nginx.conf new file mode 100644 index 0000000..c548dd6 --- /dev/null +++ b/deploy/all-in-one/nginx.conf @@ -0,0 +1,23 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + location /api/ { + proxy_pass http://127.0.0.1:8080/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /healthz { + proxy_pass http://127.0.0.1:8080/healthz; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/deploy/all-in-one/supervisord.conf b/deploy/all-in-one/supervisord.conf new file mode 100644 index 0000000..0fd1044 --- /dev/null +++ b/deploy/all-in-one/supervisord.conf @@ -0,0 +1,55 @@ +[supervisord] +nodaemon=true +logfile=/dev/null +logfile_maxbytes=0 +pidfile=/run/supervisord.pid + +[program:api] +command=/usr/local/bin/lanqin-api +autostart=true +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/fd/2 +stderr_logfile_maxbytes=0 +priority=10 + +[program:dovecot] +command=/usr/sbin/dovecot -F +autostart=true +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/fd/2 +stderr_logfile_maxbytes=0 +priority=20 + +[program:opendkim] +command=/usr/sbin/opendkim -f -x /etc/opendkim.conf +autostart=true +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/fd/2 +stderr_logfile_maxbytes=0 +priority=30 + +[program:postfix] +command=/usr/sbin/postfix start-fg +autostart=true +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/fd/2 +stderr_logfile_maxbytes=0 +priority=40 + +[program:nginx] +command=/usr/sbin/nginx -g "daemon off;" +autostart=true +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/fd/2 +stderr_logfile_maxbytes=0 +priority=50 diff --git a/deploy/docker-compose.stack.yml b/deploy/docker-compose.stack.yml new file mode 100644 index 0000000..7fd8a84 --- /dev/null +++ b/deploy/docker-compose.stack.yml @@ -0,0 +1,73 @@ +version: "3.9" + +services: + api: + build: + context: .. + dockerfile: deploy/api.Dockerfile + env_file: .env + volumes: + - lanqin-data:/data + - maildata:/var/mail/vhosts:ro + depends_on: + - dovecot + - postfix + restart: unless-stopped + + web: + build: + context: .. + dockerfile: deploy/web.Dockerfile + restart: unless-stopped + + nginx: + image: nginx:1.27-alpine + volumes: + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + ports: + - "80:80" + - "443:443" + depends_on: + - api + - web + restart: unless-stopped + + postfix: + build: + context: ./postfix + env_file: .env + volumes: + - lanqin-data:/data + - maildata:/var/mail/vhosts + ports: + - "25:25" + - "587:587" + depends_on: + - dovecot + - opendkim + restart: unless-stopped + + dovecot: + build: + context: ./dovecot + env_file: .env + volumes: + - lanqin-data:/data + - maildata:/var/mail/vhosts + ports: + - "993:993" + restart: unless-stopped + + opendkim: + build: + context: ./opendkim + env_file: .env + volumes: + - lanqin-data:/data:ro + - dkim-keys:/etc/opendkim/keys + restart: unless-stopped + +volumes: + lanqin-data: + maildata: + dkim-keys: diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 7fd8a84..d199347 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,69 +1,20 @@ version: "3.9" services: - api: + lanqin-email: build: context: .. - dockerfile: deploy/api.Dockerfile + dockerfile: deploy/all-in-one/Dockerfile env_file: .env - volumes: - - lanqin-data:/data - - maildata:/var/mail/vhosts:ro - depends_on: - - dovecot - - postfix - restart: unless-stopped - - web: - build: - context: .. - dockerfile: deploy/web.Dockerfile - restart: unless-stopped - - nginx: - image: nginx:1.27-alpine - volumes: - - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro ports: - "80:80" - "443:443" - depends_on: - - api - - web - restart: unless-stopped - - postfix: - build: - context: ./postfix - env_file: .env - volumes: - - lanqin-data:/data - - maildata:/var/mail/vhosts - ports: - "25:25" - "587:587" - depends_on: - - dovecot - - opendkim - restart: unless-stopped - - dovecot: - build: - context: ./dovecot - env_file: .env + - "993:993" volumes: - lanqin-data:/data - maildata:/var/mail/vhosts - ports: - - "993:993" - restart: unless-stopped - - opendkim: - build: - context: ./opendkim - env_file: .env - volumes: - - lanqin-data:/data:ro - dkim-keys:/etc/opendkim/keys restart: unless-stopped