做系统集成的朋友应该都有这种体会:本地跑得好好的,一到客户现场就各种水土不服。我把整个交付包改造成了Docker一键部署后,这个问题基本绝迹了。之前接手一个系统集成项目,需要把用户端Web、管理后台、API服务、定时任务和文件存储完整部署到客户机房,用传统方式折腾了快一周才稳定。改成Docker之后,从解压到服务全部起来只需要十几分钟,客户自己也能操作。这篇文章就把这套方案从头拆一遍,包括Dockerfile编写、docker-compose编排、部署脚本设计和源码目录结构,希望能给正在做系统集成的朋友一份可以直接抄作业的参考。
1. 整体设计:为什么非要用Docker做系统集成交付
1.1 传统部署方式到底卡在哪
回想最早做项目交付,通常是一份部署文档加一个tar包。文档里写着:需要JDK 1.8、MySQL 5.7、Nginx 1.18、Redis 6.0,还要手动改三个配置文件。听起来不难,但客户环境千奇百怪,有装过旧版JDK的、有MySQL端口被占的、有服务器时间不准导致登录态过期的,甚至有的机器连openssl版本都不对。每次排错都在本机和客户机器之间来回对比“环境差异”,效率极低。有一次我花了整整两个下午,最后发现是客户机器的/etc/hosts里没有映射主机名,导致服务间调用失败。
这类问题本质上是“部署环境不可控”。传统软件包假设了固定的运行环境,但又没法把整个环境一起带走。Docker解决的就是这个问题:把应用和它的运行环境(操作系统库、JDK、Python、依赖包、配置模板)一起打包成镜像,部署的时候只需要一个能跑Docker的Linux环境,镜像一加载,所有“环境差异”就消失了。对于系统集成项目来说,交付物从“文档+压缩包”变成“镜像+编排文件+部署脚本”,整个流程的确定性和可复现性提高了不止一个量级。
| 对比维度 | 传统方式 | Docker方式 |
|---|---|---|
| 环境一致性 | 完全依赖手工配置 | 镜像内置,开箱即跑 |
| 交付物 | 文档 + 压缩包 | 镜像 + 编排文件 + 脚本 |
| 故障恢复 | 靠人肉回滚 | 脚本自动备份回滚 |
| 扩容调整 | 很少做,怕搞坏环境 | 改一下compose副本数即可 |
| 新人上手成本 | 需要理解整套中间件安装 | 会几条docker命令就能启动 |
1.2 这一版系统集成的整体架构设计
我做的这个项目是典型的“多服务集成”类型。前端是两个Web应用(用户端和管理后台),后端是一个Spring Boot API服务,另外还有定时任务模块、文件存储模块,数据库用了MySQL 8.0,缓存用的Redis,文件存储直接挂宿主目录。整个依赖关系大概是:Web -> API -> MySQL/Redis,定时任务依赖API里的业务逻辑,文件存储模块独立读写数据卷。
直接把这么多服务丢到一个Docker容器里是最省事的做法,团队里也有人建议“一个容器装所有”。但我坚持拆成多个容器,用docker-compose统一编排。原因有三个:第一,每个服务独立扩容,比如API服务压力大可以单独启动多个副本;第二,故障隔离,定时任务挂了不会影响Web入口;第三,日志和配置可以按服务分别管理,排错的时候一眼看到是哪个容器在报错。拆开之后的代价是需要额外处理服务间网络,docker-compose天然创建了内部网络,服务名就是域名,这部分复杂度被语法承担了,完全可以接受。
1.3 交付物包含哪些内容
这套方案的最终交付物是一个文件夹,结构大致如下:
system-integration/ ├── docker-compose.yml # 服务编排文件 ├── .env # 环境变量模板 ├── deploy.sh # 一键部署/更新/回滚脚本 ├── backup/ # 部署前的自动备份目录 ├── services/ │ ├── api/ │ │ ├── Dockerfile │ │ ├── app.jar │ │ └── config/ │ ├── web/ # 前端Nginx镜像 │ │ ├── Dockerfile │ │ └── dist/ │ ├── admin/ │ │ └── ... │ ├── mysql/ │ │ └── init.sql │ └── redis/ │ └── redis.conf └── logs/ # 宿主机日志目录这个结构对客户来说非常友好:不懂Docker也能按文档操作,因为真正动手的只有deploy.sh一个脚本。源码不是跟着tar包走的,而是放到内部GitLab仓库,交付时给客户提供源码仓库地址和构建说明书,镜像可以由我们在CI流水线中构建好,也可以由客户在自己内网执行build-all.sh。两种方式在部署脚本里都做了支持。
2. 从Dockerfile到编排文件:核心细节拆解
2.1 后端API的Dockerfile怎么写才算过关
后端服务是Spring Boot应用,传统方式是java -jar。写Dockerfile的时候,我见过很多同事直接用maven镜像去构建,然后把整个构建产物目录COPY进去,这样会导致镜像体积膨胀到几个GB。我这里用了多阶段构建,第一阶段用maven容器编译代码,第二阶段只拷贝最终的jar包。多阶段的好处是构建环境不会残留在最终镜像里,交付的镜像只包含运行时需要的JRE和jar包。
基础镜像我选用的是eclipse-temurin:8-jre,而不是openjdk:8,因为Temurin在时区处理和安全更新上维护更积极。生产环境必须处理时区问题,如果容器默认是UTC时间,业务报表和对账会差8小时,非常坑。所以我把时区配置直接做进镜像里:
FROM eclipse-temurin:8-jre LABEL maintainer="your-team@example.com" RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && echo "Asia/Shanghai" > /etc/timezone WORKDIR /app COPY --from=builder /build/target/system-api.jar app.jar RUN useradd -r -u 1001 appuser USER appuser EXPOSE 8080 ENTRYPOINT ["java","-XX:+UseContainerSupport","-XX:MaxRAMPercentage=75.0","-jar","app.jar"]有几个细节值得解释。第一,为什么要创建appuser而不是用root启动。容器里用root启动应用,一旦应用被攻击,攻击者就有容器内最高权限,再加上docker默认的root映射,风险会进一步放大。用非root用户启动是安全基线要求。第二,JVM参数里的UseContainerSupport和MaxRAMPercentage是给JVM“认容器内存”用的,否则JVM会取宿主机的总内存来算堆大小,容器限制2GB,JVM却按16GB机器去设置,很容易被系统杀掉。第三,ENTRYPOINT里没有写--spring.profiles.active,留给环境变量在compose文件里指定,保持镜像的可复用性。
2.2 前端和文件存储镜像的处理方式
前端是打包好的静态文件,我直接用Nginx镜像。这里也有一个常见的坑:不要用最新版nginx,必须锁定大版本,比如nginx:1.22-alpine。原因是Nginx配置语法和模块在不同版本间有细微差异,一旦镜像在服务器上被更新成不兼容的版本,几百个客户站点会同时出问题。锁版本之后,每次启动都拉同一个镜像,行为可预期。
Dockerfile很简单:
FROM nginx:1.22-alpine COPY dist/ /usr/share/nginx/html/ COPY nginx.conf /etc/nginx/conf.d/default.confnginx.conf里我做了gzip压缩、前端路由history模式的try_files回退,以及静态资源缓存。特别要注意的是,Nginx容器默认会把access log打到底层stdout,方便docker logs查看,不要再单独写文件,否则日志会无限膨胀占满磁盘。文件存储模块不需要Web服务,直接挂宿主机目录,在compose里用volume声明即可,不需要单独写Dockerfile。
因为服务之间通过内部网络通信,Nginx配置里上行的API地址不要写localhost,要写compose服务名,比如proxy_pass http://api:8080。这也是新手最容易困惑的地方:在容器里,localhost指向的是当前容器自己,不是宿主机,“localhost:8080”根本连不到API服务。这个内部DNS解析能力是docker-compose创建的network自带的,也是服务编排的价值所在。
2.3 docker-compose.yml编排服务的顺序与陷阱
这是整个交付包里最核心的文件。我把详细版本贴出来:
version: "3.8" networks: app-net: driver: bridge ipam: config: - subnet: 172.28.0.0/16 volumes: mysql-data: redis-data: services: api: build: ./services/api image: registry.internal.local/system-api:${TAG:-latest} restart: unless-stopped environment: SPRING_PROFILES_ACTIVE: ${SPRING_PROFILE:-prod} DB_HOST: mysql DB_PORT: 3306 DB_NAME: system_db DB_USER: ${DB_USER} DB_PASSWORD: ${DB_PASSWORD} REDIS_HOST: redis depends_on: mysql: condition: service_healthy redis: condition: service_healthy networks: - app-net ports: - "8080:8080" logging: options: max-size: "50m" max-file: "5" web: build: ./services/web image: registry.internal.local/system-web:${TAG:-latest} restart: unless-stopped depends_on: - api networks: - app-net ports: - "80:80" admin: build: ./services/admin image: registry.internal.local/system-admin:${TAG:-latest} restart: unless-stopped depends_on: - api networks: - app-net ports: - "8081:80" mysql: image: mysql:8.0 restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} MYSQL_DATABASE: system_db MYSQL_USER: ${DB_USER} MYSQL_PASSWORD: ${DB_PASSWORD} TZ: Asia/Shanghai command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --lower_case_table_names=1 volumes: - mysql-data:/var/lib/mysql - ./services/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"] interval: 10s timeout: 5s retries: 5 networks: - app-net redis: image: redis:6.2 restart: unless-stopped command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"] volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] interval: 10s timeout: 5s retries: 5 networks: - app-net这里有几个关键设计。第一,mysql和redis加了healthcheck,api服务的depends_on条件是service_healthy,这样可以确保数据库先准备好再启动API,而不是用sleep 30来碰运气。第二,数据库密码和连接串通过环境变量注入,不写死在compose文件里,实际使用的时候放在.env文件,这个文件在交付包中只给模板,具体值由部署人员填写。第三,restart: unless-stopped保证服务器重启后容器自动拉起,对于系统集成交付来说,客户不可能半夜去手动启动服务,这个配置能省很多售后电话。
很多人会问为什么mysql的端口没有映射到宿主机。这是个故意的选择:数据库只给内部网络访问,不需要暴露到外部,API和定时任务都通过app-net连接。如果要从运维端管理,可以临时用docker exec进入容器,或者单独开一个跳板机出口。端口暴露越少,攻击面越小,这个思路在面向客户的交付中尤其重要。
3. 一键部署脚本:把复杂操作封装成黑盒
3.1 脚本要覆盖哪些场景
一键部署的核心不是“执行一条命令”,而是把部署过程中可能遇到的环境差异、异常状态、误操作风险全部隔离在脚本内部。我写的deploy.sh主要覆盖四类场景:首次部署、更新版本、回滚版本、状态查看。首次部署要做环境检查、镜像准备、配置检查、启动服务;更新版本要备份旧数据卷或数据库文件、拉取新镜像、优雅停止再启动;回滚要恢复到上一个可用版本;状态查看则是一个简单的docker compose ps。
脚本的语言用Bash,兼容性最好。不要依赖bash的某个高级特性,我尽量用POSIX语法。整个脚本入口的用法是:
./deploy.sh install # 首次安装 ./deploy.sh update # 更新到新版本 ./deploy.sh rollback # 回滚到上一个版本 ./deploy.sh status # 查看所有服务状态3.2 环境检查:把“客户机器能不能装”前置
脚本第一步不是急着拉镜像,而是检查宿主机的Docker环境。很多客户机器上要么没装Docker,要么装了老版本,甚至会出现Docker Desktop的虚拟化没开启导致Docker根本起不来的情况。我在脚本里封装了一个check_env函数,依次检查:操作系统发行版和版本、是否已安装docker、docker服务是否可运行、docker compose子命令是否可用(这里兼容老旧的docker-compose独立命令)、当前用户是否在docker组里。每项检查都会打印明确的提示,不满足条件时直接退出,并告诉用户怎么处理。
Docker的安装函数我也内置了,但只在检测到Docker缺失时才执行。支持Ubuntu、Debian、CentOS三套命令,安装源用的是系统自带源或镜像源,整个安装过程在脚本里是不交互的,用户不需要按任何确认键。这个安装函数主要是给客户现场救急用的,正常情况下建议运维同学在初始化机器时就把Docker装好。
check_env() { if ! command -v docker >/dev/null 2>&1; then echo "[ERROR] 未检测到Docker,将尝试自动安装..." install_docker || exit 1 fi if ! docker info >/dev/null 2>&1; then echo "[ERROR] Docker服务未运行,请检查虚拟化是否开启" exit 1 fi if ! docker compose version >/dev/null 2>&1 && ! command -v docker-compose >/dev/null 2>&1; then echo "[ERROR] 未找到docker compose命令" exit 1 fi }3.3 镜像准备与构建流程
脚本支持两种镜像来源:从私有镜像仓库拉取,或者在客户机器上本地构建。默认情况下,如果设置了REGISTRY_ADDRESS环境变量,就执行docker compose pull拉取镜像;如果没有,就执行docker compose build本地构建。本地构建有一个好处:不依赖外部网络,内网交付时非常实用;坏处是客户机器需要有Maven或Node的构建环境,构建时间也会比较长。我在脚本里做了一个优化:如果服务目录下没有app.jar和dist目录,脚本会自动提示先执行build-all.sh进行源码编译,避免直接对着空目录构建出错误镜像。
prepare_images() { if [ -n "$REGISTRY_ADDRESS" ]; then echo "[INFO] 从镜像仓库拉取镜像..." docker compose pull else echo "[INFO] 本地构建镜像..." docker compose build --no-cache 2>/dev/null || docker compose build fi }这里有个细节:本地构建我默认加了--no-cache,生产交付时建议每次构建都要保证产物是最新的,不要依赖缓存。但如果客户机器性能太差,--no-cache会非常慢,所以我加了fallback逻辑,如果带--no-cache失败,就退回去不带参数重新构建。这个兜底是踩过一次坑才加的:有一次构建工具版本升级,缓存里的旧层直接导致镜像里的配置文件是老的,服务起来之后登录接口全部报错。
3.4 数据库初始化与版本数据备份
数据库初始化和数据备份是系统集成交付里最容易翻车的一环。我在compose文件里已经把init.sql挂到mysql容器的docker-entrypoint-initdb.d目录,mysql初始化数据卷时就会自动执行建表脚本。但问题是:如果服务已经跑过一次,mysql数据卷里已经存在初始化数据,再挂init.sql是不生效的。所以deploy.sh update的时候不能重启后用初始化脚本覆盖,必须先做数据备份,再启动新版本。
备份函数我写成这样:
backup_data() { BACKUP_DIR="backup/$(date +%Y%m%d%H%M%S)" mkdir -p "$BACKUP_DIR" docker compose exec -T mysql mysqldump -uroot -p"${DB_ROOT_PASSWORD}" --all-databases > "$BACKUP_DIR/mysql.sql" 2>/dev/null tar czf "$BACKUP_DIR/upload.tar.gz" -C ./storage . 2>/dev/null || true echo "[INFO] 数据备份完成:$BACKUP_DIR" }注意mysqldump是在容器内执行,输出重定向到宿主机,所以不能用-t,要用-T选项禁止分配伪终端。备份完成后还会把存储目录里的上传文件也一起打包,这对带文件管理的系统集成项目来说特别重要。回滚的时候,除了把镜像TAG切回上一个版本,还要用备份出的SQL恢复数据。我遇到过客户误删数据的场景,回滚只做到了代码层面,数据却没有还原,最后还是凌晨手动从备份文件里捞数据,非常痛苦。从那以后,update之前强制备份就成了脚本里不可跳过的步骤。
3.5 启动、健康检查与优雅退出
启动服务之后不能立刻说“部署完成”,还要等所有容器都进入healthy状态。我的脚本里有一个wait_healthy函数,循环检查docker compose ps输出的健康状态,最多等待120秒。如果超过时间还没healthy,就自动打印相关容器的docker logs尾部日志,方便当场定位问题,而不是让用户把错误信息发回来再猜。
优雅退出也很重要。docker compose stop会先向容器内PID 1发送SIGTERM,让应用有机会做资源清理和事务回滚,而不是直接kill。Spring Boot默认收到SIGTERM后会优雅停机,Nginx容器收到SIGTERM也会正常退出。脚本里我特意没有用docker compose down,因为down会把网络也删掉,如果只是更新版本,保留网络能缩短重建时间。只有首次安装和完全卸载时才用down。
4. 源码结构与核心模块:拿到源码后怎么改
4.1 仓库目录到底该怎么组织
这个项目的源码我放在内部GitLab,和交付包结构不完全一样。源码仓库包含四个maven模块和三个前端工程,这七个代码库分别对应七个Docker服务。为了让“含源码”这件事真正有用,我把每个模块的README都写清楚,包括本地开发怎么跑、环境变量有哪些、Dockerfile位置在哪。很多项目源码虽然开放了,但没写怎么跑,接收源码的同学光猜目录结构就要花半天,所以这部分我特别在意。
整体目录:
system-integration-src/ ├── backend/ │ ├── system-api/ # API服务 │ ├── system-task/ # 定时任务服务 │ ├── system-common/ # 公共模块 │ └── system-file/ # 文件存储模块 ├── frontend/ │ ├── user-web/ # 用户端Web │ └── admin-web/ # 管理后台 ├── deploy/ │ └── ... # 前文交付包内容 └── sql/ ├── init.sql # 建表脚本 └── update.sql # 增量脚本4.2 核心后端源码片段解读
system-api模块里,我设置了一个自动配置类,负责从环境变量读取数据库和Redis配置。Spring Boot本身支持环境变量覆盖配置文件属性,所以代码里不需要做太多特殊处理。但有一个点需要注意:如果数据库连接串里包含特殊字符,环境变量里要做转义,否则compose解析会出错。我在application.yml里用这样的占位符:
spring: datasource: url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: ${DB_USER} password: ${DB_PASSWORD}源码里还提供了一个DatabaseInitializer类,用于在应用启动后检查基础数据是否存在,如果不存在就插入默认配置。这个类不是必需的功能,但它是系统集成项目里的一个保险:即使init.sql因为某种原因没有执行成功,应用启动时也能自动把最基础的字典数据补齐。代码思路是使用Spring的ApplicationRunner,在run方法里通过JdbcTemplate查询一张配置表的行数,为零则插入初始化语句。
4.3 前端如何通过环境变量对接不同后端地址
前端是纯静态页面,部署时如果API地址写死,换一个环境就要重新打包,非常不灵活。我的做法是在构建前端时只生成一个config.js文件,里面用全局变量存放API基础地址,Nginx容器启动时由entrypoint脚本根据环境变量渲染这个config.js。这样同一份前端镜像可以通过环境变量适配开发、测试、生产等多个环境。
nginx入口脚本的核心:
#!/bin/sh cat > /usr/share/nginx/html/config.js <<EOF window.API_BASE_URL = "${API_BASE_URL:-/api}"; EOF exec nginx -g "daemon off;"然后在Nginx配置里把/api反向代理到api服务。依赖这个机制,客户要换服务器地址,不需要改代码重打包,只需修改.env文件里的API_BASE_URL,然后重启web容器。这对面向多客户的系统集成商来说,节省了大量重复构建时间。
4.4 拿到源码后的自定义修改路径
如果读者想把这套方案改造成自己的项目,建议按这个路径走:第一步,保留deploy目录和sql目录不动,替换backend和frontend里的业务代码;第二步,确保自己的后端服务端口、健康检查路径与compose文件保持一致;第三步,修改.env里的密码和数据库名,执行deploy.sh install,先在本机跑通;第四步,再跑一遍update验证备份和回滚。按照这个顺序,通常半天内能跑起来。最忌讳的是先改编排文件,再改业务代码,出现问题后不知道是业务bug还是部署配置问题,排错成本会翻倍。
5. 常见问题与排查技巧实录
5.1 Windows环境Docker起不来怎么办
虽然最终交付目标大部分是Linux服务器,但开发人员有一半时间在Windows上。最常见的问题是Docker Desktop启动时报虚拟化未开启。这类报错往往不是因为CPU不支持,而是BIOS里没开VT-x,或者Windows功能里的虚拟机平台没有启用。处理办法是先到任务管理器性能页确认虚拟化状态,如果显示“已启用”,再去控制面板开启“适用于Linux的Windows子系统”和“虚拟机平台”两个可选功能,然后重启。这里有一个容易忽略的点:如果你用的是Windows家庭版,某些虚拟化功能入口找起来很费劲,建议直接用Linux服务器或云主机做部署验证,不要浪费时间在Docker Desktop上。
5.2 容器启动后又立即退出,怎么定位
“docker compose up -d”之后容器status显示Exited,这是交付现场遇到最多的故障。排查思路有固定套路:先看容器日志,再进容器看进程,最后检查组网和配置。我通常按这个顺序:
# 查看退出码和最近状态 docker compose ps -a # 查看具体日志 docker compose logs --tail=200 api # 检查配置是否被正确渲染 docker compose exec api env退出码是关键线索。Spring Boot应用如果数据库连不上,通常会在日志里打出connection refused;配置文件错误会打出Failed to bind properties;端口被占用会打印Port already in use。把日志尾部200行截图发给研发,基本能解决八成问题。如果日志完全没有输出就退出了,大概率是启动命令不对或缺少执行权限,这时候检查Dockerfile里的ENTRYPOINT是否可执行,用户是否有权限。
5.3 数据库时区、字符集和大小写敏感的坑
MySQL 8.0和旧版本在默认配置上有差异。客户之前用的MySQL 5.7,数据库名和表名都顺手写了大写,迁移到MySQL 8.0后Linux环境下表名默认大小写敏感,应用启动时报找不到表。我在compose里加的--lower_case_table_names=1就是为了规避这个问题。同理,字符集如果不强制指定utf8mb4,默认latin1会导致中文乱码。这两项配置必须在mysql容器的command里固定下来,不要依赖MySQL的默认值,因为不同版本默认值真的不一样。
还有时区,前面在Dockerfile里设置了Asia/Shanghai,但MySQL容器本身也需要设置TZ环境变量。否则会出现一种诡异现象:API日志时间是北京时间,数据库里的created_at却是UTC时间,前后端差8小时。排查数据问题时很容易被这种表象迷惑。我现在的习惯是,所有涉及时间的服务、数据库、脚本统一加TZ=Asia/Shanghai,不做任何例外。
5.4 服务器资源不足时的编排优化
系统集成项目经常被部署在2核4GB的低配机器上,同时跑MySQL、Redis、两个Nginx和一个Spring Boot,内存很容易爆。我通常会在compose文件里给每个服务加上部署资源限制:
deploy: resources: limits: memory: 1g reservations: memory: 512m这里要特别注意,docker compose的resource限制在老版本里只对Swarm模式生效,本地docker compose可能需要指定--compatibility参数。我的做法是直接写到compose里,同时在脚本中保留--compatibility开关。如果内存还是不够,优先关掉admin前端,或者把定时任务和API合并成同一个容器,牺牲一点隔离,换稳定运行。
5.5 日志无限增长和镜像占磁盘的清理
服务跑久了,容器日志和旧镜像会占满磁盘。我在compose里给每个服务配置了json-file日志驱动,限制最大50MB保留5个文件。同时部署脚本里加了一条clean命令,一键清理悬空镜像和停止的容器。客户现场没有专门的运维,磁盘满了系统会自动停机,所以这个清理命令要提前教给客户的对接人。
case "$1" in clean) echo "[INFO] 清理悬空镜像和停止的容器..." docker system prune -f docker image prune -a -f --filter "until=720h" ;; esac6. 从这套方案里沉淀的几个习惯
6.1 交付之后的运维交接
交付系统集成项目,不能把deploy.sh丢给客户就完事。我在交付文档里会附上一页“日常运维速查”,包括:查看状态用./deploy.sh status;查看日志用docker compose logs -f api;修改配置后重启用./deploy.sh update;手动备份用docker compose exec -T mysql mysqldump。这几条命令能覆盖客户90%以上的问题。另外,我要求每次更新前必须执行一次备份,如果客户忘了,脚本也会自动做,这能避免很多售后纠纷。
6.2 镜像仓库与版本管理
一开始我们直接用latest标签,结果有次误更新导致全量回滚。后来我强制所有交付镜像打上版本号,比如system-api:1.0.3,update脚本默认拉取当前.env里指定的TAG,回滚时把TAG改为上一个版本即可。镜像仓库内部用Registry 2搭建,只允许内网访问,既保证安全又不会因为公网拉取失败影响交付。使用TAG管理后,发布和回滚都变得非常可控,再也没有出现过“不小心把开发镜像部署到生产”的问题。
6.3 致新手的几个建议
如果你刚接触这个方案,第一件事不是写Dockerfile,而是先把自己项目的启动步骤列清楚:依赖什么服务、需要哪些环境变量、有没有外部文件。列清楚之后再看这文章里的Dockerfile和compose文件,思路会通得很快。我见过太多人把大量时间花在用什么基础镜像、怎么瘦身上,结果忽略了最重要的事情:让自己能在任何一台机器上把服务一键跑起来。先做到“能跑”,再谈“跑得好”。容器化只是一个工具,最终目标是省心。