跳到主要内容

2 篇博文 含有标签「docker」

查看所有标签

· 阅读需 5 分钟

项目采用 docker compose 来管理前后端镜像,以及承担部署的工作

自 2024-06-06 开始,国内的 Docker Hub 镜像加速器相继停止服务,导致服务器从 docker hub 拉取 image 一直失败,因此选择在本地搭建 harbor 服务来管理项目前后端镜像

没有空闲的服务器,手头只有一个 windows,只能在 wsl 中安装 harbor 服务

1 安装 wsl ubuntu

  • 方式一 命令安装(失败)
wsl --list --online
wsl --install <name>
  • 方式二 Microsoft Store 安装

已经在 Windows 上安装了 Docker Desktop,并且配置了与 WSL 2 的集成,那么就无需在 WSL 中再次安装 Docker 及其相关组件

检查 Docker Desktop 与 WSL 2 集成设置

  1. 打开 Docker Desktop 设置。
  2. 查看 General 选项卡,确保 Use the WSL 2 based engine 选项被勾选。
  3. 切换到 Resources > WSL Integration 选项卡,确保你想要使用 Docker 的 Linux 发行版(比如 Ubuntu-22.04)已经开启集成。

2 安装 harbor

  • 下载 harbor 安装包

  • 将安装包保存至 wsl

方法 1:使用 Windows 文件资源管理器

  1. 在 Windows 文件资源管理器的地址栏中,输入 \\wsl$ 后按回车键。这会显示所有已安装的 WSL 分发版本

  2. 找到并打开你想要复制文件到的目标 Linux 分发版,比如 \\wsl$\Ubuntu-22.04

  3. 现在,你可以直接拖拽文件从 Windows 文件资源管理器到这个目录中,或者使用复制粘贴的方式。这样,文件就被复制到了 WSL 分发版内

方法 2:使用命令行

如果你更喜欢使用命令行,可以使用 cp 命令从 Windows 的路径复制文件到 WSL 中,或者反过来。首先,你需要了解如何在 WSL 中访问 Windows 的文件系统。

在 WSL 中,Windows 的驱动器被挂载在 /mnt/ 下。例如,C 盘位于 /mnt/c/,D 盘位于 /mnt/d/,以此类推

cp /mnt/c/Users/<你的用户名>/Downloads/harbor-online-installer-v2.5.0.tgz ~/
  • 解压
tar xzvf harbor-offline-installer-v2.11.0.tgz
  • 修改 harbor 配置文件
# windows 内网 ip
# hostname: 192.168.31.117
hostname: 公网ip
http:
port: 70
harbor_admin_password: admin
database:
password: gsqzs123
data_volume: /home/gsemir/harbor-data
# 注释掉 https 相关配置
  • 改名
mv harbor.yml.tmpl harbor.yml
  • 执行安装脚本
sudo ./install.sh

windows 浏览器访问 192.168.31.117:70 或者 localhost:70 打开 harbor 管理页面,默认用户名 admin,密码 admin,

比较麻烦的一点是,当 ip 改变后,需要修改 harbor.yml 中的 ip 地址,然后重新执行安装脚本,重新安装

3 push 本地镜像到 harbor

Docker 默认情况下期望 Registry 使用 HTTPS。如果你的 Registry 使用的是 HTTP,你需要告诉 Docker 客户端信任这个 HTTP 地址。这可以通过配置 Docker 客户端来实现。

  1. 打开 Docker Desktop 设置。
  2. 寻找到 Docker 引擎的配置项(通常在 "Docker Engine" 或 "Daemon" 标签下)。
  3. 在 JSON 配置中,添加你的 Harbor 域名到 insecure-registries 数组
{
"insecure-registries" : ["192.168.31.117:70"]
}
  • Windows docker 登录
docker login 192.168.31.117:70
  • 修改部署脚本,支持多目标推送
  • 同时需要修改 buildx 的配置,使其信任 http 的镜像源地址
#!/bin/bash
# push.sh

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

TARGET=$2
if [ "$TARGET" = "dh" ]; then
TAG=gsemir/account-app-frontend:$VERSION
elif [ "$TARGET" = "h" ]; then
TAG=192.168.31.117:70/account-app/account-app-frontend:$VERSION
else
echo "未知目标。请指定 'dockerhub' 或 'harbor'。"
exit 1
fi

docker buildx rm mybuilder
docker buildx create --name mybuilder --config ./buildkitd.toml --use
docker buildx build --platform linux/amd64,linux/arm64 -t $TAG . --push

buildkitd.toml

[registry."192.168.31.117:70"]
http = true
insecure = true
  • 执行推送脚本
sh ./push.sh 0.2.3 h

4 服务端拉取镜像

服务器也需要配置 insecure-registries 为 windows 公网 ip,即 harbor.yml 中配置的 hostname 地址

sudo vi /etc/docker/daemon.json
# 添加后重启
sudo systemctl restart docker

然后修改 docker-compose 文件的 image 配置

services:
frontend:
- image: gsemir/account-app-frontend:0.2.2
+ image: ip:70/account-app/account-app-frontend:0.2.3

执行 docker compose up -d,完成镜像拉取

· 阅读需 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: