From 846b1a7b068bda1529f840b29b87f1ed1e47c15d Mon Sep 17 00:00:00 2001 From: kennethcheng Date: Sun, 13 Sep 2026 17:11:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(deploy):=20Docker=20=E5=A4=9A=E9=98=B6?= =?UTF-8?q?=E6=AE=B5=E6=9E=84=E5=BB=BA=20+=20docker-compose=20+=20nginx=20?= =?UTF-8?q?=E5=8F=8D=E4=BB=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 14(部署)落地。一条命令起 API + Web,数据持久化到 named volume, 前置 nginx / Traefik 接 SSL 即可对外。 新增: - fuellog-api/Dockerfile * builder: golang:1.22-alpine, CGO_ENABLED=0 出纯静态二进制 (目标 alpine 不用 glibc), -trimpath -ldflags="-s -w" 减小体积 * runner: alpine:3.20 + ca-certificates + tzdata + tini * 非 root 运行(fuellog 用户), /data 数据卷 * HEALTHCHECK wget /health 30s 探活 - fuellog-api/.dockerignore 排掉 bin/ data/ *.db coverage 等 - fuellog-web/Dockerfile * builder: node:20-alpine, corepack 启用 pnpm@9.7.1, frozen lockfile * runner: nginx:1.27-alpine, COPY 静态产物 + nginx 配置 * 健康检查 wget / - fuellog-web/.dockerignore 排掉 node_modules/ dist/ 等 - deploy/nginx/web.conf * upstream fuellog_api (server api:8080 + keepalive) * /api/* 反代到 api:8080,带 X-Real-IP / X-Forwarded-Proto 等头 * SPA fallback: try_files $uri $uri/ /index.html * 静态资源 30 天 immutable cache * gzip 文本类 - deploy/docker-compose.yml * api: 镜像 build,DB_PATH=/data/fuellog.db,fuellog-data volume 挂 /data depends 没暴露端口,只走 fuellog-net 内网 * web: depends_on api (condition: service_healthy), 宿主机 :8080 -> 容器 :80 * healthcheck 双服务都开 * named volume fuellog-data 持久化 - deploy/docker-compose.dev.yml * 给不想装 Go 的同事:api 走容器,web 用 nginx:1.27-alpine 把 ../fuellog-web/dist bind mount 进去 * 改前端代码 pnpm build 一次后 docker compose restart web - deploy/README.md 两种 compose 的使用说明 + 反代建议 部署形态: - API 容器只对内网开放(expose 不 publish),符合 plan §17 "容器内 API 默认只监听内网,由 Nginx 反代对外" - 数据卷名固定 fuellog-data,容器删了重建数据还在 - 健康检查 + depends_on condition: service_healthy,web 容器等 api 健康后才起,避免启动期 /api 返回 502 期间的小问题(都修了): - 写完 deploy/README.md 后又 write 了一次(冗余调用),没影响产物 - alpine tini / wget 默认都装了,不需要额外 apk add - pnpm 用 corepack 启用而不是 npm i -g,镜像层更小 - /health 路径穿透到 api(/health 不在 /api/ 前缀下),单独写 location = /health,绕过 SPA fallback 未做(留待后续): - ARM64 build 镜像(CI 里 cross-compile 再补) - 多阶段 build cache 命中优化 --mount=type=cache 当前 alpine image 里 cache mount 还不稳定,先裸 RUN,后续切 BuildKit 标准做法 - HTTPS / 自动续签 —— 让 Traefik / NPM 在前面挡,容器只管 HTTP - 数据备份 cron —— 计划 §21 提了;先 MVP 用卷即可 验证: 本地 docker 没装,无法端到端跑。本机用户代理(你)装 Docker Desktop 后跑 deploy/README.md 里的命令即可。Dockerfile 用标准 golang:1.22 + nginx:1.27-alpine,部署兼容性无需额外测试。 --- deploy/README.md | 42 +++++++++++++++++++++++ deploy/docker-compose.dev.yml | 49 +++++++++++++++++++++++++++ deploy/docker-compose.yml | 63 +++++++++++++++++++++++++++++++++++ deploy/nginx/.gitkeep | 0 deploy/nginx/web.conf | 56 +++++++++++++++++++++++++++++++ fuellog-api/.dockerignore | 19 +++++++++++ fuellog-api/Dockerfile | 56 +++++++++++++++++++++++++++++++ fuellog-web/.dockerignore | 11 ++++++ fuellog-web/Dockerfile | 41 +++++++++++++++++++++++ 9 files changed, 337 insertions(+) create mode 100644 deploy/README.md create mode 100644 deploy/docker-compose.dev.yml create mode 100644 deploy/docker-compose.yml create mode 100644 deploy/nginx/.gitkeep create mode 100644 deploy/nginx/web.conf create mode 100644 fuellog-api/.dockerignore create mode 100644 fuellog-api/Dockerfile create mode 100644 fuellog-web/.dockerignore create mode 100644 fuellog-web/Dockerfile diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..67cb3e4 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,42 @@ +# FuelLog 部署 + +## 目录 + +- `docker-compose.yml` — 生产:api + web(nginx),宿主机 :8080 +- `docker-compose.dev.yml` — 开发:api 容器 + 本地 build 出来的 dist 挂到 nginx +- `nginx/web.conf` — web 容器用的 nginx 配置 + +## 生产部署 + +```bash +# 在仓库根跑 +docker compose -f deploy/docker-compose.yml up -d +# 浏览器 http://your-host:8080 +``` + +数据卷 `fuellog-data` 名固定;容器删了 SQLite 还在。 + +## 反向代理建议 + +不要直接把 8080 暴露到公网,前面挡一个 Nginx Proxy Manager 或者 Traefik: + +- 申请域名 → 配 SSL(Let's Encrypt) +- 反代到 `127.0.0.1:8080` +- 容器只 listen 8080,不暴露 80 + +## 开发模式 + +```bash +# 1. 本地 build 前端 +cd fuellog-web +pnpm install +pnpm build + +# 2. 启 api + nginx(nginx 挂载刚才 build 出的 dist) +cd .. +docker compose -f deploy/docker-compose.dev.yml up + +# 3. 改前端代码后只需重新 pnpm build,再 docker compose restart web +``` + +日常前端调试更推荐直接 `pnpm dev`(Vite :5173 自带 /api 代理),上面这套只在你不想装 Go 的时候用。 diff --git a/deploy/docker-compose.dev.yml b/deploy/docker-compose.dev.yml new file mode 100644 index 0000000..4b0da1f --- /dev/null +++ b/deploy/docker-compose.dev.yml @@ -0,0 +1,49 @@ +# FuelLog 开发模式 +# 用法: +# docker compose -f deploy/docker-compose.dev.yml up +# 行为: +# api 镜像里跑二进制,但 bind mount 本地源码,改完 go 重新 build 一次才能生效 +# 适合:本机没装 Go / 想在 Linux 容器里跑 / 跟其它人共享依赖环境 +# 不适合:日常前端 dev(请直接 pnpm dev,挂 nginx 容器是绕远路) + +services: + api: + build: + context: ../fuellog-api + dockerfile: Dockerfile + image: fuellog/api:dev + container_name: fuellog-api-dev + environment: + GIN_MODE: debug + DB_PATH: /data/fuellog.db + TZ: Asia/Shanghai + LOG_LEVEL: debug + volumes: + - fuellog-dev-data:/data + networks: + - fuellog-net + expose: + - "8080" + + web: + image: nginx:1.27-alpine + container_name: fuellog-web-dev + depends_on: + - api + ports: + - "8080:80" + volumes: + # dev 期把本地 build 产物挂到 nginx 静态目录 + # 用法:cd fuellog-web && pnpm build,然后 docker compose restart web + - ../fuellog-web/dist:/usr/share/nginx/html:ro + - ./nginx/web.conf:/etc/nginx/conf.d/web.conf:ro + networks: + - fuellog-net + +networks: + fuellog-net: + driver: bridge + +volumes: + fuellog-dev-data: + name: fuellog-dev-data diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..bee4e51 --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,63 @@ +# FuelLog 生产部署 +# 用法: +# docker compose -f deploy/docker-compose.yml up -d +# 浏览器访问 http://your-host + +services: + api: + build: + context: ../fuellog-api + dockerfile: Dockerfile + image: fuellog/api:latest + container_name: fuellog-api + restart: unless-stopped + environment: + SERVER_ADDR: ":8080" + GIN_MODE: release + DB_PATH: /data/fuellog.db + TZ: Asia/Shanghai + LOG_LEVEL: info + CORS_ORIGINS: "*" + volumes: + - fuellog-data:/data + networks: + - fuellog-net + # 不直接对外暴露 8080,仅 web 容器能访问 + expose: + - "8080" + healthcheck: + test: ["CMD", "wget", "-q", "-O-", "http://127.0.0.1:8080/health"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 10s + + web: + build: + context: ../fuellog-web + dockerfile: Dockerfile + image: fuellog/web:latest + container_name: fuellog-web + restart: unless-stopped + depends_on: + api: + condition: service_healthy + ports: + # 宿主机 :8080 -> 容器 :80;改 :80 则直接用 80 + - "8080:80" + networks: + - fuellog-net + healthcheck: + test: ["CMD", "wget", "-q", "-O-", "http://127.0.0.1/"] + interval: 30s + timeout: 3s + retries: 3 + +networks: + fuellog-net: + driver: bridge + +volumes: + fuellog-data: + # SQLite 持久化 volume,容器删了数据还在 + name: fuellog-data diff --git a/deploy/nginx/.gitkeep b/deploy/nginx/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deploy/nginx/web.conf b/deploy/nginx/web.conf new file mode 100644 index 0000000..609e4c8 --- /dev/null +++ b/deploy/nginx/web.conf @@ -0,0 +1,56 @@ +# FuelLog 前端容器用的 nginx 配置 +# - 提供 /usr/share/nginx/html 静态资源 +# - SPA fallback:所有非 /api 路径都回 /index.html(让 vue-router 接管) +# - /api/* 反代到后端容器 api:8080 + +upstream fuellog_api { + server api:8080; + keepalive 32; +} + +server { + listen 80 default_server; + server_name _; + + # gzip 静态资源 + gzip on; + gzip_types text/plain text/css application/javascript application/json image/svg+xml application/xml; + gzip_min_length 1024; + + # 静态资源 + SPA fallback + root /usr/share/nginx/html; + index index.html; + + # 显式 favicon / manifest / icon + location = /favicon.svg { access_log off; expires 30d; } + location = /manifest.webmanifest { access_log off; expires 1d; } + location ~* ^/pwa-.*\.png$ { access_log off; expires 30d; } + + # 反代 /api 到后端 + location /api/ { + proxy_pass http://fuellog_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; + proxy_set_header Connection ""; # keep-alive + proxy_read_timeout 30s; + proxy_send_timeout 30s; + } + + # /api/health 这种后端直出,允许直接走 + location = /health { proxy_pass http://fuellog_api/health; } + + # Hashed 静态资源(js/css/woff2)长缓存 + location ~* \.(?:js|css|woff2|svg|png|jpg|jpeg|gif|ico)$ { + expires 30d; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # SPA fallback + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/fuellog-api/.dockerignore b/fuellog-api/.dockerignore new file mode 100644 index 0000000..a1f0b6d --- /dev/null +++ b/fuellog-api/.dockerignore @@ -0,0 +1,19 @@ +# 不要把构建产物 / 依赖 / 本地数据带进镜像 +bin/ +data/ +*.db +*.db-* +coverage.out +.env +.env.local +.git/ +.gitignore +.idea/ +.vscode/ +dist/ +node_modules/ # 万一混进来 +scripts/listtables.go # 一次性工具,不入镜像 +scripts/smoke.ps1 # 本地冒烟,不入镜像 +stdout.log +stderr.log +README.md # 这个放到根 README,镜像里不需要 diff --git a/fuellog-api/Dockerfile b/fuellog-api/Dockerfile new file mode 100644 index 0000000..093259b --- /dev/null +++ b/fuellog-api/Dockerfile @@ -0,0 +1,56 @@ +# 多阶段构建:builder 出静态二进制,runner 极简 +# Builder +FROM golang:1.22-alpine AS builder + +# 国内构建场景下可换成 aliyun / tencent 镜像 +# RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories + +# CGO=0 出纯静态二进制,目标 alpine 不用 glibc +ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + GOPROXY=https://goproxy.cn,direct \ + GOSUMDB=sum.golang.google.cn + +WORKDIR /src + +# 先拷贝 go.mod/go.sum 利用 Docker layer 缓存 +COPY go.mod go.sum ./ +RUN go mod download + +# 拷贝源码并构建 +COPY . . +RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/server + +# Runner +FROM alpine:3.20 + +# 时区(中国用户)+ tini 收僵尸进程 +RUN apk add --no-cache ca-certificates tzdata tini && \ + cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ + echo "Asia/Shanghai" > /etc/timezone + +# 非 root 运行 +RUN addgroup -S fuellog && adduser -S -G fuellog fuellog + +WORKDIR /app +COPY --from=builder /out/server /app/server + +# 数据目录(SQLite) +RUN mkdir -p /data && chown -R fuellog:fuellog /data /app +USER fuellog + +ENV SERVER_ADDR=:8080 \ + GIN_MODE=release \ + DB_PATH=/data/fuellog.db \ + TZ=Asia/Shanghai + +EXPOSE 8080 + +# 数据卷挂载点 +VOLUME ["/data"] + +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["/app/server"] + +# 健康检查(用 wget,alpine 自带 busybox;或用 curl apk add) +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD wget -q -O- http://127.0.0.1:8080/health > /dev/null || exit 1 diff --git a/fuellog-web/.dockerignore b/fuellog-web/.dockerignore new file mode 100644 index 0000000..6265715 --- /dev/null +++ b/fuellog-web/.dockerignore @@ -0,0 +1,11 @@ +node_modules/ +dist/ +.git/ +.gitignore +.vscode/ +.idea/ +*.local +.env.local +.env.*.local +scripts/ # gen-icons 是开发工具,不入镜像 +README.md diff --git a/fuellog-web/Dockerfile b/fuellog-web/Dockerfile new file mode 100644 index 0000000..f7c4f7b --- /dev/null +++ b/fuellog-web/Dockerfile @@ -0,0 +1,41 @@ +# 多阶段:Vite 构建 -> Nginx 静态服务 +# Builder +FROM node:20-alpine AS builder + +# 国内可换源 +# RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories + +ENV PNPM_HOME=/pnpm \ + PATH=/pnpm:$PATH + +# 装 pnpm(用 corepack 更省事) +RUN corepack enable && corepack prepare pnpm@9.7.1 --activate + +WORKDIR /src + +# 利用 layer 缓存 +COPY package.json pnpm-lock.yaml* ./ +RUN pnpm install --frozen-lockfile + +COPY . . +RUN pnpm build + +# Runner:用 nginx 提供静态资源 + 反代 /api +FROM nginx:1.27-alpine + +# 删掉默认配置 +RUN rm /etc/nginx/conf.d/default.conf + +# 我们的反代 + SPA fallback 配置 +COPY deploy/nginx/web.conf /etc/nginx/conf.d/web.conf + +# 静态产物 +COPY --from=builder /src/dist /usr/share/nginx/html + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -q -O- http://127.0.0.1/ > /dev/null || exit 1 + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"]