传统老手艺之容器编排

一、背景

前阵子参加了一个 AI 创意比赛,项目是一个在线考试系统,有管理后台和考生答题端两个子系统。作品的评审环节需要提供一个可演示的测试环境。

问题来了,这个项目跑在生产环境上,不能随便动。而且之前的部署方式相当传统------站点通过Kestrel部署、还要装 SQL Server、配 Redis、搭 RabbitMQ....全套下来没个把小时搞不定。咱也不能为了演示个 Demo 把全套基础设施都从头搞一遍吧。

到这我很快就想到了docker,这简直就是容器大展身手的完美场景。预期的结果就是,编排好 docker 镜像后,拿到测试环境 docker-compose up -d 一键启动,其余什么都不用


二、基础设施

可能有人会问,像关系数据库这种基础设施,放容器里不怕丢数据吗?是的,但注意,这是 Demo 环境,不是生产。目标就是让宿主机除了 docker 之外什么都不用装。数据库备份文件(.bak)挂进去,启动后 restore 一下,数据就回来了。

dockerfile 用了多阶段构建:

plain 复制代码
SDK 8.0 编译阶段 → 发布 → ASP.NET 8.0 运行时阶段

管理端的 Dockerfile 还多装了一个 mssql-tools18,方便在容器里直接执行 sqlcmd 恢复数据库。

事实上,生成 Dockerfile 的工作,IDE基本可以自动完成90%,剩下的就是我们自己根据情况调整,比如上面提到的需要在管理端做一次备份的操作,是需要手动在 Dockerfile 里编辑的。而且现在 AI 这么普及,不会的命令查一下就有了。注意用 AI 得到答案后,还是要自己消化的。

以下是管理端的Dockerfile基本结构。

dockerfile 复制代码
# 管理端镜像文件
# ==========================================

# Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Copy项目文件,
# Copy项目文件,就是那些csproj中包含的内容,编译时用到
# 内容省略

# 重新生成
RUN dotnet restore "xxx.csproj"

# 拷贝所有代码
COPY . .

# 发布
WORKDIR "/src/xxxx"
RUN dotnet publish -c Release -o /app/publish --no-restore

# 准备运行时
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app

# 安装MSSQL,这一步要是手动添加
RUN apt-get update && \
    apt-get install -y curl apt-transport-https gnupg && \
    curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg && \
    echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/debian/12/prod bookworm main" > /etc/apt/sources.list.d/mssql-release.list && \
    apt-get update && \
    ACCEPT_EULA=Y apt-get install -y mssql-tools18 && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

ENV PATH="$PATH:/opt/mssql-tools18/bin"

# 拷贝发布好的文件
COPY --from=build /app/publish .

# 创建上传目录并授权,这个也是手动创建,和业务逻辑有关,无典型性
RUN mkdir -p /app/wwwroot/upfile && \
    chmod 777 /app/wwwroot/upfile

# 指定端口
EXPOSE 8080

# 启动
ENTRYPOINT ["dotnet", "xxx.dll"]

端口规划:

服务 容器端口 映射到宿主机
管理后台 8080 5000
考生端 8080 5256
SQL Server 1433 1433
Redis 6379 6379
RabbitMQ 5672 / 15672 5672 / 15672

docker-compose.yml 写起来倒是不复杂,5 个 service,各自配好 image、ports、volumes、depends_on,再加一个 bridge 网络打通通信。RabbitMQ 带管理界面,15672 端口也映射出来方便排查。

