ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Docker Compose 实战指南:从多容器编排到微服务环境一键部署

Docker Compose 实战指南:从多容器编排到微服务环境一键部署 先声明一下这确实是系列的第二篇。上一篇我们把 Docker 镜像、容器、数据卷这些基础概念过了一遍也用docker run手动拉起来过几个容器。但如果你稍微认真一点就会发现一个问题当你的项目不是一个孤零零的进程而是一堆服务互相配合的时候靠一个个敲命令的方式去管理容器基本就是给自己找罪受。这篇要讲的 Docker Compose就是来干这个的。它可以把你整套“微服务小队”——前端、后端、MySQL、Redis、消息队列——写进一个 YAML 文件里用一条命令全部启动用一条命令全部停止配置都放在代码里换台机器也能一行命令复现整套环境。这篇文章会从“为什么需要编排”讲起然后拆解 compose 文件的核心字段最后用一套真实的微服务示例带你照着抄一遍。看完你就能明白这东西到底是怎么把乱糟糟的多容器管理变得有条理的以及它和 K8s 这类编排工具的边界到底在哪。1. 为什么需要编排Compose 实际解决了什么问题1.1 从“手动启停”到“声明式编排”的演进先回想一下没有 Compose 的日子。假设你的项目有五个服务网关、用户服务、订单服务、MySQL、Redis。启动的时候你要做这些事先启动 MySQL 和 Redis命令得带数据卷、端口映射、环境变量等 MySQL 初始化好了再启动用户服务和订单服务环境变量里要写对数据库地址、Redis 地址最后启动网关配置要指对后端服务的地址。而且这些服务之间还有依赖关系——后端服务启动时如果连不上数据库很可能直接崩溃退出重启数据库后又得手动把后端服务再拉起来。如果哪天换了台电脑或者新同事加入项目光是把这些命令整理出来就可能花掉半天时间。这就是手动管理的第一个痛点启动顺序靠人脑记忆环境配置靠复制粘贴容器之间的网络打通靠手动创建自定义网络否则容器名根本解析不了。这不是“能用就行”的问题而是这套流程根本没法可靠复制。Compose 的解法非常简单粗暴把你所有想启动的服务、它们各自的镜像/构建方式、端口映射、环境变量、依赖关系、数据卷、网络配置全部写进一个docker-compose.yml文件里。以后启动整套路环境只需要敲一条命令docker compose up -d这个文件本身可以提交到 Git 仓库里任何一个人拉下来执行这一条命令就能得到和你在本地完全一致的整套环境。这不是抽象的概念而是“声明式管理”的核心思路你告诉 Compose“最终我要什么样的状态”它负责把当前状态调整到这个目标状态。就像装修房子——你不是每天告诉工人“这里砌砖、那里刷墙”而是给一张图纸工人按图施工。Compose 文件就是你这套环境的“图纸”。1.2 Compose 与 K8s 的边界什么时候选谁很多人听到“容器编排”四个字第一反应是 K8sKubernetes。但实际上Compose 和 K8s 解决的完全不是一个量级的问题也不能简单地说谁比谁强。我个人的划分标准是这样的如果你的所有容器都在一台机器上用 Compose 就足够了。日常开发环境、测试环境、个人项目的生产环境、小团队的内部工具单机部署完全够用。Compose 提供了服务编排、网络互通、数据卷管理、健康检查、滚动重启这些核心能力对单机场景来说已经非常完整。一旦你的服务需要跨多台机器部署或者需要自动扩容、自愈、滚动升级、服务发现、负载均衡这些分布式能力才是 K8s 的战场。举个例子你把整套微服务部署在一台 4 核 8G 的服务器上用户量也就几百人这时候上 K8s 纯属给自己找不痛快——光维护控制面组件就够头疼了。但如果你的服务要部署到几十台机器上手动管理容器就彻底失控了这时候才是 K8s 发力的时候。所以我给团队的建议一直是先把 Compose 玩透K8s 的很多概念你在 Compose 里其实已经见过了——服务service、网络network、健康检查healthcheck、配置environment、数据卷volume这些词到了 K8s 里只是换了个形式继续存在。Compose 是理解容器编排的最好起点而不是一个“不够格的玩具”。1.3 一套“微服务小队”的完整构成在动手写 Compose 文件之前先明确一下我们要编排的“小队”到底有哪些成员。以一套典型的微服务架构为例不管你是 Java、Go、Node.js 还是 Python 写的思路都一样接入层Nginx 或 Spring Cloud Gateway 之类的网关负责统一入口、路由转发、鉴权业务服务用户服务、订单服务、商品服务等一般按业务模块拆分成独立的服务进程基础组件MySQL数据持久化、Redis缓存、RabbitMQ/Kafka消息队列、Nacos/Consul注册发现前端静态资源如果你有 Web 页面Nginx 还需要托管打包好的静态文件。没有 Compose 的时候你要自己保证这些服务的启动顺序——基础设施先起、业务服务后起、网关最后起而且每个服务都要配一堆环境变量。有了 Compose你可以把整支“队伍”的编组方式写进一个文件让工具替你保证顺序、网络、配置这三件最琐碎的事。2. 编写第一个 Compose 文件核心字段逐项拆解2.1 services一个服务一棵“进程树”Compose 文件的最顶层有三个核心配置块services、networks、volumes。其中services是绝对的主角你定义每一个容器服务都在services下面占一个位置。services: mysql: image: mysql:8.0 redis: image: redis:7-alpine user-service: build: ./services/user-service这里的mysql、redis、user-service就是服务的名字这个名字有几个关键作用在 Compose 自动创建的网络里它就是这个容器的主机名其他服务可以直接用mysql:3306来访问它它是docker compose ps、docker compose logs这些命令的操作对象它是你在配置依赖关系时引用的名称。这一点非常关键服务名就是容器在 Compose 网络里的 DNS 名称。也就是说你的 Java 服务配置数据库连接地址时直接写jdbc:mysql://mysql:3306/yourdb就可以了不需要去查 MySQL 容器的 IP 地址也不需要把你的数据库地址暴露到宿主机网络上再绕回来。容器间的通信走的是 Docker 内部的网络直连、高效、配置简单。2.2 image 与 build镜像从哪来每个服务要运行必须有一个镜像。Compose 给了你两种选择services: mysql: image: mysql:8.0 # 直接拉取已有镜像 user-service: build: context: ./services/user-service # 指定构建上下文目录 dockerfile: Dockerfile # 指定 Dockerfile 文件名 image: myapp/user-service:latest # 构建成功后打成的镜像名用image字段适合 MySQL、Redis、Nginx 这些现成的组件直接指定版本号即可。用build字段适合你自己的业务代码——Compose 会在你执行up时先用 Dockerfile 把代码构建成镜像再启动容器。这里有一个容易踩的坑build.context指的是构建上下文目录也就是 Dockerfile 所在的目录以及 COPY 指令能访问到的目录范围。如果你的服务代码不在项目根目录一定要把 context 指对。比如你的目录结构是./backend/user-service那 context 就要写成./backend/user-service而不是./。否则 Dockerfile 里的COPY . /app会找不到文件。2.3 ports 与 expose端口映射的两种姿势多容器环境下端口管理是最容易乱的环节。Compose 区分了两个概念services: nginx-gateway: ports: - 8080:80 # 宿主机 8080 映射到容器 80 user-service: expose: - 8081 # 仅容器间可访问不映射到宿主机ports的作用是把容器的端口暴露到宿主机上外部访问者可以通过宿主机IP:8080访问到容器里的 Nginx。这种映射有两个注意点宿主机端口不能冲突。如果你启动了两个容器一个映射8080:80另一个也映射8080:80后启动的会直接失败报错 “port is already allocated”。我在实操中见过很多次这种低级错误排查方法直接看docker compose ps里的端口列或者用netstat -tlnp | grep 8080看看谁占用了。如果你只是需要容器间互相访问不需要对外暴露就只写expose别写ports。业务服务之间的调用根本不需要映射到宿主机——它们在同一个 Docker 网络里可以通过服务名直接访问。把端口映射出去反而增加暴露面属于很不安全的行为。比如你的 MySQL 只给后端服务用就完全没必要把 3306 暴露到公网。这个原则很多人一开始没意识到等被人扫到端口爆破数据库的时候就晚了。2.4 environment 与 env_file环境配置的两种推荐方式容器里跑的应用一般通过环境变量读取配置——数据库地址、用户名密码、Redis 地址、各种开关。Compose 里传环境变量有两种方式services: user-service: environment: - DB_HOSTmysql - DB_PORT3306 - DB_PASSWORDyourpassword order-service: env_file: - ./config/order-service.envenvironment直接写在 YAML 里适合少量配置或者配置数量少、不需要经常修改的场景。env_file引用外部文件适合配置项多、希望统一管理的场景。两种方式可以混用但有个优先级规则需要记住environment里显式设置的变量会覆盖env_file里的同名变量。这里我要分享一个实际项目的管理建议不要在 Compose 文件里写明文密码和环境差异配置。推荐的做法是准备一个.env文件这个文件加入.gitignore不进版本库里面放一份默认配置然后 Compose 文件里用${VAR_NAME}引用。Compose 会自动读取项目目录下名为.env的文件来做变量替换。services: mysql: image: mysql:8.0 environment: - MYSQL_ROOT_PASSWORD${DB_ROOT_PASSWORD}这样你的docker-compose.yml可以提交到 Git敏感信息留在本地.env里。不同环境开发、测试、生产只要换.env文件不用改 Compose 主体内容。2.5 depends_on 与 healthcheck解决“服务假启动”问题微服务编排里最烦人的问题之一就是启动顺序。用户服务要连接 MySQL但 MySQL 容器启动到真正可以接受连接中间可能隔着几秒到几十秒的初始化时间。如果你让用户服务在 MySQL“容器起来了但还没就绪”的时候就尝试连接大概率会报Connection refused然后整个服务崩溃退出。很多人第一次会想到用depends_onservices: user-service: depends_on: - mysql但这里有一个巨大的坑默认情况下depends_on只保证 MySQL 容器已经创建并启动不保证 MySQL 进程已经准备好接收连接。也就是说它只能解决“启动顺序”问题解决不了“服务就绪”问题。真正可靠的解法是组合拳depends_on healthcheck condition。services: mysql: image: mysql:8.0 healthcheck: test: [CMD, mysqladmin, ping, -h, localhost] interval: 5s timeout: 3s retries: 10 user-service: build: ./services/user-service depends_on: mysql: condition: service_healthyhealthcheck让 Compose 定期检查 MySQL 容器的健康状态直到它健康了才启动user-service。这个写法在我维护的所有 Compose 项目里几乎是必备的因为“容器起来了”和“服务可用了”之间隔着巨大的鸿沟。如果你图省事只写depends_on不加健康检查在第一次启动时大概率会遇到随机性的连接失败而且这种问题特别难排查——因为第二次启动可能就好了。2.6 volumes 与 networks数据持久化和服务发现名的关键理解最后说两个容易忽略但特别基础的概念。volumes数据卷容器是无状态的容器一删除容器里写的数据就没了。MySQL 的数据、上传的图片、日志文件这些都是需要持久化的数据。Compose 里把数据卷挂载到容器的指定目录services: mysql: image: mysql:8.0 volumes: - mysql-data:/var/lib/mysql - ./mysql-init:/docker-entrypoint-initdb.d:ro volumes: mysql-data:第一种mysql-data:/var/lib/mysql是命名卷由 Docker 管理数据存放在 Docker 的数据目录里这种方式数据生命周期和 Compose 项目绑定执行down不加-v时数据不会丢。第二种./mysql-init:/docker-entrypoint-initdb.d:ro是绑定挂载把宿主机目录直接映射进容器适合放初始化 SQL 脚本——首次启动时 MySQL 镜像会自动执行这个目录下的.sql文件这在搭建一套带初始数据的测试环境时非常实用。这里有一个必须要提醒的细节MySQL 的常见镜像对挂载目录的权限很敏感。如果你在挂载宿主机目录时遇到 “Permission denied” 之类的报错通常是因为目录的所有者 UID 和容器内进程的 UID 不一致。最简单的方法是用chown把目录所有权改成对应 UID或者换用命名卷让 Docker 自动处理。这种问题在 macOS 上少一些在 Linux 服务器上非常常见。networks网络Compose 默认会为项目创建一个网络所有服务都在这个网络里可以直接通过服务名互相访问。如果你想手动控制网络划分可以把服务划分到不同网络里实现网络层面的隔离services: user-service: networks: - backend mysql: networks: - backend nginx-gateway: networks: - backend - frontend networks: backend: frontend:不过对大多数小型项目来说使用 Compose 自动创建的默认网络就够了不需要手动定义。只有在类似“网关能访问业务服务但数据库不能对外”这种隔离场景下手动划分网络才真正派上用场。3. 实操用 Compose 拉起一套微服务示例环境3.1 准备项目骨架网关 用户服务 订单服务 MySQL Redis理论讲再多不如直接实操一套。我这里的示例就以“若依微服务”那种典型的后台管理系统为蓝本但去掉了具体的业务代码只留下结构和配置。目标是用 Compose 一键拉起一套包含五个成员的环境nginx-gatewayNginx统一入口网上方服务路由同时托管前端静态页面user-service用户服务写个最简单的 HTTP 服务查询用户信息数据从 MySQL 读取order-service订单服务写个最简单的 HTTP 服务查询订单信息依赖 Redis 做缓存mysqlMySQL 8.0存数据redisRedis 7做缓存。目录结构如下compose-demo/ ├── docker-compose.yml ├── .env ├── nginx/ │ ├── nginx.conf │ └── html/ │ └── index.html ├── user-service/ │ ├── Dockerfile │ └── app.py ├── order-service/ │ ├── Dockerfile │ └── app.py └── mysql-init/ └── init.sql这里我用 Python 的 Flask 来模拟两个后端服务纯粹是为了演示方便你的实际项目换成任何语言都行——反正最终都是打成镜像一个查 MySQL一个查 Redis。3.2 完整 docker-compose.yml 示例可直接套用下面是我在实际项目里非常常用的一个 Compose 文件模板你做微服务项目时完全可以照这个结构去改services: mysql: image: mysql:8.0 container_name: demo-mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} MYSQL_DATABASE: demo MYSQL_USER: demo MYSQL_PASSWORD: ${DB_PASSWORD} volumes: - mysql-data:/var/lib/mysql - ./mysql-init:/docker-entrypoint-initdb.d:ro healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -p${DB_ROOT_PASSWORD}] interval: 5s timeout: 3s retries: 10 redis: image: redis:7-alpine container_name: demo-redis restart: unless-stopped healthcheck: test: [CMD, redis-cli, ping] interval: 5s timeout: 3s retries: 10 user-service: build: ./user-service container_name: demo-user-service restart: unless-stopped environment: DB_HOST: mysql DB_PORT: 3306 DB_NAME: demo DB_USER: demo DB_PASSWORD: ${DB_PASSWORD} depends_on: mysql: condition: service_healthy order-service: build: ./order-service container_name: demo-order-service restart: unless-stopped environment: REDIS_HOST: redis REDIS_PORT: 6379 depends_on: redis: condition: service_healthy nginx-gateway: image: nginx:1.27-alpine container_name: demo-nginx restart: unless-stopped ports: - 8080:80 volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/html:/usr/share/nginx/html:ro depends_on: - user-service - order-service volumes: mysql-data:同时准备一个.env文件不要提交到 GitDB_ROOT_PASSWORDroot123456 DB_PASSWORDdemo123456这里面有几个配置值得单独讲一下restart: unless-stopped是我在几乎所有服务里都会加的。它的意思是如果容器非正常退出Docker 自动重启它只有你手动docker compose stop时才保持停止。这在服务器上非常实用——防止因为某些临时性错误比如内存不足导致进程被杀导致服务长时间离线。不加这个服务死了可不会自己活过来。MySQL 健康检查里我传了-p${DB_ROOT_PASSWORD}这是因为mysqladmin ping在 MySQL 8.0 里需要密码才能返回正确结果。如果你没传密码它可能会返回 “Access denied”健康检查会误判服务不健康。这个坑我在早期踩过写出来给你们避雷。container_name不是必须的。不写的话Compose 会自动生成项目名-服务名-序号这样的容器名。我写了是因为演示环境里固定名字更方便排查但如果你要扩展成多副本就别写container_name——容器名必须唯一写死了反而没法扩展。3.3 启动流程与常用命令up 到 down 的完整闭环现在到了最激动人心的环节。在项目根目录执行docker compose up -d-d是后台运行不加的话日志会直接刷满终端。第一次执行时Compose 会先构建user-service和order-service的镜像如果 Dockerfile 里写了然后拉取 MySQL、Redis、Nginx 的镜像接着按依赖关系启动容器。启动过程你可以用docker compose ps观察服务状态NAME IMAGE STATUS demo-mysql mysql:8.0 Up (healthy) demo-redis redis:7-alpine Up (healthy) demo-user-service compose-demo-user-service Up demo-order-service compose-demo-order-service Up demo-nginx nginx:1.27-alpine Up如果某个服务的状态不是Up (healthy)而是Up (unhealthy)或者直接Exit那就是启动出问题了。排查的第一件事永远是看日志docker compose logs user-servicelogs命令可以指定服务名看单个服务的日志不加-f是输出最近的日志加了-f会跟着日志实时滚动。我的习惯是先看退出的服务的日志再沿着依赖链一步步往前查——比如 user-service 退出了先查 user-service 日志看到底是连不上数据库还是代码报错再回去看 mysql 是否真正健康。整套环境验证没问题后日常最常用的命令就是这几个命令作用docker compose up -d启动所有服务会先构建/拉取缺失的镜像docker compose ps查看全部服务状态docker compose logs -f user-service实时跟踪某个服务的日志docker compose restart user-service重启某个服务docker compose stop停止所有服务不删除容器docker compose start重新启动所有服务docker compose down停止并删除所有容器和网络docker compose down -v停止并删除容器、网络同时删除命名卷注意数据会没这里要特别提醒down和down -v之间就差一个-v但差别是天壤之别。down只是把你启动的容器和默认网络清理掉mysql-data这个命名卷里的数据还在下次up起来数据照旧down -v会连卷一起删MySQL 里的数据就彻底没了。如果你只是暂时停一下环境千万别加-v。我见过有人想在服务器上“清理一下环境”执行完down -v一整年的测试数据直接消失。血泪教训。3.4 横向扩展与滚动更新模拟真实微服务的伸缩场景Compose 虽然偏单机但提供了最基本的横向扩展能力——--scale参数。比如你想启动三个用户服务实例分担请求压力docker compose up -d --scale user-service3执行后docker compose ps会看到三个 user-service 实例容器名是demo-user-service-1、demo-user-service-2、demo-user-service-3这样。Nginx 网关里的upstream会自动通过服务名解析到所有实例实现简单的负载均衡。但这里有一个我必须说清楚的限定Compose 的--scale只是把容器数量复制多份它默认不做真正的负载均衡和自动注册发现。如果你的业务服务启动时会开启固定端口并依赖此端口对外通信多个副本很容易端口冲突。解决这个问题的常见做法是让服务监听 0.0.0.0 的随机端口由 Docker 网络内 DNS 解析分配或者用 Nginx 的upstream配置指向user-service:端口来自动发现多实例。如果你的服务还用了 Nacos/Consul 这类注册中心那多实例的注册发现就是它们来管了。另外--scale和ports字段有冲突——如果你在服务配置里固定映射了宿主机端口比如ports: - 8081:8081那扩展到多副本时第二个副本会因为宿主机 8081 端口被占用而启动失败。所以要伸缩的服务别在 compose 里写固定ports映射换成expose。关于更新Compose 的常规做法是docker compose up -d——它会自动检测到某个服务的镜像发生改变比如你重新docker compose build user-service构建了新镜像然后对这个服务做滚动重建先停旧容器启新容器。如果你想要更严格的滚动更新逐个替换、保持服务不中断Compose 也有局限那一刻才是真正需要 K8s 的时候。但对个人项目和中小团队Compose 的更新流程已经足够顺滑。4. 常见问题与排查技巧实录4.1 端口被占用/映射失效这是我被咨询得最多的问题之一。现象是docker compose up执行到一半报错Error starting userland proxy: listen tcp4 0.0.0.0:8080: bind: address already in use不用猜肯定是宿主机 8080 端口被其他进程占了。排查命令netstat -tlnp | grep 8080 # 或者 lsof -i :8080找到占用进程之后要么停掉那个进程要么把 Compose 文件里的宿主机端口的映射改掉。这里我额外提一个不太容易被发现的情况很多时候不是你自己的进程占用了端口而是另一个 Compose 项目或者一个没清理的容器占用了。用docker ps -a看一下是否有旧容器还在运行顺手docker rm -f 容器名清理掉。4.2 容器间连不上network 与 localhost 误区后端服务里写的是jdbc:mysql://localhost:3306/demo结果在容器里运行时报Connection refused。这是新手最容易犯的错因为localhost在容器里指的是容器自己不是宿主机更不是 MySQL 容器。正确写法是同一 Compose 网络内的服务之间用服务名当主机名。也就是jdbc:mysql://mysql:3306/demo。这里的mysql就是 compose 文件里那个服务名。如果你确实需要从容器访问宿主机上的某个端口用host.docker.internalDocker Desktop 和较新版本的 Linux Docker 都支持这个特殊域名。但更好的做法是把需要访问的东西也容器化一起纳入 Compose 网络里用服务名访问。毕竟你都在用 Compose 了没必要搞特殊通道。4.3 depends_on 导致服务启动顺序仍不对现象user-service的日志显示Communications link failure明显是数据库没就绪它就尝试连接了。虽然你写了depends_on: - mysql。原因我在前面 2.5 节已经讲透了默认depends_on不等待服务就绪只等待容器启动。解决办法就是给 MySQL 加healthcheck然后在depends_on里用condition: service_healthy。如果你的数据库是其他类型PostgreSQL、MongoDB思路一样——先配置健康检查再在依赖方等待健康状态。另一个容易忽略的点是depends_on只对up生效对restart不生效。也就是说如果你手动docker compose restart mysql依赖它的服务不会自动跟着重启。解决方案是docker compose restart user-service手动重启依赖方或者直接docker compose up -d让 Compose 自动处理依赖关系。4.4 数据卷权限与文件挂载不生效Linux 服务器上常见两个问题第一个是绑定挂载的宿主机目录权限不对。比如你把./mysql-init挂载进 MySQL 容器但目录权限是 755、所有者是 root容器内 mysql 用户写不进去导致初始化脚本报错。排查方法很直接ls -ln ./mysql-init然后chown -R 999:999 ./mysql-init999 是 MySQL 容器内用户 UID不同的镜像可能不一样。不过初始化脚本是只读挂载:ro一般不太会遇到写权限问题但数据目录mysql-data这种命名卷Docker 会自动处理权限相对省心。第二个是修改了宿主机文件但容器里没变化。比如改了nginx.conf刷新页面还是旧的。这是因为 Nginx 正则读取配置文件但有些版本的 nginx 容器在启动时缓存配置改完需要docker compose exec nginx nginx -s reload重新加载或者直接docker compose restart nginx。同理挂载的静态文件如果浏览器缓存严重也可能看着像是“没生效”实际上文件已经更新了只是浏览器的 Cache 还挂着旧版本。4.5 docker compose 命令找不到/版本差异如果你在比较老的环境里还会遇到docker-compose带横杠和docker compose带空格两种写法并存的情况。前者是 Python 写的旧版工具需要单独安装后者是 Docker 官方集成的插件从 Docker Engine 20.10 之后默认自带。我的建议是无脑用docker compose带空格它是官方在新版本里的主力方向命令更简洁还多了docker compose watch这种新功能。如果你的机器提示找不到很可能是 Docker Engine 版本太老升级到较新的稳定版就行。查看版本docker compose version顺便提一个版本差异的坑旧版docker-compose.yml会在开头写version: 3.8这种版本号。新版的 Docker Compose V2 已经彻底忽略了这个字段写了也不报错但不推荐。我的建议是干脆别写version因为 Compose 版本和 Docker Engine 的兼容性逻辑已经不需要这个字段了。如果你在网上看的教程里写了version: 3别太在意不碍事。4.6 镜像拉取慢与构建缓存问题国内访问 Docker Hub 经常慢得让人抓狂这是大家都会遇到的问题。比较务实的解决方案是给 Docker 配置镜像加速器。打开 Docker 的 daemon 配置文件Linux 在/etc/docker/daemon.jsonDocker Desktop 在设置面板里把 registry-mirrors 配上即可{ registry-mirrors: [ https://docker.m.daocloud.io ] }配置完后记得systemctl restart dockerLinux或重启 Docker Desktop。构建缓存的问题是另外一回事你改了代码重新docker compose build user-service时发现镜像构建特别慢甚至用了旧代码。这通常是 Dockerfile 的层缓存没打中。最常见的错误是把COPY . .写在RUN之前——这样只要任何代码文件变了后面的RUN步骤全部缓存失效。优化思路是把“不常变的层”放前面。比如先COPY requirements.txt依赖清单再RUN pip install安装依赖最后才COPY . .拷贝代码。因为依赖文件不常变所以大多数时候构建能直接命中RUN pip install的缓存层这一步能节省大量时间。下面是个优化过的 Python 服务 Dockerfile 片段FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [python, app.py]5. 进阶技巧与个人体会5.1 用 profiles 拆分开发/生产环境一个常见需求是开发环境要启动 MySQL、Redis 这些基础组件但不需要启动生产环境才用的监控服务比如 Prometheus、Grafana。如果你用一个 Compose 文件管理所有环境可以通过profiles给服务分组services: mysql: image: mysql:8.0 # 默认启动 prometheus: image: prom/prometheus profiles: [monitoring] # 只有显式指定该 profile 时才启动启动开发环境docker compose up -d # 不启动 prometheus启动带监控的完整环境docker compose --profile monitoring up -d这个功能比维护两份 compose 文件优雅很多。同一份配置进 Git不同环境用不同的启动参数减少维护成本。5.2 用 extends 复用公共配置如果你有多个业务服务它们的配置几乎一样都用同一个 Dockerfile 模板、都依赖同一个数据库、都设置了同样的 restart 策略可以用extends减少重复services: user-service: extends: file: common.yml service: backend-common environment: SERVICE_NAME: user-service order-service: extends: file: common.yml service: backend-common environment: SERVICE_NAME: order-servicecommon.yml里定义了公共配置services: backend-common: build: context: . dockerfile: Dockerfile restart: unless-stopped depends_on: mysql: condition: service_healthy但这里也要提醒一句extends的能力有限而且会降低配置文件的可读性——别人看docker-compose.yml时还得去翻common.yml才能完全明白某个服务的完整配置。我的建议是服务数量超过三四个、且业务服务配置高度相似时可以考虑用extends如果你对 Compose 还不熟悉宁可重复写几遍配置也别过早抽象。配置文件的“过早抽象”和代码里一样有害。5.3 用 watch 实现代码热更新这是 Docker Compose V2 里我个人特别喜欢的功能。传统的开发流程是改代码 →docker compose build→docker compose up -d每改一次就重新构建一遍镜像十几秒甚至几分钟就没了。Compose 的watch功能可以监听宿主机文件变化自动同步到容器里并热重启先在 compose 文件里给服务加develop配置services: user-service: build: ./user-service develop: watch: - path: ./user-service action: sync target: /app - path: ./user-service/requirements.txt action: rebuild然后改用这个命令启动docker compose watchaction: sync表示文件改动时直接把文件同步到容器无需重建镜像action: rebuild表示改动时触发镜像重新构建。这个功能最妙的地方是它把“代码编辑—容器感知—自动生效”这个循环变得几乎无感极大地提升了本地开发体验。不过要注意docker compose watch是前台命令会一直挂着所以它一般用于本地开发终端不适合在服务器上跑。生产环境你不需要 watch正常docker compose up -d就够了。5.4 最后说点个人体会我见过太多人一上来就学 K8s结果被一堆概念劝退很受挫。实际上 Compose 才是容器编排真正的“小学教材”而且是那种把知识点揉碎了喂到你嘴边的教材。你在 Compose 里学会的服务、网络、健康检查、数据卷、滚动更新到 K8s 里全部摇身一变成为 Pod、Service、Deployment、Ingress 这些概念但底层的思维模型是相通的。还有一点我特别想对团队管理者说不要把 Compose 局限在开发环境。对于很多中小型项目单台服务器上的生产环境用 Compose 完全足够而且维护成本比 K8s 低一个数量级。你在 Compose 里定义重启策略、健康检查、日志收集已经能给服务提供不错的基本保障。等用户量和机器数真的涨上去了再从 Compose 平滑迁移到 K8s——那时候你已经对“编排”这件事有了直觉不会迷失在一堆新概念里。最后再分享一个实用小技巧在docker compose后面加--env-file参数可以指定不同的.env文件比如你有.env.dev和.env.prod两套配置就分别用docker compose --env-file .env.dev up -d docker compose --env-file .env.prod up -d这样同一个 compose 文件可以管理多套环境的配置。配合profiles使用一个文件管天下我实测下来非常稳。
返回列表