From 913a0ef4201abfb7a60f474beda0672899955876 Mon Sep 17 00:00:00 2001 From: LanQin Date: Mon, 15 Jun 2026 22:05:41 +0800 Subject: [PATCH] =?UTF-8?q?chore(deploy):=20=E5=A2=9E=E5=BC=BA=20Docker=20?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E4=B8=8E=E5=8F=91=E5=B8=83=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 GitHub Actions Docker 工作流,自动执行前端检查、后端测试并发布 GHCR 镜像。 - 为单容器与多容器部署补充镜像配置、使用说明和环境变量示例。 - 优化各 Dockerfile 的构建缓存与镜像体积,提升构建速度并减少无关文件进入镜像。 --- .dockerignore | 51 +++++++++++ .github/workflows/docker.yml | 144 ++++++++++++++++++++++++++++++++ README.md | 11 +++ deploy/.env.example | 100 +++++++++++++++++++++- deploy/README.md | 39 +++++++++ deploy/all-in-one/Dockerfile | 20 +++-- deploy/api.Dockerfile | 14 +++- deploy/docker-compose.stack.yml | 5 ++ deploy/docker-compose.yml | 1 + deploy/dovecot/Dockerfile | 7 +- deploy/opendkim/Dockerfile | 7 +- deploy/postfix/Dockerfile | 7 +- deploy/web.Dockerfile | 5 +- 13 files changed, 396 insertions(+), 15 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2e05e77 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,51 @@ +.git +.github +.claude +.cursor +.vscode +.idea + +node_modules +**/node_modules +dist +**/dist +build +**/build +.vite +**/.vite +.cache +**/.cache +coverage +**/coverage +*.tsbuildinfo + +tmp +data +maildata +dkim-keys +logs +*.log + +.env +.env.* +!*.env.example +!**/.env.example + +*.db +*.db-shm +*.db-wal +*.sqlite +*.sqlite3 + +*.pem +*.key +*.crt + +*.exe +*.test +*.out +coverage.out + +.DS_Store +Thumbs.db +desktop.ini diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..56686cb --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,144 @@ +name: CI and Docker + +on: + push: + branches: + - main + - master + tags: + - "v*" + pull_request: + workflow_dispatch: + +permissions: + contents: read + packages: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + PLATFORMS: linux/amd64 + +jobs: + checks: + name: Check web and api + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: apps/web/package-lock.json + + - name: Install web dependencies + working-directory: apps/web + run: npm ci + + - name: Check shadcn/ui usage + working-directory: apps/web + run: npm run check:shadcn + + - name: Build web + working-directory: apps/web + run: npm run build + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: apps/api/go.mod + cache-dependency-path: apps/api/go.sum + + - name: Test api + working-directory: apps/api + run: go test ./... + + docker: + name: Build and publish ${{ matrix.name }} + runs-on: ubuntu-latest + needs: checks + if: github.event_name != 'pull_request' + strategy: + fail-fast: false + matrix: + include: + - name: all-in-one + suffix: "" + context: . + file: ./deploy/all-in-one/Dockerfile + - name: api + suffix: -api + context: . + file: ./deploy/api.Dockerfile + - name: web + suffix: -web + context: . + file: ./deploy/web.Dockerfile + - name: postfix + suffix: -postfix + context: ./deploy/postfix + file: ./deploy/postfix/Dockerfile + - name: dovecot + suffix: -dovecot + context: ./deploy/dovecot + file: ./deploy/dovecot/Dockerfile + - name: opendkim + suffix: -opendkim + context: ./deploy/opendkim + file: ./deploy/opendkim/Dockerfile + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Prepare image name + id: image + shell: bash + run: | + image="${REGISTRY}/${GITHUB_REPOSITORY}${{ matrix.suffix }}" + echo "name=${image,,}" >> "$GITHUB_OUTPUT" + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ steps.image.outputs.name }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=sha- + labels: | + org.opencontainers.image.title=LanQin Email ${{ matrix.name }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ${{ matrix.context }} + file: ${{ matrix.file }} + platforms: ${{ env.PLATFORMS }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: | + type=gha,scope=lanqin-${{ matrix.name }} + type=registry,ref=${{ steps.image.outputs.name }}:buildcache + cache-to: | + type=gha,mode=max,scope=lanqin-${{ matrix.name }} + type=registry,ref=${{ steps.image.outputs.name }}:buildcache,mode=max diff --git a/README.md b/README.md index 9f30263..4223b5a 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,17 @@ docker compose up -d --build 这个容器内部集成:API、Web、Nginx、Postfix、Dovecot、OpenDKIM。 +也可以直接使用 GitHub Actions 自动发布到 GHCR 的镜像: + +```bash +cd deploy +cp .env.example .env +# 修改 .env 里的域名、管理员密码,以及 LANQIN_IMAGE +# LANQIN_IMAGE=ghcr.io//:latest +docker compose pull +docker compose up -d +``` + 如需多容器调试版: ```bash diff --git a/deploy/.env.example b/deploy/.env.example index 214c56e..0cf99d5 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -1,16 +1,112 @@ +# LanQin Email 部署环境变量示例 +# 用法:cp deploy/.env.example deploy/.env,然后修改 deploy/.env。 +# 注意:.env 里不要写引号;真实密码、Secret Key 不要提交到 Git。 + +# ========================= +# Docker 镜像 +# ========================= +# 单容器部署镜像。默认 local 表示本机 docker compose up --build 构建。 +# 如果使用 GitHub Actions 发布到 GHCR,改成:ghcr.io//:latest +LANQIN_IMAGE=lanqin-email:local + +# 多容器调试版镜像。只在 docker-compose.stack.yml 中使用。 +LANQIN_API_IMAGE=lanqin-email-api:local +LANQIN_WEB_IMAGE=lanqin-email-web:local +LANQIN_POSTFIX_IMAGE=lanqin-email-postfix:local +LANQIN_DOVECOT_IMAGE=lanqin-email-dovecot:local +LANQIN_OPENDKIM_IMAGE=lanqin-email-opendkim:local + +# ========================= +# 对外访问地址 +# ========================= +# 邮件服务器主机名,一般填你的邮件域名,例如 mail.example.com。 +# 会影响 Postfix myhostname、DNS 展示、邮件链接等。 LANQIN_PUBLIC_HOSTNAME=mail.example.com + +# Webmail 对外访问地址。生产环境建议使用 https。 LANQIN_PUBLIC_BASE_URL=https://mail.example.com + +# ========================= +# 初始管理员 +# ========================= +# 第一次启动时会创建这个管理员账号。 LANQIN_ADMIN_EMAIL=admin@example.com + +# 生产环境必须改掉默认密码。 LANQIN_ADMIN_PASSWORD=ChangeMe123! -LANQIN_DATA_DIR=/data -LANQIN_DB_PATH=/data/lanqin.db + +# ========================= +# API / 数据存储 +# ========================= +# API 监听地址。单容器里由 Nginx 反代到 127.0.0.1:8080。 LANQIN_ADDR=:8080 + +# 容器内数据目录。docker-compose.yml 已把 lanqin-data 挂到 /data。 +LANQIN_DATA_DIR=/data + +# SQLite 数据库路径。 +LANQIN_DB_PATH=/data/lanqin.db + +# Session Cookie 名称,一般不用改。 +LANQIN_COOKIE_NAME=lanqin_session + +# 登录会话有效期,单位:小时。默认 168 小时,即 7 天。 +LANQIN_SESSION_TTL_HOURS=168 + +# 是否允许非 HTTPS 下的 Cookie。生产环境建议 false;本地 HTTP 调试可改 true。 LANQIN_ALLOW_INSECURE_HTTP=false + +# ========================= +# 注册与安全 +# ========================= +# 是否开放用户自行注册。 LANQIN_OPEN_REGISTRATION=false + +# 是否启用 2FA 功能总开关。 LANQIN_TWO_FACTOR_ENABLED=false + +# 是否启用 Cloudflare Turnstile 人机验证。 +LANQIN_TURNSTILE_ENABLED=false + +# Turnstile Site Key,前端公开使用。未启用时可以留空。 +LANQIN_TURNSTILE_SITE_KEY= + +# Turnstile Secret Key,服务端校验使用。未启用时可以留空。 +LANQIN_TURNSTILE_SECRET_KEY= + +# ========================= +# SMTP 发信 +# ========================= +# 单容器部署默认提交给容器内 Postfix。 +# 如果要走外部 SMTP,把 Host/Port/Username/Password 改成外部服务配置。 LANQIN_SMTP_HOST=127.0.0.1 LANQIN_SMTP_PORT=25 +LANQIN_SMTP_USERNAME= +LANQIN_SMTP_PASSWORD= + +# 外部 SMTP 要求 STARTTLS / TLS 时改 true;本机 Postfix 默认 false。 LANQIN_SMTP_REQUIRE_TLS=false + +# ========================= +# 收件 / Maildir 同步 +# ========================= +# Dovecot Maildir 根目录。docker-compose.yml 已把 maildata 挂到这里。 LANQIN_MAILDIR_ROOT=/var/mail/vhosts + +# 扫描 Maildir 的间隔,单位:秒。 LANQIN_MAILDIR_SCAN_SECONDS=30 + +# 无人收件开关。false:未注册邮箱默认拒收;true:未注册收件地址进入全部邮件。 +LANQIN_CATCH_ALL_ENABLED=false + +# Webmail 是否自动轮询刷新邮件。 +LANQIN_MAIL_AUTO_REFRESH=true + +# 自动刷新间隔,单位:秒。 +LANQIN_MAIL_REFRESH_SECONDS=30 + +# ========================= +# 系统 +# ========================= +# 容器时区。 TZ=Asia/Shanghai diff --git a/deploy/README.md b/deploy/README.md index aa85f5b..99064cc 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -30,6 +30,45 @@ lanqin-email docker compose logs -f lanqin-email ``` +## 使用 GitHub Actions 构建好的镜像 + +仓库已添加 `.github/workflows/docker.yml`: + +- pull request:检查前端 shadcn 规则、前端构建、后端测试。 +- push 到 `main` / `master`:检查通过后发布 Docker 镜像到 GHCR。 +- push tag,例如 `v1.0.0`:发布对应 tag 镜像。 + +默认发布镜像: + +```text +ghcr.io//:latest +ghcr.io//-api:latest +ghcr.io//-web:latest +ghcr.io//-postfix:latest +ghcr.io//-dovecot:latest +ghcr.io//-opendkim:latest +``` + +单容器部署时,把 `.env` 里的 `LANQIN_IMAGE` 改成你的 GHCR 镜像: + +```env +LANQIN_IMAGE=ghcr.io//:latest +``` + +然后在服务器执行: + +```bash +cd deploy +docker compose pull +docker compose up -d +``` + +如果镜像是私有的,服务器需要先登录 GHCR: + +```bash +echo "" | docker login ghcr.io -u --password-stdin +``` + ## 可选:多容器调试部署 如果需要分别查看 Postfix / Dovecot / OpenDKIM 日志,可以使用保留的 stack 编排: diff --git a/deploy/all-in-one/Dockerfile b/deploy/all-in-one/Dockerfile index c8d9620..69d1ed5 100644 --- a/deploy/all-in-one/Dockerfile +++ b/deploy/all-in-one/Dockerfile @@ -1,25 +1,33 @@ +# syntax=docker/dockerfile:1.7 + 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 +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download COPY apps/api ./ -RUN CGO_ENABLED=0 GOOS=linux go build -o /out/lanqin-api ./cmd/server +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + CGO_ENABLED=0 GOOS=linux go build -trimpath -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 +RUN --mount=type=cache,target=/root/.npm \ + npm ci --prefer-offline --no-audit || npm install --prefer-offline --no-audit 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 \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ + rm -f /etc/apt/apt.conf.d/docker-clean && \ + 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/* + opendkim opendkim-tools COPY --from=api-build /out/lanqin-api /usr/local/bin/lanqin-api COPY --from=web-build /src/apps/web/dist /usr/share/nginx/html diff --git a/deploy/api.Dockerfile b/deploy/api.Dockerfile index c7c74d3..1e5f8e1 100644 --- a/deploy/api.Dockerfile +++ b/deploy/api.Dockerfile @@ -1,12 +1,20 @@ +# syntax=docker/dockerfile:1.7 + FROM golang:1.22-bookworm AS build WORKDIR /src/apps/api COPY apps/api/go.mod apps/api/go.sum ./ -RUN go mod download +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download COPY apps/api ./ -RUN CGO_ENABLED=0 GOOS=linux go build -o /out/lanqin-api ./cmd/server +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + CGO_ENABLED=0 GOOS=linux go build -trimpath -o /out/lanqin-api ./cmd/server FROM debian:bookworm-slim -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates tzdata && rm -rf /var/lib/apt/lists/* +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ + rm -f /etc/apt/apt.conf.d/docker-clean && \ + apt-get update && apt-get install -y --no-install-recommends ca-certificates tzdata WORKDIR /app COPY --from=build /out/lanqin-api /usr/local/bin/lanqin-api EXPOSE 8080 diff --git a/deploy/docker-compose.stack.yml b/deploy/docker-compose.stack.yml index 7fd8a84..a144233 100644 --- a/deploy/docker-compose.stack.yml +++ b/deploy/docker-compose.stack.yml @@ -2,6 +2,7 @@ version: "3.9" services: api: + image: ${LANQIN_API_IMAGE:-lanqin-email-api:local} build: context: .. dockerfile: deploy/api.Dockerfile @@ -15,6 +16,7 @@ services: restart: unless-stopped web: + image: ${LANQIN_WEB_IMAGE:-lanqin-email-web:local} build: context: .. dockerfile: deploy/web.Dockerfile @@ -33,6 +35,7 @@ services: restart: unless-stopped postfix: + image: ${LANQIN_POSTFIX_IMAGE:-lanqin-email-postfix:local} build: context: ./postfix env_file: .env @@ -48,6 +51,7 @@ services: restart: unless-stopped dovecot: + image: ${LANQIN_DOVECOT_IMAGE:-lanqin-email-dovecot:local} build: context: ./dovecot env_file: .env @@ -59,6 +63,7 @@ services: restart: unless-stopped opendkim: + image: ${LANQIN_OPENDKIM_IMAGE:-lanqin-email-opendkim:local} build: context: ./opendkim env_file: .env diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index d199347..03e5efa 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -2,6 +2,7 @@ version: "3.9" services: lanqin-email: + image: ${LANQIN_IMAGE:-lanqin-email:local} build: context: .. dockerfile: deploy/all-in-one/Dockerfile diff --git a/deploy/dovecot/Dockerfile b/deploy/dovecot/Dockerfile index bffb7fa..b0ca9a7 100644 --- a/deploy/dovecot/Dockerfile +++ b/deploy/dovecot/Dockerfile @@ -1,6 +1,11 @@ +# syntax=docker/dockerfile:1.7 + FROM debian:bookworm-slim ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y --no-install-recommends dovecot-core dovecot-imapd dovecot-lmtpd dovecot-sqlite ssl-cert ca-certificates && rm -rf /var/lib/apt/lists/* +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ + rm -f /etc/apt/apt.conf.d/docker-clean && \ + apt-get update && apt-get install -y --no-install-recommends dovecot-core dovecot-imapd dovecot-lmtpd dovecot-sqlite ssl-cert ca-certificates COPY dovecot.conf /etc/dovecot/dovecot.conf COPY dovecot-sql.conf.ext /etc/dovecot/dovecot-sql.conf.ext COPY entrypoint.sh /entrypoint.sh diff --git a/deploy/opendkim/Dockerfile b/deploy/opendkim/Dockerfile index 3401531..b9696ba 100644 --- a/deploy/opendkim/Dockerfile +++ b/deploy/opendkim/Dockerfile @@ -1,6 +1,11 @@ +# syntax=docker/dockerfile:1.7 + FROM debian:bookworm-slim ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y --no-install-recommends opendkim opendkim-tools sqlite3 ca-certificates && rm -rf /var/lib/apt/lists/* +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ + rm -f /etc/apt/apt.conf.d/docker-clean && \ + apt-get update && apt-get install -y --no-install-recommends opendkim opendkim-tools sqlite3 ca-certificates COPY opendkim.conf /etc/opendkim.conf COPY TrustedHosts /etc/opendkim/TrustedHosts COPY entrypoint.sh /entrypoint.sh diff --git a/deploy/postfix/Dockerfile b/deploy/postfix/Dockerfile index 2690c4d..65ac390 100644 --- a/deploy/postfix/Dockerfile +++ b/deploy/postfix/Dockerfile @@ -1,6 +1,11 @@ +# syntax=docker/dockerfile:1.7 + FROM debian:bookworm-slim ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y --no-install-recommends postfix postfix-sqlite ca-certificates rsyslog && rm -rf /var/lib/apt/lists/* +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ + rm -f /etc/apt/apt.conf.d/docker-clean && \ + apt-get update && apt-get install -y --no-install-recommends postfix postfix-sqlite ca-certificates rsyslog COPY main.cf /etc/postfix/main.cf COPY master.cf /etc/postfix/master.cf COPY sqlite-*.cf /etc/postfix/ diff --git a/deploy/web.Dockerfile b/deploy/web.Dockerfile index 4c98379..4cb72b5 100644 --- a/deploy/web.Dockerfile +++ b/deploy/web.Dockerfile @@ -1,7 +1,10 @@ +# syntax=docker/dockerfile:1.7 + FROM node:20-bookworm-slim AS build WORKDIR /src/apps/web COPY apps/web/package.json apps/web/package-lock.json* ./ -RUN npm ci || npm install +RUN --mount=type=cache,target=/root/.npm \ + npm ci --prefer-offline --no-audit || npm install --prefer-offline --no-audit COPY apps/web ./ RUN npm run build