yaml 复制代码
services:
  # ==================== 基础设施 ====================
  # SQL Server 2022 Express (免费版本)
  mssql:
    image: ccr.ccs.tencentyun.com/{个人命名空间}/examimage:mssql-2022
    container_name: exam-mssql
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=${MSSQL_SA_PASSWORD}
      - MSSQL_PID=Express
    ports:
      - "1433:1433"
    volumes:
      - mssql-data:/var/opt/mssql
      # 挂载备份文件目录
      - ./backups:/var/opt/mssql/backup
    networks:
      - exam-network
    restart: unless-stopped
    healthcheck:
      test: /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "${MSSQL_SA_PASSWORD}" -Q "SELECT 1" -C || exit 1
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s

  # Redis 7 (单节点)
  redis:
    image: ccr.ccs.tencentyun.com/{个人命名空间}/examimage:redis-7
    container_name: exam-redis
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    networks:
      - exam-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  # RabbitMQ 3 (带管理界面)
  rabbitmq:
    image: ccr.ccs.tencentyun.com/{个人命名空间}/examimage:rabbitmq-3
    container_name: exam-rabbitmq
    environment:
      - RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER}
      - RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS}
      - RABBITMQ_DEFAULT_VHOST=${RABBITMQ_DEFAULT_VHOST}
    ports:
      - "5672:5672"
      - "15672:15672"
    volumes:
      - rabbitmq-data:/var/lib/rabbitmq
    networks:
      - exam-network
    restart: unless-stopped
    healthcheck:
      test: rabbitmq-diagnostics -q ping
      interval: 30s
      timeout: 10s
      retries: 5

  # ==================== 应用服务 ====================
  # 管理后台
  exam-manage:
    image: ccr.ccs.tencentyun.com/{个人命名空间}/examimage:exam-manage-latest
    container_name: exam-manage
    ports:
      - "${EXAM_MANAGE_PORT}:8080"
    environment:
      - ASPNETCORE_ENVIRONMENT=Docker
      - ASPNETCORE_URLS=http://+:8080
      # 生产环境主机地址配置(覆盖 appsettings.Docker.json)
      - baseHost=${EXAM_MANAGE_HOST:-http://{公网ip+端口或者域名}}
      - ExamClientHost=${EXAM_CLIENT_HOST:-http://{公网ip+端口或者域名}}
      - corsHosts=${EXAM_CORS_HOSTS:-http://{公网ip+端口或者域名},http://{公网ip+端口或者域名}}
      # 资源文件访问地址(覆盖 resoucesetting.json)
      - resoucesetting__resourceHost=${EXAM_MANAGE_HOST:-http://{公网ip+端口或者域名}}
    volumes:
      - exam-uploads:/app/wwwroot/upfile
      - exam-visualizations:/app/App_Data/visualizations
    depends_on:
      mssql:
        condition: service_healthy
      redis:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
    networks:
      - exam-network
    restart: unless-stopped

  # 考生答题端
  exam-client:
    image: ccr.ccs.tencentyun.com/{个人命名空间}/examimage:exam-client-latest
    container_name: exam-client
    ports:
      - "${EXAM_CLIENT_PORT}:8080"
    environment:
      - ASPNETCORE_ENVIRONMENT=Docker
      - ASPNETCORE_URLS=http://+:8080
      # 生产环境主机地址配置(覆盖 appsettings.Docker.json)
      - baseHost=${EXAM_CLIENT_HOST:-http://{公网ip+端口或者域名}}
      - corsHosts=${EXAM_CORS_HOSTS:-http://{公网ip+端口或者域名},http://{公网ip+端口或者域名}}
      - payNotify=${EXAM_CLIENT_HOST:-http://{公网ip+端口或者域名}}/order/alipaynotify/pagepay
      - payReturnUrl=${EXAM_CLIENT_HOST:-http://{公网ip+端口或者域名}}/order/alipaynotify/pagepay
      # 资源文件访问地址(覆盖 resoucesetting.json)
      - resoucesetting__resourceHost=${EXAM_CLIENT_HOST:-http://{公网ip+端口或者域名}}
    volumes:
      - exam-uploads:/app/wwwroot/upfile
      - exam-visualizations:/app/App_Data/visualizations
    depends_on:
      mssql:
        condition: service_healthy
      redis:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
    networks:
      - exam-network
    restart: unless-stopped

# ==================== 网络和卷 ====================

networks:
  exam-network:
    driver: bridge

volumes:
  mssql-data:
  redis-data:
  rabbitmq-data:
  exam-uploads:
  exam-visualizations:

三、配置分离

第一个关键决策来了,我新增了一个docker环境,前提是本地的开发环境不能受影响。

docker 环境里,数据库连接字符串是 server=mssql(容器名),Redis 是 server=redis,RabbitMQ 是 server=rabbitmq。而本地开发环境用的是 localhost\SQLEXPRESS127.0.0.1:6379

不能改本地配置,否则同事拉代码跑不起来,影响日常开发。

ASP.NET Core 的配置系统天然支持这个场景:WebApplication.CreateBuilder 会自动加载 appsettings.{ASPNETCORE_ENVIRONMENT}.json。只要在 docker-compose 里设置环境变量 ASPNETCORE_ENVIRONMENT=Docker,框架就会自动加载 appsettings.Docker.json,覆盖 appsettings.json 中的配置。

所以做法很简单:

  1. 项目根目录放 appsettings.Docker.json,里面写 docker 专用的连接字符串和其他配置
  2. docker-compose.yml 里设置 ASPNETCORE_ENVIRONMENT=Docker
  3. 本地 dotnet run 不受影响,因为本地没有这个环境变量

配套的还有 cachesetting.Docker.json(Redis 配置)、mqsetting.Docker.json(RabbitMQ 配置),以及 .env 文件统一管理密码和端口。

配置文件的内容就不贴了,实际项目中应该有对应环境的独有配置文件即可,如下图

.env 文件是专门为 docker 环境生成的存储关键密码的文件,这个在生产环境里,我们习惯性的是放到环境变量里,但在 docker 环境,为了方便,我是创建了一个配置文件,这里也不贴了。

最后本地跑一下,看看效果

访问容器地址正常👇


四、发布部署

镜像在本地跑通了,接下来要部署到云服务器上给评审演示,这里最直接的方式就是推到 dockerhub,然后到目标服务器去拉取,但众所周知的网络环境,彼时我在本地往上推是没问题的,但在测试服务器往下拉就麻烦一点,所以放弃了 dockerhub 的方案,现在基本就剩以下2个方向。

4.1 方案一:tar 导出,手动传输

docker save命令把镜像导出成 tar 文件,再传到服务器,docker load 导入。

这个方案,最大的问题就是,中途我要修改代码重新打包的时候,每次都要重新导出,上传,在导入,虽然好用但过于原始,也过于消耗体力,所以放弃。

4.2 方案二:腾讯云 TCR 镜像仓库

既然 dockerhub 访问不了,刚好我有个个人的腾讯云账号,而且贴心的提供了一定额度的免费服务(TCR)

所以,现在思路就变成了:

本地构建 → 推送到 TCR → 服务器直接拉取

完整的流程如下。

构建镜像

bash 复制代码
docker-compose build

这一步可能会花点时间,具体看你的服务大小。

打标签

bash 复制代码
docker tag {镜像服务1} ccr.ccs.tencentyun.com/{TCR个人命名空间}/{容器名1}:{标签名1}
docker tag {镜像服务2} ccr.ccs.tencentyun.com/{TCR个人命名空间}/{容器名2}:{标签名2}
...

推送

bash 复制代码
docker push ccr.ccs.tencentyun.com/{TCR个人命名空间}/examimage:exam-manage-v20260714
docker push ccr.ccs.tencentyun.com/{TCR个人命名空间}/examimage:exam-client-v20260714

注意,为了方便部署,可以滚动一个latest标签

bash 复制代码
docker tag magicguangdongexam-exam-manage ccr.ccs.tencentyun.com/{TCR个人命名空间}/examimage:exam-manage-latest
docker push ccr.ccs.tencentyun.com/{TCR个人命名空间}/examimage:exam-manage-latest

标签规范也很简单:版本标签按日期 (如v20260714,可追溯),滚动标签用 latest(方便部署)。推送新版本的同时更新 latest。

4.3 基础镜像也要推

这里有个细节:连 mcr.microsoft.com/mssql/server:2022-latestredis:7-alpinerabbitmq:3-management 这些基础镜像,我也一并拉到本地、打标签、推到了 TCR,所有镜像都从同一个仓库拉取,全程流畅。

这些我之前已经推过了,如下

4.4 最终部署体验

最终TCR上会展示如下几个镜像

然后我们登录云服务,拉取这几个镜像,完成启动即可

启动完成后,再通过公网IP+端口,来访问一下

至此,服务容器化构建,推送,部署整个流程基本结束,之后每次更新代码,只需要本地重新 build、push,服务器上 pull,再重启应用容器即可。

注意

上面的截图演示,是因为我之前已经部署过,所以有一个关键步骤没有截图,就是第一次再云端拉去镜像后,需要先启动基础设施,恢复数据库,这是一次性操作,后续部署成功后,无需再反复执行。

bash 复制代码
# 启动基础设施
docker compose -f docker-compose.prod.yml up -d mssql redis rabbitmq
# 等待 SQL Server 就绪(查看日志,等待 "Recovery is complete")
docker compose -f docker-compose.prod.yml logs -f mssql

# 查看备份文件的逻辑文件名
docker exec -it exam-mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "<YOUR_SA_PASSWORD>" -Q "RESTORE FILELISTONLY FROM DISK='/var/opt/mssql/backup/Magic.Guangdong.Exam.bak'" -C

# 恢复数据库
docker exec -it exam-mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "<YOUR_SA_PASSWORD>" -Q "
RESTORE DATABASE [Magic.Guangdong.Exam]
FROM DISK='/var/opt/mssql/backup/Magic.Guangdong.Exam.bak'
WITH MOVE 'Magic.Guangdong.Exam' TO '/var/opt/mssql/data/Magic.Guangdong.Exam.mdf',
     MOVE 'Magic.Guangdong.Exam_log' TO '/var/opt/mssql/data/Magic.Guangdong.Exam_log.ldf'
" -C

# 创建消息队列数据库(这个是因为我使用了CAP,需要对队列消息做持久化存储,所以需要一对这样的数据表来存储发布订阅的消息记录)
docker exec -it exam-mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "<YOUR_SA_PASSWORD>" -Q "CREATE DATABASE [Magic.Guangdong.Exam.MsgQuene]" -C

五、几个小但致命的坑

5.1 静态文件报错

我在部署到容器环境后,发现页面显示不正常,打开控制台一看一堆 error 输出,查了半天才发现,是文件大小写的问题。以前常在 Windows 环境开发,这个问题确实没有引起重视,这点大家还是要引以为戒,大小写敏感的问题要随时注意,Linux 文件系统对大小写是敏感的,而Windows不是。

5.2 文件共享问题

管理后台上传的图片(比如考试封面、用户头像),考生端页面上显示不出来。

这是因为,两个容器各自有独立的文件系统。管理端把文件写到了自己的容器里,考生端自然读不到需要使用Docker Volume 配置共享。

plain 复制代码
volumes:
  exam-uploads:          # 共享上传文件
  exam-visualizations:   # 共享动画 JS 文件

两个容器都挂载同一个 volume,文件写进去双方都能读。同时 Dockerfile 里提前 mkdir -p 创建好目录,避免目录不存在导致启动失败。


六、龙虾乱入*

这里插一个章节。

其实到了云端部署环节,如果你的服务器是你自己的,也确定这是一个测试环境,没有任何数据安全问题。且你刚好有一只自己的 OpenClaw 助手,那事情就会变得简单且有趣起来。

再去服务器上检查一下,果然都关掉了。

同理,如果你需要让他帮你启动容器,再直接对话就行,非常方便。

再次提醒:使用 openclaw 助手之前,一定要确保这是你自己的服务器,或者你有绝对的管理权限,且确认没有安全风险的前提下。

七、一点感悟

好了,基本就是这些,前阵子的比赛初选已经结束了,很遗憾我没有进入复赛,但在将近3w名参赛选手中排到了200名左右,也非常欣慰了。上面这一套基于容器部署演示环境的流程,不是本次的比赛内容,但把一套繁琐的依赖基础设施通过 Docker 完成容器化编排、做到秒级拉起 Demo,本身就是一次非常过瘾的技术沉淀。顺手的工具链,才是开发者最坚实的后盾,我觉得这也挺值的记录一下,因此有了此篇。


相关资源:

相关推荐
Slice_cy1 小时前
Mint 自研框架设计与实现:从重复开发走向配置驱动(三)
前端·后端·架构
Conan在掘金1 小时前
ArkTS 进阶之道(1):为哈禁 any/unknown?从「编译期就拦」理解鸿蒙严格类型哲学
后端
Patrick_Wilson2 小时前
为什么让程序主动崩溃反而更可靠?聊聊 supervisor 与 let it crash
后端·erlang·elixir
张忠琳2 小时前
【NVIDIA】NVIDIA k8s-device-plugin v0.19.3 配置API模块深度分析之二
云原生·容器·架构·kubernetes·nvidia
摇曳的精灵2 小时前
dify的docker部署
运维·docker·容器·dify
软件资深者3 小时前
QQ音乐PC版 使用教程:去广告绿色便携版音乐播放器,本地曲库管理与在线试听一键搞定,音乐播放器新手 5 分钟上手(2026)
测试工具·容器·娱乐·软件需求
武子康3 小时前
权重已到、Recipe 还在变:Kimi K3 发布后该怎样做工程验收
人工智能·后端·agent
站大爷IP3 小时前
列表推导式一用就爽?大数据量下它把我服务器内存榨干了,原来生成器才是yyds
后端