feat(deploy): Docker 多阶段构建 + docker-compose + nginx 反代
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,部署兼容性无需额外测试。
This commit is contained in:
@@ -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 的时候用。
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,镜像里不需要
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.local
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
scripts/ # gen-icons 是开发工具,不入镜像
|
||||||
|
README.md
|
||||||
@@ -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;"]
|
||||||
Reference in New Issue
Block a user