跳到主要内容

记一次部署

· 阅读需 6 分钟

记账项目部署

将项目前端、后端、数据库这三个服务打包的镜像,push 到 docker hub,再利用 docker compose 统一拉取新版本镜像进行服务端部署

后端

源码:https://github.com/GSemir0418/account-app-gin

镜像:https://hub.docker.com/r/gsemir/account-app-backend

  • 流程

容器共需两个基础镜像,一个用来构建golang:1.21-alpine3.20 AS builder),一个用来运行项目(alpine:latest

构建时先复制依赖文件,安装依赖,之后再将源码复制到容器进行构建

构建产物及环境变量文件复制到运行时的镜像中,启动项目即可

  • 区别环境变量

在构建运行时镜像配置中,添加容器环境变量;启动项目之前将环境变量文件复制到运行时容器中

# ...
FROM alpine:latest
ENV GIN_ENV prod
ENV GIN_MODE release
# ...
WORKDIR /app
COPY --from=builder /app/main /app/main
COPY --from=builder /app/.env.prod /app/.env.prod
# ...

然后代码中通过 GIN_ENV 区别加载的文件,注意是运行时才会区别环境而不是构建时

// 获取环境变量 GIN_ENV,默认值为 "dev"
env := os.Getenv("GIN_ENV")
if env == "" {
env = "dev"
}

// 确定要加载的 .env 文件
envFile := filepath.Join(basepath, "..", ".env."+env)
// 加载环境文件
err := godotenv.Load(envFile)
  • 数据库连接要配置容器名而不是 localhost 或 ip
# env
DB_DSN=username:password@tcp(mysql-container-name:3306)/db_name?charset=utf8mb4&parseTime=True&loc=Local
  • 记得配置 go 代理,加速依赖包
FROM golang:1.21-alpine3.20 AS builder
WORKDIR /app
ENV GOPROXY=https://goproxy.cn,direct
# ...
  • docker build 报错
failed commit on ref "layer-sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1": "layer-sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1" failed size validation: 0 != 32: failed precondition

解决方法:使用 docker pull 手动拉取镜像

  • 先同步数据库,再启动项目
CMD ["sh", "-c", "./main db migrate:create && ./main server"]
  • 镜像构建脚本
#!/bin/bash
# push.sh

VERSION=$1
if [ -z "$VERSION" ]; then
echo "Error: No version specified."
exit 1
fi

docker build -t account-app-backend:$VERSION .
docker tag account-app-backend:$VERSION gsemir/account-app-backend:$VERSION
docker push gsemir/account-app-backend:$VERSION

先构建镜像,再打标签,最后 push 到 dockerhub。在执行脚本时必须传递版本号 sh push.sh 0.1.0

前端

源码:https://github.com/GSemir0418/account-app-vite

镜像:https://hub.docker.com/r/gsemir/account-app-frontend

  • 流程

与后端差不多,需要两个基础镜像,一个用来构建(node:20-alpine3.20 AS build),一个用来运行(nginx:alpine

构建时同样先复制 package.json 与 package-lock.json 到镜像,安装依赖,之后再打包

启动时使用 nginx 开启服务即可

  • 跨域

使用 nignx 反向代理浏览器发出的 api 接口请求

location /api/ {
proxy_pass http://account-app-backend:8080/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

此时前端的 axios 的 baseURL 要使用本地的 origin,即 window.location.origin,不能写死

写死的话 nginx 根本不会匹配到对应的 url 并转发,只能靠后端改 CORS 响应头

  • Vite 多环境配置

根目录定义 .env.development.env.production 等多环境环境变量文件

页面中使用 import.meta.env.XXX_XXX 来访问环境变量

注意 Vite 会在构建时替换对应的环境变量的值,因此在 build 项目前要声明 NODE_ENV 环境变量

  • NODE_ENV 与依赖

这里还存在一个问题,就是如果设置了 NODE_ENV=production 环境变量,npm 在安装依赖时会忽略掉 devDependencies

但是通常情况下,构建阶段应包含所有开发依赖。所以这里我们把设置 NODE_ENV 的步骤放到安装依赖之后、打包项目之前

  • npm ci 替换 npm install

npm ci 会严格安装 lock 文件安装依赖,忽略对 package.json 中版本范围的解析等过程,且安装依赖前会删除现有的 node_modules 目录

总的来说,npm ci 更适合 CICD 的流程中,更严格、更、更干净地安装依赖

数据库

数据库使用官方的 mysql:latest 镜像,使用 docker run 命令直接启动也可以

docker run -d \
--name account-app-mysql \
--network network1 \
-e MYSQL_ROOT_PASSWORD=xxx \
-e MYSQL_DATABASE=account_app_db \
-e MYSQL_USER=xxx \
-e MYSQL_PASSWORD=xxx \
-p 3307:3306 \
-v mysql-data:/var/lib/mysql \
mysql:latest

但是后面会集成到 docker compose 中

docker-compose

源码:https://github.com/GSemir0418/account-app-deploy

Docker compose 的配置实际上就是整合多个容器服务的 docker run 命令

  • network

注意配置网络环境,保证三个服务在同一网络环境下

services:
frontend:
image: gsemir/account-app-frontend:0.1.3
networks:
- account-app-network

backend:
image: gsemir/account-app-backend:0.1.0
networks:
- account-app-network

db:
image: mysql:latest
networks:
- account-app-network

networks:
account-app-network:
driver: bridge
  • 端口

注意端口映射,ports: 70:80 表示宿主环境的(外部)的 70 端口对应容器内部环境的 80 端口,即服务启动(暴露)的端口

  • 环境变量与持久化

会自动读取根目录中的 .env 文件中定义的数据库连接所需的环境变量

services:
db:
image: mysql:latest
container_name: account-app-mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- mysql-data:/var/lib/mysql

volumes:
mysql-data: