go docker-compose对比开发环境配置的生产环境配置优化

发布于:2025-09-13 ⋅ 阅读:(19) ⋅ 点赞:(0)

前言

这篇文章对比配置完开发环境,写的生产环境配置的优化项。核心目标是减小镜像体积,提高安全性,解决开发环境配置切换为生产环境配置出现的问题。

开发环境配置详见: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优化:不做,记着

  1. 持久化数据存在宿主机/data/project/testdata/中,注意限制使用大小,避免占满(项目支持了大文件上传)。

  2. 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