文章目录
前言
这篇文章对比配置完开发环境,写的生产环境配置的优化项。核心目标是减小镜像体积,提高安全性,解决开发环境配置切换为生产环境配置出现的问题。
开发环境配置详见:go docker-compose启动前后端分离项目 踩坑之旅-CSDN博客
步骤一:dockerfile配置
生产环境使用多阶段构建(构建阶段、运行阶段),对比开发环境使用单阶段构建,优势:
镜像体积小:单阶段构建包含go编译器等,多阶段构建仅包含构建好的二进制文件和CA证书
安全性更高:生产镜像中不包含源码和编译器,即使镜像泄露,也不会暴露核心代码,降低安全风险
可移植性强:通过CGO_ENABLED=0编译的静态二进制文件,可以在任意liunx系统上运行,无需依赖系统库
business后端项目dockerfile配置
dockerfile配置
FROM golang:1.24.5-alpine AS builder
WORKDIR /app/business
# 构建阶段
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# 核心编译命令,生成可执行二进制文件:
# CGO_ENABLED=0:禁用 CGO(避免依赖系统 C 库,实现 “静态链接”,让二进制文件可在任何 Linux 环境运行)
# GOOS=linux:指定目标运行系统为 Linux(确保编译产物适配后续的 Alpine 镜像)
# -a -installsuffix cgo:强制重新编译所有包,避免依赖 CGO 相关的编译产物
# -o business_dist:指定编译输出的二进制文件名为 app(存放在 /app/business/business_dist)
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o business_dist .
# 第二阶段:运行二进制文件(运行阶段)
FROM alpine:latest
WORKDIR /app/business
# 安装 CA 证书(如需 HTTPS 请求)
# RUN #apk --no-cache add ca-certificates
#1. --from=builder:从第一阶段(别名 builder)复制文件
#2. 将构建阶段生成的所有文件(核心是 business_dist 二进制文件)复制到当前阶段的 /app/business 目录
#3. 这一步是多阶段构建的关键:仅复制 “运行必需的二进制文件”,丢弃编译器、源代码等,大幅减小镜像体积
COPY --from=builder /app/business/business_dist ./
#拷贝数据库迁移脚本
COPY --from=builder /app/business/migrations ./migrations
RUN adduser -D -u 1000 appuser && \
chown -R appuser:appuser /app/business
USER appuser
EXPOSE 8080
CMD ["./business_dist"]
操作用户安全性配置
不使用root权限,创建并使用appuser用户(普通用户)运行项目;
appuser固定uid=1000(普通用户uid通常从1000开始,root用户的uid是0),方便控制。比如:项目内支持文件上传,上传文件要持久化就需要使用volumns(如下图)。如果项目支持预览功能,就需要打开宿主机上的文件,需要在宿主机上给appuser即uid=1000的用户/data/project/testdata/下文件的操作权限
# 宿主机上执行给uid=1000的用户/data/project/testdata/下文件的操作权限
chown -R 1000:1000 /data/project/testdata/tmp_file
如果没有开放权限,会出现无法打开文件的问题:
client前端项目dockerfile配置
dockerfile配置
在构建阶段,构建完成后删除yarn缓存、node_modules等来减少中间层体积。使用nginx来运行项目,nginx中配置压缩、静态资源缓存等优化。
说明:因为我使用的centos7(老旧了),node版本只能支持到<=17,但yarn依赖中存在要求node 18 | >20的,所以yarn不下来,就没有yarn.lock文件。正常有yarn.lock文件需要拷贝yarn.lock文件进容器,用来锁住版本使用一致依赖的。
#采用多阶段构建
#第一阶段(builder):用node环境编译代码(yarn build),生产静态文件
#第二阶段:基于轻量的nginx:alpine(仅~23MB)运行,只复制编译后的build目录,镜像体积可从几百MB缩小到~30MB
# 构建阶段
FROM node:20-alpine AS builder
WORKDIR /app/client
# 复制 package.json 和 yarn.lock(先复制依赖文件,利用 Docker 缓存)
#COPY package.json yarn.lock ./
COPY package.json ./
#因为yarn build需要构建工具:--production=false 安装包括devDependencies的所有依赖
#构建完成后删除node_modules和yarn缓存,减少中间层体积
#--frozen-lockfile确保依赖版本一致
#RUN yarn install --production=false --frozen-lockfile \
# && rm -rf /root/.npm /root/.yarn /usr/local/share./cache/yarn/v6 # 清理缓存
RUN yarn install --production=false \
&& rm -rf /root/.npm /root/.yarn /usr/local/share./cache/yarn/v6 # 清理缓存
COPY . .
RUN yarn build \
&& rm -rf node_modules # 清理构建阶段的依赖,减少中间层体积
# 运行阶段
FROM nginx:alpine
COPY --from=builder /app/client/dist /usr/share/nginx/html
# 把自定义的nginx.conf配置放进去,包含自己的优化,比如优化静态资源缓存、压缩等
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8000
CMD ["nginx", "-g", "daemon off;"]
nginx.conf
注意:代理转发要写后端服务的名字,不能写0.0.0.0或者localhost;proxy_pass http://backend-prod:8080;也不能写成这样:proxy_pass http://backend-prod:8080/api/;
server {
listen 8000;
root /usr/share/nginx/html;
index index.html;
# 解决 React Router History 模式刷新 404
location / {
try_files $uri $uri/ /index.html; # 所有未匹配的请求指向 index.html
}
# 代理 API 请求到后端服务(跨域处理)
location /api/ {
proxy_pass http://backend-prod:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; # 传递真实客户端 IP
}
# 静态资源缓存(JS/CSS/图片等)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Gzip 压缩
gzip on;
gzip_types text/css application/javascript image/svg+xml;
}
步骤二:后端使用migrations工具实现数据库初始化
开发环境使用volumns映射,直接执行的sql/init下的所有脚本,存在没有版本控制、无法失败回滚等问题。开发环境使用migrations工具维护数据库初始化。
# 安装迁移工具(支持 MySQL 驱动)
go install -tags 'mysql' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
# 验证安装(需确保 $GOPATH/bin 在 PATH 中,如 ~/go/bin)
migrate --version
# 在项目根目录创建迁移目录
mkdir -p migrations
# 生成初始化脚本(-seq 按序号命名,便于排序)
migrate create -ext sql -dir migrations -seq init_mysql_schema
# 生成两个文件:
# 000001_init_mysql_schema.up.sql:升级脚本(初始化表结构和基础数据)
# 000001_init_mysql_schema.down.sql:回滚脚本(删除表结构,用于异常恢复)
添加脚本内容:
代码集成migration:
dockerfile中拷贝migrations进入容器:
#拷贝数据库迁移脚本
COPY --from=builder /app/business/migrations ./migrations
问题一:脚本执行语法报错
查看后端日志,migrate提示语法错误如图:
解决方案:https://github.com/golang-migrate/migrate/issues/573
dsn需要添加multiStatements=true,因为一个sql脚本中有多个语句
问题二:migration脚本未更新成功,存在脏数据如何清理
错误信息:
| 2025/09/09 14:21:03 migrate 迁移工具初始化失败: Dirty database version 1. Fix and force version.
解决方案:
# 进入MySQL容器
docker-compose exec mysql-prod mysql -u root -p
# 在MySQL命令行中执行:
USE business;
-- 1. 检查当前迁移状态
SELECT * FROM schema_migrations;
-- 2. 如果dirty为1,修复状态
UPDATE schema_migrations SET dirty = 0 WHERE version = 1;
-- 3. 验证修复
SELECT * FROM schema_migrations;
问题三:插入数据中文无法识别
错误如图:
问题排查:尝试进入mysql手动插入一条包含中文的数据,发现终端无法输入中文。
# 在mysql-prod 容器中查看当前语言环境
echo $LANG
# 没有输出,系统会使用默认的POSIX locale(通常是ASCII),它不支持中文等非ASCII字符
解决方案:在docker-compose添加环境变量配置默认语言环境为utf-8
演示:
同时注意:dsn拼接和创建表的时候最好也明确charset=utf8mb4
//charset=utf8mb4:设置连接字符集
//collation=utf8mb4_unicode_ci:设置排序规则
var DSN = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&&collation=utf8mb4_unicode_ci&parseTime=True&multiStatements=true", Username, Password, Ip, Port, DbName)
CREATE TABLE xxx ( ... ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
详细排查记录:
查看数据库和表的字符集:
步骤三:.env配置秘密信息
.env中存储不方便提交到git仓库,仅本地使用的秘密信息,比如数据库密码;
生产环境、开发环境各一份,变量名称相同,内容不同;
项目启动时需要配置–env-file docker-compose -f docker-compose.prod.yml --env-file .env.prod .............
步骤四:.bashrc命令别名配置
命令太长可以使用.bashrc配置别名
# 命令终端生效配置
source ./.bashrc
步骤五:docker-compose.prod.yml配置
# docker-compose -f docker-compose.yml up -d
# alias dcprod up -d 存在.bashrc
services:
frontend-prod:
build: ./client
ports:
# # 宿主机的80 映射到 内部的8000
# - "80:8000"
- "8000:8000"
depends_on:
- backend-prod
networks:
- app-network
restart: unless-stopped
# MySQL服务
mysql-prod:
image: mysql:8.0
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
# 不添加无法识别中文
- LANG=C.UTF-8 # 添加语言环境变量
- LC_ALL=C.UTF.8 # 添加语言环境变量
volumes:
- /data/project/testdata/mysql_data:/var/lib/mysql
networks:
- app-network
restart: unless-stopped
healthcheck:
test: [ "CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p123456" ]
interval: 10s
timeout: 5s
retries: 5
backend-prod:
build: ./business
ports:
- "8080:8080"
depends_on:
mysql-prod:
condition: service_healthy
redis-prod:
condition: service_healthy
environment:
# 优化:账号密码相关放在.env文件中,不上传git,增强安全性
# 数据库和Redis配置
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_HOST=${MYSQL_HOST}
- MYSQL_PORT=${MYSQL_PORT}
- MYSQL_DBNAME=${MYSQL_DBNAME}
- REDIS_ADDR=${REDIS_ADDR}
# 文件存储路径(容器内路径)
- FILE_TMP_PATH=/app/testdata/tmp_file
- FILE_TARGET_PATH=/app/testdata/file
- GO_ENV=production
- TZ=Asia/Shanghai
volumes:
- /data/project/testdata/file:/app/testdata/file
- /data/project/testdata/tmp_file:/app/testdata/tmp_file
networks:
- app-network
restart: unless-stopped
healthcheck:
test: [ "CMD", "redis-cli", "ping" ]
interval: 10s
timeout: 5s
retries: 5
# Redis服务
redis-prod:
image: redis:alpine
ports:
- "6379:6379"
volumes:
- /data/project/testdata/redis_data:/data
networks:
- app-network
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
app-network:
driver: bridge
优化:后端服务等待mysql、redis健康检查通过再启动
backend-prod:
...
depends_on:
mysql-prod:
condition: service_healthy
redis-prod:
condition: service_healthy
todo优化:不做,记着
持久化数据存在宿主机/data/project/testdata/中,注意限制使用大小,避免占满(项目支持了大文件上传)。
nginx服务现在使用root权限跑的,要使用user nginx(普通用户)跑。普通用户就要考虑数据权限问题,在尝试添加如下的权限配置的时候还是遇到permission denied
# 使用非root用户运行(增强安全性) # 该命令含义:确保nginx用户对nginx运行所需的核心目录/文件有足够的权限 # chown 修改文件/目录所有者 -R 递归处理 nginx:nginx 将所有者改为nginx用户,所属用户组改为nginx组(nginx镜像默认内置nginx用户,用于安全运行服务) # /var/run/nginx.pid 运行时创建,并自动添加权限 # RUN chown -R nginx:nginx /usr/share/nginx/html /var/cache/nginx \ # && sed -i 's/^user root;/user nginx;/g' /etc/nginx/nginx.conf