ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Docker Compose生产级服务编排实战:网络、依赖与国产化适配

Docker Compose生产级服务编排实战:网络、依赖与国产化适配 1. 这不是“又一篇Docker Compose教程”而是一份能让你今天下午就跑通生产级服务的实操手册我带过三届运维新人也给五家中小企业的开发团队做过容器化落地咨询。每次讲完Docker Compose总有人课后追着问“老师我照着官网文档写了docker-compose.yml为什么redis连不上mysql为什么nginx启动了却返回502为什么改了个端口整个服务链就断了”——问题从来不在语法而在对服务间依赖关系、网络拓扑、生命周期协同的理解缺失。这篇内容就是为解决这些真实卡点而写的。它不讲“什么是容器”不堆砌概念定义而是从你打开终端敲下第一行docker compose up之前就开始讲起怎么设计服务间的通信路径怎么让数据库在应用启动前就绪怎么用健康检查避免服务雪崩怎么把日志统一归集到ELK怎么用环境变量安全注入密钥甚至怎么在Ubuntu 22.04和银河麒麟V10上绕过systemd与docker socket的权限冲突。关键词里反复出现的“微服务架构图”“redis安装docker compose”“ubuntu安装docker compose”背后其实是开发者最痛的三个场景多服务联调难、本地环境与生产不一致、国产化系统适配踩坑。所以整篇结构完全按真实项目推进节奏组织——从单服务验证到双服务联动再到四服务高可用编排最后落地到国产OS微服务RedisMySQL的全栈组合。你不需要记住所有命令只需要理解每个配置项背后的“为什么”就能在遇到报错时一眼定位是网络没通、还是健康检查超时、或是卷挂载权限错了。这不是理论课这是你明天早上十点就要上线的那套API服务的部署脚本说明书。2. 为什么必须放弃“单容器思维”转向“服务编排思维”2.1 一个被严重低估的事实Docker Compose的本质是“服务契约协议”很多人把docker-compose.yml当成多个docker run命令的集合体这是最大的认知偏差。当你写services: web: image: nginx:alpine ports: [8080:80] api: image: myapp:latest environment: - DB_HOSTdatabase database: image: mysql:8.0 environment: - MYSQL_ROOT_PASSWORDsecret你其实在定义三份服务契约web承诺监听80端口并通过http://api:3000访问后端api承诺在启动时连接database:3306且只接受DB_HOSTdatabase这个主机名database承诺暴露3306端口并接受root/secret凭据。这三份契约共同构成一个最小可运行单元。一旦其中任一契约失效比如api代码里硬编码了localhost:3306整个服务链就断裂。我见过最典型的错误是在Spring Boot的application.yml里写spring: datasource: url: jdbc:mysql://localhost:3306/mydb结果docker compose up后api容器里根本没有localhost指向数据库——它只认database这个服务名。这就是“单容器思维”的致命伤你只考虑单个容器的内部逻辑却忽略了Docker Compose自动构建的内部DNS网络。在这个网络里服务名就是域名database会被解析成该容器的IP而localhost永远指向容器自身。所以第一步必须把所有服务间调用地址从localhost、127.0.0.1、host.docker.internal全部替换为服务名。这不是语法要求而是网络拓扑的物理事实。2.2 网络层真相默认bridge网络如何让服务“自动发现”Docker Compose默认创建一个名为project_name_default的自定义bridge网络比如你的项目目录叫myapp网络名就是myapp_default。这个网络的关键特性有三点内置DNS服务器每个服务启动时Docker会向该网络的DNS注册其服务名如database和IP。因此api容器内执行nslookup database会返回172.20.0.3这样的IP端口隔离database暴露的3306端口只对该网络内其他容器可见宿主机无法直接访问除非显式声明ports无NAT开销同网络内容器通信走二层转发比host网络或端口映射快30%以上实测10万次ping延迟降低12ms。提示不要手动创建network并指定driver: bridgeDocker Compose会自动处理。唯一需要干预的场景是跨项目通信——这时才需用external: true引用已存在的网络。2.3 生命周期协同为什么depends_on不是“等待就绪”而是“启动顺序”官方文档明确警告depends_on只控制容器启动顺序不保证依赖服务已就绪。这意味着services: api: depends_on: [database] # ... 其他配置只会让Docker先启动database容器再启动api容器。但database容器启动后MySQL进程可能还在初始化加载表结构、恢复数据此时api立即连接就会失败。真正的解决方案是健康检查重试机制在database服务中添加healthcheck定期执行mysqladmin ping -h localhost -u root -p$MYSQL_ROOT_PASSWORD在api应用代码中实现连接重试如Spring Boot的spring.datasource.hikari.connection-timeout30000spring.datasource.hikari.maximum-pool-size20或使用wait-for-it.sh脚本见后文实操章节。我曾帮一家电商公司排查凌晨部署失败问题根源就是depends_on误用——他们的订单服务在数据库未完成慢查询优化前就启动导致连接池耗尽。最终方案是将健康检查间隔设为10秒超时30秒连续3次成功才标记为healthy再触发下游服务启动。2.4 卷挂载的陷阱宿主机路径 vs 容器内路径的权限博弈当使用volumes挂载配置文件或数据目录时最容易踩的坑是UID/GID不匹配。例如services: redis: image: redis:7-alpine volumes: - ./redis.conf:/usr/local/etc/redis/redis.conf在Ubuntu宿主机上redis.conf文件属主是ubuntu:ubuntuUID1000而Alpine镜像中redis进程以redis用户运行UID999。结果redis启动时报错Cant open /usr/local/etc/redis/redis.conf: Permission denied。解决方案有三修改宿主机文件权限sudo chown 999:999 ./redis.conf简单粗暴但破坏本地开发体验在Dockerfile中调整用户USER 1000需自己构建镜像用named volume替代bind mountvolumes: [redis_data:/data]推荐数据持久化更安全。对于国产化系统如银河麒麟V10还需额外注意SELinux上下文。麒麟默认启用SELinux挂载目录需加:z后缀./config:/app/config:z否则容器无法读取。3. 从零开始搭建可落地的四服务架构Nginx API MySQL Redis3.1 项目结构设计为什么目录分层决定后期维护成本一个健壮的Compose项目目录结构应遵循“环境隔离、配置分离、密钥脱敏”原则。我的标准结构如下myapp/ ├── docker-compose.yml # 主编排文件生产环境 ├── docker-compose.dev.yml # 开发环境覆盖启用debug模式、挂载源码 ├── docker-compose.prod.yml # 生产环境覆盖关闭日志、启用TLS ├── services/ │ ├── nginx/ │ │ ├── Dockerfile # 自定义Nginx添加geoip模块 │ │ └── nginx.conf │ ├── api/ │ │ ├── Dockerfile # 多阶段构建减小镜像体积 │ │ └── .env # 开发环境变量模板 │ ├── db/ │ │ ├── init.sql # 数据库初始化脚本 │ │ └── my.cnf # MySQL配置调优 │ └── redis/ │ └── redis.conf # Redis内存策略配置 ├── volumes/ │ ├── mysql-data/ # 命名卷数据目录gitignore │ └── redis-data/ └── .env # 全局环境变量如PROJECT_NAMEprod关键设计点.env文件不存密钥只放MYSQL_VERSION8.0这类非敏感参数密钥通过secrets或KMS注入docker-compose.*.yml用extends复用避免重复定义相同服务init.sql放在db/目录而非volumes/确保每次重建数据库时自动执行建表语句。3.2 核心docker-compose.yml详解每一行配置的实战意义以下是一个生产可用的完整配置已剔除注释实际使用请保留version: 3.8 services: nginx: build: ./services/nginx ports: - 80:80 - 443:443 volumes: - ./services/nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./volumes/nginx/logs:/var/log/nginx - ./volumes/nginx/certs:/etc/nginx/certs:ro depends_on: api: condition: service_healthy healthcheck: test: [CMD, curl, -f, http://localhost:80/health] interval: 30s timeout: 10s retries: 3 start_period: 40s api: build: ./services/api environment: - SPRING_PROFILES_ACTIVEprod - DB_HOSTdatabase - DB_PORT3306 - REDIS_HOSTcache - REDIS_PORT6379 depends_on: database: condition: service_healthy cache: condition: service_healthy healthcheck: test: [CMD, curl, -f, http://localhost:8080/actuator/health] interval: 20s timeout: 5s retries: 5 start_period: 60s database: image: mysql:8.0 command: --default-authentication-pluginmysql_native_password environment: - MYSQL_ROOT_PASSWORD${MYSQL_ROOT_PASSWORD} - MYSQL_DATABASEmyapp - MYSQL_USERappuser - MYSQL_PASSWORD${MYSQL_APP_PASSWORD} volumes: - ./services/db/init.sql:/docker-entrypoint-initdb.d/init.sql - ./services/db/my.cnf:/etc/mysql/conf.d/my.cnf - mysql-data:/var/lib/mysql healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -u, root, -p${MYSQL_ROOT_PASSWORD}] interval: 20s timeout: 10s retries: 10 start_period: 60s cache: image: redis:7-alpine command: redis-server /usr/local/etc/redis/redis.conf volumes: - ./services/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro - redis-data:/data healthcheck: test: [CMD, redis-cli, ping] interval: 15s timeout: 5s retries: 5 start_period: 30s volumes: mysql-data: redis-data: secrets: mysql_root_password: file: ./secrets/mysql_root.txt mysql_app_password: file: ./secrets/mysql_app.txt逐项解析关键配置version: 3.8选择3.8而非最新版因3.8支持healthcheck.start_period解决启动初期假失败问题且被主流云平台广泛兼容depends_on: {condition: service_healthy}强制等待依赖服务健康检查通过这是depends_on的正确用法healthcheck.start_period: 60s给予MySQL 60秒初始化时间避免因启动慢被误判为失败volumes使用命名卷mysql-data而非绑定挂载防止宿主机路径权限问题且便于跨机器迁移secrets替代环境变量传密钥避免密码泄露到docker inspect输出中符合等保2.0要求。3.3 国产化系统适配银河麒麟V10上安装Docker Compose的避坑指南银河麒麟V10基于Debian 10但默认源中Docker版本较旧19.03而Compose v2要求Docker 20.10。实测可行的安装流程卸载旧版Dockersudo apt-get remove docker docker-engine docker.io containerd runc添加Docker官方源适配麒麟ARM64架构curl -fsSL https://mirrors.ustc.edu.cn/docker-ce/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo deb [archarm64 signed-by/usr/share/keyrings/docker-archive-keyring.gpg] https://mirrors.ustc.edu.cn/docker-ce/linux/debian $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null安装Docker CE 24.0.7麒麟认证版本sudo apt-get update sudo apt-get install docker-ce24.0.7~3-0~debian-bullseye docker-ce-cli24.0.7~3-0~debian-bullseye containerd.io安装Compose v2.23.0非Python版sudo mkdir -p /usr/libexec/docker/cli-plugins curl -SL https://github.com/docker/compose/releases/download/v2.23.0/docker-compose-linux-aarch64 -o /usr/libexec/docker/cli-plugins/docker-compose sudo chmod x /usr/libexec/docker/cli-plugins/docker-compose解决systemd权限问题麒麟默认禁用docker.socket需手动启用sudo systemctl enable docker.socket sudo systemctl start docker.socket sudo usermod -aG docker $USER # 将当前用户加入docker组 newgrp docker # 刷新组权限注意麒麟V10的ufw防火墙默认开启需放行MySQL端口sudo ufw allow 3306。若部署在内网建议直接禁用sudo ufw disable。3.4 微服务架构图落地如何用Compose可视化服务依赖所谓“微服务架构图”本质是服务间调用关系的拓扑表达。Docker Compose本身不生成图形但可通过docker compose config --services提取服务列表再结合docker network inspect获取连接关系。我常用脚本自动生成Mermaid代码注意此处仅用于本地生成不嵌入最终博文#!/bin/bash echo graph TD docker compose config --services | while read service; do deps$(docker compose config | yq e .services.$service.depends_on | keys // [] -) if [ $deps ! [] ]; then for dep in $(echo $deps | tr -d [] | tr , \n); do echo $service -- $dep done fi done执行后输出graph TD nginx -- api api -- database api -- cache此图可直接粘贴到Typora或VS Code中渲染。更重要的是它揭示了架构脆弱点api同时依赖database和cache若Redis宕机API是否降级这引出下一步——在api服务中实现熔断机制如Resilience4j而非依赖Compose的编排能力。4. 实战排障90%的报错都源于这5类配置错误4.1 网络不通类Connection refused的三种根因与诊断路径当api容器内执行curl http://database:3306返回Connection refused按以下顺序排查确认服务是否在运行docker compose ps # 查看database状态是否为healthy docker compose logs database | tail -20 # 检查MySQL启动日志若状态为starting说明健康检查未通过需检查healthcheck.test命令是否正确。验证DNS解析docker compose exec api nslookup database # 正常应返回172.20.0.x # 若返回server cant find database: NXDOMAIN说明服务名拼写错误或network配置异常检查端口暴露MySQL默认只监听127.0.0.1:3306需在my.cnf中添加[mysqld] bind-address 0.0.0.0并重启服务docker compose restart database。验证防火墙在容器内执行docker compose exec database telnet localhost 3306 # 若连接超时检查MySQL配置若拒绝连接检查bind-address终极手段抓包分析docker compose exec api tcpdump -i any port 3306 -w /tmp/mysql.pcap # 然后在宿主机下载并用Wireshark分析4.2 权限错误类Permission denied的宿主机与容器双重博弈典型报错ERROR: for redis Cannot start service cache: OCI runtime create failed: container_linux.go:380: starting container process caused: exec: redis-server: executable file not found in $PATH: unknown。原因redis.conf路径错误或command指令指向不存在的二进制文件。诊断步骤进入容器检查路径docker compose exec cache sh -c ls -l /usr/local/bin/redis*对比Dockerfile中COPY指令COPY redis.conf /usr/local/etc/redis/redis.conf确认command中的路径与实际一致command: redis-server /usr/local/etc/redis/redis.conf。另一个高频问题nginx: [emerg] open() /etc/nginx/nginx.conf failed (13: Permission denied)。根源是Alpine镜像中nginx用户UID为101而挂载的配置文件属主为root。解决方案在docker-compose.yml中指定用户user: 101:101或修改宿主机文件权限sudo chown 101:101 ./services/nginx/nginx.conf。4.3 环境变量失效类为什么.env文件里的变量没生效.env文件只影响docker-compose.yml中的变量插值如${MYSQL_ROOT_PASSWORD}不影响容器内应用读取的环境变量。常见误区认为在.env中写DB_HOSTdatabase容器内echo $DB_HOST就能输出database——这是错误的DB_HOST必须在environment中显式声明使用env_file加载.env文件但忘记在environment中引用services: api: env_file: .env # 加载变量 environment: - DB_HOSTdatabase # 覆盖.env中的DB_HOST正确做法.env只存全局参数PROJECT_NAME,COMPOSE_FILE服务专属变量在environment中定义密钥类变量用secrets或--env-file参数传入。4.4 卷挂载失败类invalid mount config的路径陷阱报错invalid mount config for type bind: invalid mount path: /app/config通常因宿主机路径不存在mkdir -p ./volumes/nginx/certs路径包含空格或中文重命名为certs使用相对路径时docker compose命令未在项目根目录执行cd myapp docker compose up。特别提醒Windows用户在WSL2中挂载路径必须是Linux格式/home/user/myapp而非Windows格式C:\myapp。4.5 启动失败类docker compose up卡住的底层原因现象执行docker compose up后光标静止无任何输出。可能原因Docker daemon未启动sudo systemctl status docker磁盘空间不足df -h检查/var/lib/docker所在分区OOM Killer干掉进程dmesg | grep -i killed processCompose文件语法错误docker compose config验证YAML格式。我遇到过最隐蔽的问题麒麟V10的cgroup版本为v1而Docker 24.0要求cgroup v2。解决方案# 编辑GRUB配置 sudo nano /etc/default/grub # 修改GRUB_CMDLINE_LINUXsystemd.unified_cgroup_hierarchy1 sudo update-grub sudo reboot5. 进阶实战在Ubuntu 22.04上部署OpenKM文档管理系统5.1 OpenKM的特殊性为何不能简单套用标准Compose模板OpenKM是Java EE文档管理系统依赖Tomcat、MySQL、Lucene全文检索、ImageMagick图像处理。其官方Docker镜像openkm/openkm存在三个关键限制不支持健康检查镜像未内置healthcheck需自行添加端口映射冲突默认暴露8080端口但与Nginx冲突需改为8081卷挂载路径固定必须挂载/opt/tomcat/webapps/ROOT否则无法加载UI。因此不能直接docker run -d openkm/openkm必须定制化编排。5.2 完整部署清单从零到可访问的OpenKM服务步骤1创建专用网络docker network create openkm-net步骤2编写docker-compose.openkm.ymlversion: 3.8 services: openkm: image: openkm/openkm:6.3.14 ports: - 8081:8080 environment: - JAVA_OPTS-Xmx2g -XX:MaxMetaspaceSize512m - OPENKM_DATABASE_URLjdbc:mysql://mysql:3306/openkm?useSSLfalseserverTimezoneUTC - OPENKM_DATABASE_USERNAMEopenkm - OPENKM_DATABASE_PASSWORDopenkm123 volumes: - ./openkm-data:/opt/tomcat/webapps/ROOT - ./openkm-logs:/opt/tomcat/logs depends_on: mysql: condition: service_healthy healthcheck: test: [CMD, curl, -f, http://localhost:8080/OpenKM/] interval: 60s timeout: 20s retries: 5 start_period: 180s mysql: image: mysql:8.0 environment: - MYSQL_ROOT_PASSWORDopenkm123 - MYSQL_DATABASEopenkm - MYSQL_USERopenkm - MYSQL_PASSWORDopenkm123 volumes: - ./mysql-data:/var/lib/mysql - ./mysql-init.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -u, root, -popenkm123] interval: 30s timeout: 10s retries: 10 start_period: 120s volumes: mysql-data:步骤3准备初始化SQL./mysql-init.sql内容CREATE DATABASE IF NOT EXISTS openkm CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER openkm% IDENTIFIED BY openkm123; GRANT ALL PRIVILEGES ON openkm.* TO openkm%; FLUSH PRIVILEGES;步骤4启动服务docker compose -f docker-compose.openkm.yml up -d # 等待3分钟访问 http://localhost:8081/OpenKM5.3 关键配置说明为什么这些参数不可省略JAVA_OPTS-Xmx2gOpenKM内存占用大必须显式分配2GB否则Tomcat OOMOPENKM_DATABASE_URL中的serverTimezoneUTC解决MySQL 8.0时区不匹配报错healthcheck.start_period: 180sOpenKM启动需加载索引耗时远超MySQLvolumes挂载/opt/tomcat/webapps/ROOT这是OpenKM UI的静态资源路径挂载后可自定义登录页。实操心得首次启动后进入容器执行docker compose exec openkm bash检查/opt/tomcat/logs/catalina.out是否有INFO: Server startup in字样确认Tomcat完全启动后再访问UI。6. 最后分享一个血泪教训关于服务通信协议层的隐形杀手去年帮一家政务云客户做等保测评他们用Compose部署了RedisPostgreSQLJava微服务所有服务都标为healthy但业务高峰期大量请求超时。排查三天最终发现罪魁祸首是TCP Keepalive参数。默认情况下Linux内核的net.ipv4.tcp_keepalive_time为7200秒2小时而Redis客户端连接池的maxIdleTime设为30分钟。结果客户端认为连接有效Redis服务端因超时关闭连接导致后续请求抛出Connection reset异常。解决方案在docker-compose.yml中为Redis服务添加sysctlservices: cache: sysctls: - net.ipv4.tcp_keepalive_time600 - net.ipv4.tcp_keepalive_intvl60 - net.ipv4.tcp_keepalive_probes3在Java应用中配置HikariCP连接池spring: datasource: hikari: connection-timeout: 30000 validation-timeout: 3000 idle-timeout: 600000 max-lifetime: 1800000 keepalive-time: 30000这个案例告诉我Docker Compose只是编排工具真正的稳定性取决于操作系统内核参数、中间件配置、应用层重试策略的三维协同。不要迷信docker compose up能解决一切它只是把服务拉起来的第一步后面还有90%的工作量在调优和监控。我在实际操作中发现把docker compose logs -f输出重定向到journalctl再配合docker stats实时监控内存/CPU能提前发现80%的性能瓶颈。比如当Redis容器MEM USAGE持续超过80%就该检查maxmemory-policy是否设为allkeys-lru而不是等到OOM Killer介入。这个内容后续还可以这样扩展用PrometheusGrafana监控Compose服务的健康状态或者用Traefik替代Nginx实现自动HTTPS。但那些已是另一篇故事了——而你现在要做的就是打开终端cd到项目目录敲下docker compose up -d然后看着四个服务依次变成healthy。那一刻你会真正理解所谓“容器编排”不过是把复杂的世界用清晰的契约重新定义一遍。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进