拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Docker Compose多容器编排实战:从YAML配置到生产部署

1. 从三条命令说起多容器编排到底在解决什么问题1.1 手动 docker run 的三重痛点先说个场景你在本地开发一个 Web 应用后端要连 Redis还要挂一个 MySQL。这个时候最朴素的做法就是开三个终端分别把三条 docker run 敲进去docker run -d --name web -p 8080:80 --link redis:redis my-web-image docker run -d --name redis -p 6379:6379 -v redis-data:/data redis:7-alpine docker run -d --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORDroot mysql:8.0第一次敲还行第二次也能忍到第三次你就会发现一个很现实的问题这些命令越来越长长得连换行都不知道该怎么断。而且容器一多你要记住每个容器的端口、数据卷、网络别名、环境变量稍一疏忽就漏了某个-v等重启之后数据丢了你才意识到问题出在哪。这还不是最头疼的。更麻烦的是依赖关系。比如你的 Web 服务依赖 Redis 已经就绪可是裸用 docker run 的时候容器之间的启动顺序完全靠人肉把控。今天你先启动了 Web 再启动 Redis应用启动时连不上缓存直接崩了。虽然大多数应用有重试机制但这种不确定性在开发环境里特别耗神。再加上你还要管理自定义网络。为了让两个容器能用容器名互相访问你得手动docker network create my-network然后每条命令后面再加一个--network my-network。一条命令里塞这么多参数已经不是在写命令了是在练记忆力。1.2 Docker Compose 到底是什么又不是什么Docker Compose 的核心思路一句话就能说清把一堆docker run参数转换成一份声明式的 YAML 配置文件然后用docker compose up -d一条命令全部拉起来。它不是一个新的容器运行时也不是 Docker 的替代品。它本质上是一个编排工具负责解释那份 YAML 文件然后帮你把启动、网络、数据卷、依赖关系这些底层操作全部做了。你可以把它理解成装修房屋时的施工图纸docker run是现场跟工人说这里放个沙发、那里放个茶几Compose 则是把每件家具的型号、位置、摆放顺序全部画在一张图纸上工人照着图纸施工就行。这份配置文件通常叫docker-compose.yml放在项目的根目录。里面定义了你需要哪些服务、用哪个镜像、暴露哪些端口、挂载哪些数据卷、加入哪个网络。以后不管是你自己还是同事只要拿到这份文件在装有 Docker 的机器上执行docker compose up -d就能得到一个完全一致的环境。我见过很多人分不清 Compose 和 KubernetesK8s的区别。简单说Compose 处理的是一台机器上的多容器编排适合开发环境、小型生产部署、家庭服务器、单体应用拆分的场景K8s 则是面向多台机器的大规模集群调度。如果你还没有几十个服务要管理用 Compose 就对了没必要一上来就搬一套 K8s 集群回来自己维护。1.3 适合谁、不适合谁先想清楚再上车Docker Compose 最典型的适用人群有这几类本地开发需要同时跑多个中间件Redis、MySQL、RabbitMQ、MongoDB的后端开发者需要在服务器上部署一套完整应用前端 Nginx 后端 API 数据库 缓存的运维工程师想在自己 NAS 或家庭服务器上跑一堆自建服务比如相册、博客、监控面板的爱好者用 CI/CD 流水线做自动化测试需要临时拉起一组依赖服务的测试工程师那有没有不适合用 Compose 的场景有。如果你的项目只有一个容器只敲一条docker run就能搞定那就没必要刻意引入 Compose。另外如果你的服务需要根据流量自动扩容缩容、需要跨多台机器调度那就是 K8s 或 Docker Swarm 的领域了Compose 帮不了你。不过我个人还是建议只要你的服务数量超过一个或者你发现自己经常要重复输入同一串 docker run 参数就值得把 Compose 用起来。一次迁移成本并不高收益却是长期的。2. 核心概念拆解docker-compose.yml 里有什么2.1 services组成应用的每一块砖services是 docker-compose.yml 里最核心的字段它定义了应用由哪些容器组成。每个 service 对应一个镜像或一个构建上下文你可以理解成一类容器。一个最基本的 service 定义长这样services: redis: image: redis:7-alpine container_name: my-redis ports: - 6379:6379 volumes: - redis-data:/data这里面redis是服务名在 Compose 内部网络里其他容器可以直接用redis这个主机名访问它。container_name是可选的不写的话 Compose 会自动生成一个项目名-服务名-序号形式的容器名。ports是端口映射6379:6379表示把容器的 6379 端口映射到宿主机的 6379 端口。需要注意的是service 不一定要用现成镜像也可以从 Dockerfile 构建services: web: build: . ports: - 8080:80这里的build: .表示使用当前目录下的 Dockerfile 构建出镜像再运行容器。你还可以指定build的context和dockerfile字段来精确控制构建路径。2.2 networks容器之间怎么通信默认情况下执行docker compose up时 Compose 会为整个项目自动创建一个网络项目里的所有 service 都会加入这个网络。在同一个网络里容器之间可以互相通过服务名访问完全不需要知道对方的 IP 地址。这个设计非常实用。比如 Web 服务要连接 Redis你在代码里配置连接地址时只需要填redis:6379而不需要填localhost:6379或某个具体的 IP。因为 Compose 内置的 DNS 会把服务名解析成对应容器的 IP。你可能会问为什么不直接把 Redis 的端口映射到宿主机然后在 Web 服务里连localhost:6379在开发环境你可以这么干但在生产环境或者多网络环境里会有问题端口映射会暴露到宿主机上别人也能直接访问你的 Redis而且一旦端口被占用整个环境就起不来了。所以 Compose 更推荐的做法是需要对外提供服务比如 Web 的 80 端口、数据库的 3306 端口才用ports映射到宿主机服务之间的内部调用直接通过服务名走内置网络不做端口映射。这样既安全又干净。2.3 volumes数据不丢的关键容器本身是临时的重建容器之后里面的数据就没了。所以凡是需要持久化的数据比如数据库文件、上传的图片、日志文件都应该挂载到 volume 或宿主机目录里。Compose 里的 volume 有两种写法一种是具名卷services: mysql: image: mysql:8.0 volumes: - mysql-data:/var/lib/mysql volumes: mysql-data:这里mysql-data是一个具名卷Docker 会在宿主机上创建一个受管目录来存储数据。即使你docker compose down删掉了容器和网络数据依然保留在卷里下次up的时候重新挂载就能恢复。另一种是 bind mount直接映射宿主机目录services: nginx: image: nginx:alpine volumes: - ./html:/usr/share/nginx/html这种方式适合开发场景本地改了文件容器里立刻生效不用重新构建镜像。但要注意目录权限问题尤其是以非 root 用户运行的容器经常会出现容器内写不了宿主机目录的错误。2.4 depends_on、restart 与 env_file几个容易被忽视的配置depends_on用来声明服务之间的启动依赖关系services: web: image: my-web depends_on: - redis这个配置告诉 Compose启动web之前先启动redis。但请注意它只保证先启动不保证已经就绪。也就是说redis容器启动了可 Redis 进程可能还没初始化完web就已经开始连它了。想解决就绪问题可以配合 healthcheck 使用这种写法在一些新版本 Compose 里也支持services: web: image: my-web depends_on: redis: condition: service_healthy redis: image: redis:7-alpine healthcheck: test: [CMD, redis-cli, ping] interval: 5s timeout: 3s retries: 5这样 Compose 就会等 Redis 的 healthcheck 通过之后才启动 Web可靠性高很多。我在后面讲生产部署的时候会再展开。restart是容器的重启策略常见值有no、always、on-failure、unless-stopped。生产环境里我一般给常驻服务配unless-stopped或always这样服务器重启或者容器异常退出后Docker 会自动把它拉起来。env_file可以把环境变量拆到独立文件里避免把密码、密钥直接写进 docker-compose.yml。比如services: app: image: my-app env_file: - .env.env文件里一行一个KEYVALUE注意不要提交到 Git 仓库尤其是生产环境的密钥。2.5 为什么 Compose 比一条条 docker run 更稳从工程角度说Compose 最大的价值是可重复性。手工敲命令的时候每个人对参数的记忆和理解都不一样A 说多加一个-eB 说忘了一个--link最后环境之间差别越来越大。而 Compose 把环境定义固化成了代码团队协作时只要共享一份 docker-compose.yml每个人拉起来的环境就是一样的。另一个价值是操作粒度。一条条 docker run 管的是单个容器Compose 管的是一组服务。docker compose up -d把一组全部拉起来docker compose ps看到的是全组状态docker compose logs -f跟的是全组日志docker compose down把全组按依赖顺序全部停掉。这种整组操作的体验用 docker run 是完全没有的。3. 完整实战用 Docker Compose 编排一个 Web Redis 应用3.1 预先想好的目录结构和镜像选型为了让你能直接照着做我设计一个最常见的场景一个简单的访问计数服务Web 后端用 Python Flask 写数据存到 Redis 里。整个项目目录结构这样安排compose-demo/ ├── app/ │ ├── app.py │ ├── requirements.txt │ └── Dockerfile ├── docker-compose.yml └── .env镜像选型上Web 服务用自己的 Dockerfile 构建基础镜像用python:3.11-slimRedis 直接用官方redis:7-alpine。为什么用 alpine因为它镜像体积小、启动快作为 Redis 这种单一服务的运行环境很合适。app.py 的代码故意写得很简单import os import redis from flask import Flask app Flask(__name__) r redis.Redis(hostredis, port6379, db0) app.route(/) def index(): count r.incr(hits) return fHello Docker Compose! This is visit #{count}.注意代码里连接 Redis 的主机名是redis这个就是 Compose 网络里服务名解析的结果而不是localhost。requirements.txtflask3.0.3 redis5.0.4DockerfileFROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY app.py . EXPOSE 5000 CMD [python, app.py]3.2 第一个 docker-compose.yml 长这样在项目根目录创建docker-compose.ymlservices: web: build: ./app container_name: compose-demo-web ports: - 8080:5000 environment: - REDIS_HOSTredis depends_on: redis: condition: service_healthy networks: - demo-network restart: unless-stopped redis: image: redis:7-alpine container_name: compose-demo-redis volumes: - redis-data:/data healthcheck: test: [CMD, redis-cli, ping] interval: 5s timeout: 3s retries: 5 networks: - demo-network restart: unless-stopped volumes: redis-data: networks: demo-network:这个文件我给每个字段加了实际含义一条条说web服务通过build: ./app从源码构建镜像。Docker 会先在当前目录找 Dockerfile构建完成之后用这个镜像启动容器。ports把容器的 5000 端口映射到宿主机 8080这样浏览器访问localhost:8080就能看到页面。environment里设置了REDIS_HOSTredis这其实是给忘了改代码的情况留一个兜底。你完全可以在代码里直接读这个环境变量而不是硬编码主机名。redis服务挂载了redis-data卷到/data这是 Redis 持久化数据的默认目录。healthcheck用redis-cli ping检查 Redis 是否真正就绪。Web 的depends_on.condition: service_healthy表示只有 Redis 健康检查通过后才会启动 Web。restart: unless-stopped保证容器异常退出后能自动恢复。3.3 启动、查看状态、伸缩与停止日常操作全流程写完配置文件之后执行docker compose up -d-d表示后台运行。如果你不写-d日志会直接打印在当前终端按 CtrlC 会同时停掉所有容器这适合调试场景。第一次执行时Docker 会自动构建 Web 镜像并拉取 Redis 镜像耐心等一会儿。启动完成后看状态docker compose ps正常情况下你会看到两个容器都在运行一个是 web一个是 redis。接着打开浏览器访问http://localhost:8080每刷新一次页面计数加一说明 Web 和 Redis 之间的链路是通的。如果想看日志用docker compose logs -f-f是 follow 模式类似tail -f。也可以只跟某个服务docker compose logs -f web当你想停掉整套环境时区分两个命令非常关键docker compose stop docker compose downstop只停容器数据卷、网络都还在下次docker compose start可以快速恢复。down会删除容器和网络但具名卷默认还在数据不会丢。如果你想连卷一起清掉用docker compose down -v这个命令请慎用因为会把 redis-data 里的数据全删掉。3.4 网络细节为什么在容器里可以访问 redis 这个名字我见过不少刚开始用 Compose 的人卡在这里明明 Redis 的端口没有映射到宿主机为什么 Web 容器里能用redis:6379连上答案就是 Compose 自动创建的那个网络。你可以在宿主机上执行docker network ls会看到一个compose-demo_demo-network这样的网络。然后docker network inspect compose-demo_demo-network能看到这个网络里挂着的两个容器以及它们的 IP。当 Web 容器尝试访问redis这个主机名时Docker 内置 DNS 会把它解析成 redis 容器在这个网络里的 IP所以不需要走宿主机的端口映射。这个机制在生产环境特别有用。你不需要把数据库的端口暴露到公网因为只有同一个自定义网络里的服务才能互相访问。默认网络之外的主机根本连不上数据库安全风险直接降了一个量级。4. 生产环境部署时的编排经验别把开发配置直接照搬4.1 设置 restart policy 前先想清楚restart: always看起来省心但生产环境并不建议无脑用。如果容器因为配置错误、密码错误等原因反复崩溃restart 策略会一直尝试重启造成日志刷屏甚至宿主机负载升高。更稳妥的做法是区分场景数据库、Redis、消息队列这类基础设施可以用unless-stopped它在 Docker 守护进程启动时自动拉起但如果你手动 stop 了容器它不会自动再拉起。应用服务如果希望崩溃后自动恢复可以配on-failure:5这样的策略只对非零退出码重启最多重试 5 次。批量任务类容器比如定时跑脚本、数据迁移任务一般不要配 restart跑完就退出配了反而会陷入重启循环。我个人在生产服务器上通常用unless-stopped它比always多了一个好处手动 stop 时不会被守护进程重新拉起来方便维护。4.2 健康检查与依赖排序depends_on 不是万能的前面提到的depends_on: condition: service_healthy是一个比较稳妥的依赖方案但有个前提依赖的服务必须配置了 healthcheck。如果没配Compose 会直接认为该服务已经就绪然后启动依赖它的服务。在老的 docker-compose 版本里depends_on没有condition选项只有简单的启动顺序控制很多人因此踩过坑。所以如果你的生产环境用到depends_on确认一下 Compose 版本至少是 2.x。执行docker compose version查看版本号。健康检查本身也要设计合理。比如 Redis 的健康检查用redis-cli ping返回 PONG 才算通过。MySQL 的检查则可以用mysqladmin ping -h localhost。注意健康检查的interval不要太短5 秒是一个合理的默认值如果中间件启动很慢retries要多给一点设置 10 次甚至更多都不过分。4.3 日志、资源限制与 .env 的实战用法容器日志如果不控制时间长了会占满磁盘。Compose 支持配置 logging 驱动services: app: image: my-app logging: driver: json-file options: max-size: 10m max-file: 3这样单份日志最大 10MB保留 3 份超过自动轮转。这套配置在生产环境几乎是必须的否则跑一个月的服务日志可能轻松涨到几个 GB。资源限制也很重要。不想让某个服务把宿主机 CPU 或内存吃满可以这样写services: app: image: my-app deploy: resources: limits: cpus: 0.5 memory: 512M这里限制了 app 最多用 0.5 个 CPU 核心和 512MB 内存。注意deploy字段在 Compose v2 里对单机部署是部分支持的实测下来限制内存和 CPU 是有效的。.env文件我建议在生产环境必须用起来。把容易变化的值单独拎出来比如端口号、版本号、密码避免每次修改都去改 docker-compose.yml# .env APP_PORT8080 REDIS_IMAGEredis:7-alpine MYSQL_ROOT_PASSWORDchange-medocker-compose.yml 里通过${APP_PORT}引用services: web: build: ./app ports: - ${APP_PORT}:5000这样做的好处是不同环境测试、生产可以各自维护一套 .env而配置文件本身完全不用改。4.4 常见生产环境组合Redis、RabbitMQ、数据库一起上实际生产部署里很少只有两个服务。我以一个典型业务后端为例给出一个组合配置风格的片段services: api: build: ./api depends_on: mysql: condition: service_healthy redis: condition: service_healthy rabbitmq: condition: service_healthy ports: - 8000:8000 env_file: - .env worker: build: ./api command: celery -A tasks worker --loglevelinfo depends_on: - rabbitmq env_file: - .env mysql: image: mysql:8.0 volumes: - mysql-data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: ${MYSQL_DATABASE} MYSQL_USER: ${MYSQL_USER} MYSQL_PASSWORD: ${MYSQL_PASSWORD} healthcheck: test: [CMD, mysqladmin, ping, -h, localhost] interval: 5s timeout: 3s retries: 10 redis: image: redis:7-alpine volumes: - redis-data:/data healthcheck: test: [CMD, redis-cli, ping] interval: 5s timeout: 3s retries: 5 rabbitmq: image: rabbitmq:3.12-management hostname: rabbitmq environment: RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER} RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS} volumes: - rabbitmq-data:/var/lib/rabbitmq healthcheck: test: [CMD, rabbitmq-diagnostics, ping] interval: 5s timeout: 3s retries: 10 volumes: mysql-data: redis-data: rabbitmq-data:几个注意点worker和api用同一个镜像但通过command覆盖启动命令省去再写一个 Dockerfile 的麻烦。RabbitMQ 需要设置hostname否则集群模式下节点名会随机变化容易出问题。MySQL 初次启动会执行初始化脚本耗时较长健康检查的retries一定要给足。所有敏感值全部通过 .env 注入docker-compose.yml 里不出现明文密码。这套组合能覆盖大多数中小型项目的部署需求。如果你要部署的应用本身就带回调、队列、缓存这类机制这个模板基本可以直接改改用。5. 常见问题与排查技巧实录5.1 cannot connect to the docker daemon 是怎么回事这个报错信息几乎每个 Docker 用户都见过Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?遇到这个报错先别慌按顺序排查第一确认 Docker 服务是否在运行。Linux 上执行systemctl status docker如果没在运行启动它sudo systemctl start docker如果你用的是 CentOS 或某些最小化安装的系统Docker 服务默认不会开机自启记得执行sudo systemctl enable docker。第二检查当前用户是否有权限访问 Docker socket。如果是刚装完 Docker你的用户可能不在docker用户组里普通用户执行 docker 命令就会报这个错。解决办法sudo usermod -aG docker $USER然后退出重新登录终端让组权限生效。第三比较隐蔽的是 Docker daemon 启动失败。这种情况需要看 daemon 的日志journalctl -u docker --since today常见原因包括daemon.json 配置了无效的镜像加速地址、磁盘空间不足、iptables 规则被其他软件覆盖等。我在一台服务器上遇到过因为磁盘写满导致 docker daemon 起不来的情况清理完/var/lib/docker下的日志和悬空镜像之后才恢复正常。5.2 镜像拉取慢或失败怎么处理国内服务器拉取 Docker Hub 镜像经常遇到超时或速度极慢的情况。最直接的办法是配置镜像加速器修改/etc/docker/daemon.json{ registry-mirrors: [ https://docker.mirrors.ustc.edu.cn ] }改完之后重启 Dockersudo systemctl restart docker注意不同加速器的可用性和速度会有波动建议多试几个留下实际速度最快的一个。如果你在公司内网可能还有内网私有的镜像仓库配置方式是在镜像地址前加上仓库地址比如registry.example.com/library/redis:7-alpine。另一个经验是如果 push 或 pull 超时可以设置更长的超时时间但这只能在 Docker 客户端的配置里部分调整总体而言还是加速器更直接。5.3 端口冲突、容器启动顺序、Compose 命令版本差异端口冲突是非常经典的问题。启动时报这个错Error starting userland proxy: listen tcp4 0.0.0.0:8080: bind: address already in use说明 8080 端口已经被占用了。先用下面的命令看是谁占了ss -lntp | grep 8080找到占用进程后要么停掉它要么把 Compose 里的宿主机端口改掉。容器启动顺序的问题前面提过再强调一遍depends_on只是启动顺序不是就绪等待。如果确认 order 没问题但还是连不上优先检查依赖服务的健康检查是否真的通过了。可以用docker compose ps看健康状态那一列是不是healthy。还有个很容易踩的坑是命令版本差异。老版本的 Compose 命令是docker-compose带横线新版本是docker compose带空格。如果你的系统里只装了旧版或者只装了新版命令就用对。查看版本docker compose version如果提示找不到这个命令说明你的 Docker 版本太老或者没有安装 Compose 插件。CentOS 上可以用sudo yum install -y docker-compose-pluginUbuntu/Debian 上通常安装docker-compose-v2包或者直接用 Docker 官方脚本安装完 Docker 后就自带 Compose v2。5.4 从日志快速定位问题的三板斧容器起不来的时候第一步永远是看日志而不是反复重启docker compose logs --tail100--tail100表示只看最后 100 行。如果日志显示某个服务连接不上比如redis.exceptions.ConnectionError说明可能是 Redis 没起来或者网络不通。这时再看docker compose ps确认所有容器的状态。如果有容器显示 Restarting多半是启动后马上崩了日志里会有具体原因。有时候单看容器日志不够还需要看容器的详细配置docker compose exec web env这个命令可以直接在运行中的容器里执行环境变量查看确认环境配置对不对。更多时候需要直接进容器里去手动试docker compose exec redis redis-cli ping如果返回 PONG说明 Redis 正常。再去 Web 容器里试试网络连通性docker compose exec web ping redis如果 ping 不通优先检查是不是自定义网络配置有问题或者容器没有加入同一个网络。6. 一点个人体会Docker Compose 用到现在最让我省心的不是省了那几条命令而是环境的确定性。以前项目换人维护光是把开发环境跑起来就要折腾半天大家装的依赖版本还不一样问题多到怀疑人生。现在拿到项目第一件事就是docker compose up -d整套环境一拉就起来团队成员之间的结构差异几乎为零。最后分享一个小实践我会把docker compose down docker compose up -d --build写成一个部署脚本配合代码仓库里的版本标签实现一键发布。每次发布前先docker compose pull拉取新镜像再up -d重建容器整个过程不超过 30 秒。这套流程简单可靠比很多复杂的发布系统都顺手。Compose 的上手门槛不高但能玩出的深度不低。建议你先从两三个服务的组合开始跑通之后再加健康检查、资源限制、日志轮转这些生产必备配置。等哪天你发现自己已经在用脚本管理十几条 docker run 的时候回头看这份 docker-compose.yml会觉得当初的迁移决定做得太值了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门