1. 别被“容器”吓住:我带第一批学员从零跑通第一个Docker镜像的真实过程
很多人第一次看到“Docker”三个字,下意识就点开教程准备背命令——结果三分钟之后卡在docker build -t myapp .这行,终端报错no such file or directory: Dockerfile,人直接懵了。这不是你手速慢,也不是记性差,而是绝大多数入门资料从一开始就埋了个坑:它默认你已经理解“为什么非得用Docker”,而没告诉你——Docker本质上不是新工具,它是对“环境一致性”这个老问题的一次外科手术式解决。
我带过几十期不同背景的学员,有刚毕业的应届生、转行做后端的设计师、还有某高校实验室里连Linux基础命令都要查手册的研究生。他们共同的卡点从来不是docker run语法,而是搞不清“我到底在解决什么问题”。比如A同学,本地写好Python脚本,测试全过,一交给导师部署到服务器就报ModuleNotFoundError: No module named 'pandas';B同学更典型,开发时用Node.js v18.16,运维给的生产机只有v14.21,接口突然返回空数组,查了两天才发现是Object.hasOwn()在低版本不支持。这些都不是代码bug,是环境漂移(Environment Drift)——同一份代码,在不同机器上因依赖、版本、路径、权限等细微差异,行为完全不可预测。
Docker要干的事,就是把“代码+所有依赖+运行时环境”打包成一个自包含的、可移植的单元,叫镜像(Image)。它不修改宿主机系统,也不要求你全局安装一堆包,而是启动时拉起一个轻量级隔离空间(容器),里面只放你明确声明需要的东西。就像你寄快递,不会把整个书房打包发走,而是把书、笔、台灯、充电线放进一个定制纸箱,贴上“请勿倒置”标签——Docker镜像就是这个纸箱,docker run就是快递员按单发货。
关键词里虽然没填,但实际场景中高频出现的词是:环境一致、快速复现、团队协作、CI/CD前置条件。这不是为炫技学的,而是当你第一次需要把本地调试好的服务,原封不动搬到测试服务器、交付给客户、或者让同事一键复现你的实验环境时,它就成了刚需。我见过最真实的案例:某跨平台图像处理Demo,开发用macOS,测试用Ubuntu,客户验收用CentOS,三方环境差异导致色彩校准模块输出偏差0.3%,最后靠Docker统一基础镜像(python:3.9-slim)+固定OpenCV版本(opencv-python==4.8.1.78),三天内闭环。
所以别急着敲docker pull ubuntu。先问自己三个问题:
- 我当前项目最常遇到的“换环境就崩”问题,具体是哪类?(Python包冲突?Java版本不匹配?Nginx配置路径差异?)
- 这个问题如果不用Docker,我通常怎么临时解决?(手动改配置?重装依赖?截图求同事帮忙?)
- 如果有一个“环境快照”能一键还原,我最希望它包含哪些东西?(是整个IDE?还是仅
src/目录+requirements.txt+nginx.conf?)
答案会帮你锚定学习重心——Docker不是万能胶,它的价值永远绑定在你正在解决的具体问题上。接下来,我们就从一个真实可运行的最小闭环开始:不碰网络、不配数据库、不写Dockerfile,就用官方镜像跑通一个静态网页,亲眼看到“环境封装”如何生效。
2. 从hello world到my-first-site:用三步验证Docker的核心承诺
很多教程一上来就让你写Dockerfile,结果学员对着FROM、COPY、CMD发呆,不知道每行在解决什么。我们反其道而行:先跳过构建,直接用现成镜像跑一个最简单的服务,亲手验证Docker是否真能“隔绝环境差异”。目标很明确——在你的电脑上,不装任何Web服务器,只靠一条命令,让一个HTML文件通过浏览器访问。
2.1 第一步:准备一个“裸”HTML文件(5秒)
新建一个文件夹,比如my-first-site,在里面创建index.html:
<!DOCTYPE html> <html> <head> <title>My First Docker Site</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; text-align: center; margin-top: 2rem; } .container { max-width: 600px; margin: 0 auto; padding: 2rem; background: #f8f9fa; border-radius: 8px; } </style> </head> <body> <div class="container"> <h1>✅ Docker is working!</h1> <p>This page is served by nginx inside a container.</p> <p><strong>Current time:</strong> <span id="time"></span></p> </div> <script> document.getElementById('time').textContent = new Date().toLocaleString(); </script> </body> </html>注意:这个文件里不要安装nginx、不要配置Apache、不要改hosts。它就是一个纯文本文件,和你双击用浏览器打开的效果一样。
2.2 第二步:用官方nginx镜像“借壳”运行(30秒)
打开终端,进入my-first-site目录,执行:
docker run --rm -d -p 8080:80 -v $(pwd):/usr/share/nginx/html nginx:alpine拆解这条命令的每个参数,它直指Docker最核心的三个能力:
--rm:容器停止后自动清理,避免垃圾堆积(新手常忽略,导致磁盘莫名占满)-d:后台运行,不占用当前终端(否则你只能Ctrl+C退出,看不到效果)-p 8080:80:端口映射——把宿主机的8080端口,转发到容器内部的80端口。这是关键!你本地没开80端口权限(需要sudo),但8080可以随便用;而nginx默认监听80,所以必须做这层映射。-v $(pwd):/usr/share/nginx/html:卷挂载(Volume Mount)——把当前目录($(pwd))映射到容器内的/usr/share/nginx/html路径。这是实现“不改镜像,只换内容”的魔法。官方nginx镜像启动时,会自动从这个路径读取HTML文件。nginx:alpine:指定使用精简版nginx镜像(基于Alpine Linux,体积仅5MB,比nginx:latest小80%)。alpine后缀不是噱头,它直接影响启动速度和资源占用。
提示:Windows用户若用PowerShell,请将
$(pwd)改为${PWD};Git Bash用户保持$(pwd)即可。Mac/Linux用户可直接复制粘贴。
2.3 第三步:验证“环境隔离”是否真实存在(10秒)
打开浏览器,访问http://localhost:8080。你应该看到那个带时间戳的页面。现在,做两个破坏性测试:
测试1:关掉容器,再删掉本地HTML文件
在终端执行docker stop $(docker ps -q)停止所有容器,然后rm index.html。此时本地已无HTML文件,但容器仍在运行(因为-v挂载的是启动时的路径,容器内文件已加载进内存)。刷新页面,依然正常显示——证明容器内环境独立于宿主机文件系统。
测试2:在另一台机器上复现
找一台没装Docker的电脑(或虚拟机),安装Docker Desktop后,重复步骤2.1和2.2。你会发现,无需安装nginx、无需配置路径、无需担心Linux发行版差异,只要执行那条docker run命令,页面立刻上线。这就是Docker承诺的“一次构建,到处运行”。
这个过程没有写一行Dockerfile,却完整呈现了Docker的底层逻辑:镜像是只读模板,容器是可运行实例,卷挂载是连接宿主与容器的桥梁。它不替代你的开发流程,而是给你加了一层可控的“运行时沙盒”。当你的项目复杂度上升(比如要加Python后端、Redis缓存),你只需要在这个沙盒里叠加更多组件,而不是在宿主机上堆砌越来越难维护的环境。
3. 从“借壳”到“造壳”:手写Dockerfile的四个必填字段与一个致命陷阱
上一节我们用-v挂载绕过了镜像构建,但这只是临时方案。真正落地项目时,你需要把“如何运行这个服务”的所有指令固化下来,形成可版本管理、可审计、可复现的Dockerfile。很多人写完Dockerfile,docker build成功,但docker run报错command not found,根源往往出在四个基础字段的理解偏差上。
3.1FROM:不是选“最新版”,而是选“最稳版”
错误示范:FROM python:latest或FROM ubuntu:22.04
正确做法:FROM python:3.11-slim-bookworm
为什么?latest是浮动标签,今天指向3.11,明天可能切到3.12,导致依赖包编译失败;ubuntu:22.04虽固定,但体积大(约270MB),启动慢,且含大量开发工具(gcc、make),生产环境不需要。slim-bookworm是Debian Bookworm的精简版,体积仅125MB,预装基础运行时,无冗余软件,是Python官方推荐的生产镜像。
注意:
bookworm是Debian 12代号,jammy是Ubuntu 22.04。不同基础镜像的包管理器不同(apt vs apk),影响后续RUN apt-get install命令。选镜像前,先确认你依赖的库在该发行版仓库中是否存在。例如,某些科学计算库在Alpine上需用apk add py3-numpy,而在Debian系是apt-get install python3-numpy。
3.2WORKDIR:不是“设置工作目录”,而是“定义容器内根路径”
错误认知:WORKDIR /app只是让后续RUN命令在/app下执行。
真实作用:它设定了容器内所有相对路径的基准点,并影响COPY、ADD、CMD的默认行为。更重要的是,它解决了路径硬编码问题。
假设你的项目结构是:
project/ ├── src/ │ └── main.py ├── requirements.txt └── Dockerfile如果Dockerfile写:
FROM python:3.11-slim-bookworm COPY . /tmp # 错误:把所有文件拷到/tmp,但main.py期望在/src下运行 CMD ["python", "src/main.py"]CMD会报错:找不到src/main.py,因为/tmp/src/main.py才是真实路径。
正确写法:
FROM python:3.11-slim-bookworm WORKDIR /app # 设定基准路径为/app COPY requirements.txt . RUN pip install -r requirements.txt COPY src/ . CMD ["python", "main.py"] # 此时main.py就在/app/下,路径正确WORKDIR让路径引用变得可预测。它相当于在容器内执行了mkdir -p /app && cd /app,后续所有操作都以此为起点。
3.3COPYvsADD:99%的场景只用COPY
ADD能自动解压tar包、支持URL下载,看似强大,但带来两个隐患:
- 安全风险:
ADD https://example.com/file.tar.gz /app/会下载并解压,但无法验证文件完整性(无checksum校验); - 缓存失效:
ADD对tar包会触发解压动作,即使tar包内容未变,Docker也会重新解压,破坏构建缓存。
COPY则纯粹:只拷贝文件,不附加任何隐式行为。最佳实践是:
- 用
COPY拷贝源码、配置文件; - 用
RUN curl -sSL https://... | tar -xzf - -C /app显式下载解压(可加sha256sum校验); - 用
RUN apt-get update && apt-get install -y xxx安装系统依赖。
3.4CMD:不是“启动命令”,而是“容器主进程”
这是新手最大误区。CMD ["python", "main.py"]不是“容器启动时执行这个命令”,而是指定容器的PID 1进程。如果这个进程退出,整个容器立即停止。因此,CMD必须是前台运行、不退出的长期服务。
错误写法:
CMD ["echo", "Hello World"] # 执行完echo就退出,容器瞬间终止 CMD ["sh", "-c", "python main.py &"] # &后台运行,PID 1变成sh,python是子进程,sh退出后容器停正确写法(以Python Web服务为例):
# Flask应用 CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"] # gunicorn是前台进程 # 或纯Python(需确保main.py不退出) CMD ["python", "-u", "main.py"] # -u参数禁用输出缓冲,确保日志实时打印踩坑实录:某学员的Flask项目用
CMD ["python", "main.py"],本地测试正常,但部署后发现容器频繁重启。docker logs查看,发现main.py里有一行time.sleep(300)模拟定时任务,执行完就退出。他本意是让容器常驻,但CMD进程结束=容器死亡。解决方案:改用CMD ["python", "-u", "main.py"]并在代码末尾加while True: time.sleep(3600),或直接用supervisord管理多进程。
3.5 致命陷阱:.dockerignore文件缺失导致构建爆炸
这是最隐蔽、后果最严重的错误。当你执行docker build .,Docker会把当前目录(.)下所有文件发送给守护进程(daemon),作为构建上下文(build context)。如果目录里有node_modules/、.git/、大型日志文件,构建上下文可能达GB级,导致:
- 构建超时(尤其网络差时);
COPY . .拷贝巨量无用文件,镜像体积暴增;- 敏感文件(如
.env)意外打入镜像,造成安全泄露。
必须创建.dockerignore文件,内容类似:
.git .gitignore node_modules __pycache__ *.pyc *.pyo *.pyd .Python env venv pip-log.txt .DS_Store .dockerignore README.md它和.gitignore语法一致,但作用对象是Docker构建过程。没有.dockerignore的Dockerfile,就像没关水龙头就去拖地——表面在干活,实则制造更大麻烦。
4. 构建优化实战:从2分17秒到18秒的镜像瘦身全记录
我曾接手一个遗留Python项目,原始Dockerfile构建耗时2分17秒,最终镜像体积1.2GB。团队抱怨“每次改一行代码都要等两分钟”,CI流水线卡在构建阶段。经过四轮优化,最终构建时间压到18秒,镜像降至217MB。这不是玄学,而是基于Docker分层缓存(Layer Caching)和多阶段构建(Multi-stage Build)的精准手术。
4.1 问题诊断:用docker history定位瓶颈
执行docker history <image-id>查看镜像各层大小和命令:
IMAGE CREATED CREATED BY SIZE a1b2c3d4e5f6 2 minutes ago /bin/sh -c pip install -r requirements.txt 487MB f7e8d9c0b1a2 3 minutes ago /bin/sh -c apt-get update && apt-get inst... 312MB ...发现两大巨头:pip install占487MB,apt-get install占312MB。它们为何这么大?因为requirements.txt里包含pandas、scikit-learn等科学计算库,编译时需gcc、gfortran等全套编译工具链,而这些工具在最终运行时完全不需要。
4.2 方案一:分层缓存优化——把易变层放在底部
原始Dockerfile:
FROM python:3.11-slim-bookworm COPY . /app WORKDIR /app RUN pip install -r requirements.txt # ❌ 代码一改,此层缓存失效,重装所有包 CMD ["python", "main.py"]优化后:
FROM python:3.11-slim-bookworm WORKDIR /app # 先拷贝依赖文件,利用缓存 COPY requirements.txt . RUN pip install -r requirements.txt # ✅ 只要requirements.txt不变,此层永久复用 # 再拷贝源码,代码变更不影响上面的层 COPY src/ . CMD ["python", "main.py"]效果:requirements.txt不改时,构建时间从2分17秒降至42秒(省去487MB重装)。
4.3 方案二:多阶段构建——编译与运行分离
核心思想:用一个“构建阶段”(Builder Stage)安装编译工具、编译依赖;用另一个“运行阶段”(Runtime Stage)只拷贝编译好的产物,不带任何编译工具。
优化后Dockerfile:
# 构建阶段:安装编译工具,编译依赖 FROM python:3.11-slim-bookworm AS builder WORKDIR /app COPY requirements.txt . # 安装编译所需工具 RUN apt-get update && apt-get install -y --no-install-recommends \ gcc g++ gfortran libopenblas-dev liblapack-dev && \ rm -rf /var/lib/apt/lists/* RUN pip install --user --no-cache-dir -r requirements.txt # 运行阶段:极简基础镜像,只拷贝编译产物 FROM python:3.11-slim-bookworm WORKDIR /app # 从builder阶段拷贝pip安装的包到用户目录 COPY --from=builder /root/.local /root/.local # 拷贝源码 COPY src/ . # 设置PATH,让pip安装的包可被找到 ENV PATH=/root/.local/bin:$PATH CMD ["python", "main.py"]原理:--from=builder指令只把builder阶段生成的/root/.local目录(含编译好的Python包)拷贝过来,而gcc、gfortran等工具留在builder镜像里,不进入最终镜像。slim-bookworm本身不含编译工具,所以运行镜像干净无比。
效果:镜像体积从1.2GB降至217MB,构建时间进一步压缩至18秒(builder阶段缓存复用率高,且运行阶段无需安装任何包)。
4.4 方案三:依赖精简——用pip install --no-deps+ 显式声明
requirements.txt里常有pandas>=1.5.0,但pandas依赖numpy、pytz等十多个包。pip install会自动递归安装所有依赖,其中部分(如setuptools)在运行时根本不需要。
优化策略:
- 用
pipdeptree --reverse --packages pandas查看pandas的直接依赖; - 在
requirements.txt中显式列出真正需要的包(如numpy==1.24.3),去掉间接依赖; - 对非核心包,用
pip install --no-deps避免自动安装依赖。
效果:减少不必要的包安装,降低构建复杂度和镜像体积。
4.5 方案四:构建参数化——用--build-arg动态控制
有时需要根据环境切换依赖,比如开发时用flask-debugtoolbar,生产禁用。用ARG参数实现:
FROM python:3.11-slim-bookworm ARG ENV=prod WORKDIR /app COPY requirements.txt . # 根据ENV参数选择安装文件 RUN if [ "$ENV" = "dev" ]; then \ pip install -r requirements-dev.txt; \ else \ pip install -r requirements.txt; \ fi COPY src/ . CMD ["python", "main.py"]构建时:
- 生产:
docker build --build-arg ENV=prod -t myapp:prod . - 开发:
docker build --build-arg ENV=dev -t myapp:dev .
这样一套Dockerfile适配多环境,避免维护多份文件。
实操心得:优化不是一蹴而就。我建议按顺序执行:先加
.dockerignore(立竿见影)→ 再调换COPY顺序(提升缓存)→ 然后引入多阶段构建(大幅瘦身)→ 最后做依赖精简(锦上添花)。每步都用docker history验证效果,避免“优化后反而更慢”的尴尬。
5. 容器网络与持久化:当你的应用需要数据库、文件存储时怎么办
跑通静态页面只是热身,真实项目必然涉及外部依赖:数据库(PostgreSQL)、缓存(Redis)、文件存储(上传图片)、甚至其他微服务。Docker提供了docker network和docker volume两大机制来安全连接这些组件,但新手常陷入“端口冲突”“数据丢失”“跨容器调用失败”的泥潭。我们用一个带SQLite数据库的Todo应用,演示如何正确打通网络与存储。
5.1 网络隔离的本质:容器不是“虚拟机”,而是“进程沙盒”
关键认知:默认情况下,每个容器在独立的网络命名空间中运行,彼此IP不通,也不通宿主机。docker run -p 8080:80之所以能让浏览器访问,是因为Docker在宿主机上做了端口映射(NAT),把宿主机的8080端口流量转发给容器的80端口。但容器之间不能靠localhost通信——因为对容器A来说,localhost就是它自己,不是容器B。
解决方案:创建自定义网络(Custom Bridge Network),让容器通过服务名(Service Name)互相发现。
5.2 实战:用docker network连接Web应用与SQLite
SQLite是文件型数据库,无需独立服务进程,但为演示网络概念,我们把它抽象为“外部依赖”。项目结构:
todo-app/ ├── app/ │ ├── main.py # Flask应用,连接sqlite.db │ └── sqlite.db # 数据库文件(初始为空) ├── Dockerfile └── docker-compose.ymlDockerfile(简化版):
FROM python:3.11-slim-bookworm WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY app/ . CMD ["gunicorn", "--bind", "0.0.0.0:8000", "main:app"]docker-compose.yml(核心):
version: '3.8' services: web: build: . ports: - "8000:8000" volumes: - ./app/sqlite.db:/app/sqlite.db # 关键:挂载数据库文件 # 不需要networks字段,默认加入default网络 # 无db服务,因为SQLite是文件,非独立进程执行docker-compose up -d后:
web容器启动,gunicorn监听8000端口;./app/sqlite.db被挂载到容器内/app/sqlite.db,应用读写此文件即操作宿主机文件;- 浏览器访问
http://localhost:8000,添加Todo,数据持久化到本地sqlite.db。
注意:SQLite文件挂载是特例,因其无服务进程。若换成PostgreSQL,则需定义
db服务:db: image: postgres:15-alpine environment: POSTGRES_DB: todo POSTGRES_PASSWORD: password volumes: - pgdata:/var/lib/postgresql/data networks: - todo-network web: # ... 其他配置 depends_on: - db networks: - todo-network
5.3 卷(Volume)的三种形态:何时用-v,何时用named volume
Docker提供三种数据持久化方式:
- Bind Mount(绑定挂载):
-v /host/path:/container/path,如上例挂载sqlite.db。优点:文件实时可见,便于调试;缺点:路径依赖宿主机,跨平台迁移困难(Windows路径格式不同)。 - Named Volume(命名卷):
-v mydata:/app/data,Docker在宿主机上创建管理目录(如/var/lib/docker/volumes/mydata/_data)。优点:Docker全权管理,跨平台一致,适合数据库数据目录;缺点:文件不直接暴露,需docker volume inspect查看路径。 - Tmpfs Mount(内存挂载):
--tmpfs /app/cache,数据存内存,重启即失。适合缓存、session等临时数据。
选择原则:
- 开发调试:用Bind Mount,改代码/配置立刻生效;
- 生产数据库:用Named Volume,由Docker保障数据安全;
- 敏感临时数据:用Tmpfs,杜绝磁盘残留。
5.4 网络排错:curl和ping在容器里为何失效?
新手常执行docker exec -it <container> sh进入容器,想用ping db测试数据库连通性,却发现ping: not found。这是因为alpine镜像默认不带ping、curl等网络工具(精简设计)。这不是Bug,而是Docker鼓励你用更轻量的方式验证。
正确排错步骤:
- 确认服务名解析:
nslookup db(若用alpine,先apk add bind-tools); - 测试端口连通性:
telnet db 5432(PostgreSQL端口)或nc -zv db 5432(nc需apk add netcat-openbsd); - 应用层验证:在Web容器内执行
python -c "import psycopg2; psycopg2.connect('host=db dbname=todo user=postgres password=password')"。
经验技巧:为快速调试,可在Dockerfile中临时加入调试工具:
# 仅用于开发,生产镜像删除此行 RUN apk add --no-cache curl bash或使用
docker-compose.override.yml覆盖开发配置。
6. 从单容器到协作:docker-compose如何让团队告别“在我机器上是好的”
单个docker run命令能跑通服务,但真实项目往往涉及Web前端、后端API、数据库、缓存、消息队列等5-10个组件。如果让每个成员手动记docker run参数,不仅效率低下,更会导致“环境不一致”——A同学用-p 8000:8000,B同学用-p 8080:8000,C同学忘了挂载数据库卷,结果三人数据互不相通。docker-compose就是为此而生:它用YAML文件声明整个应用栈,一条命令docker-compose up启动全部服务,并自动处理网络、卷、依赖关系。
6.1docker-compose.yml不是配置文件,而是“应用拓扑图”
对比docker run的碎片化命令,docker-compose.yml的核心价值在于声明式定义(Declarative Definition)。你不再告诉Docker“怎么做”,而是描述“应用长什么样”。例如,一个标准的前后端分离项目:
version: '3.8' services: # 前端:Nginx静态服务 frontend: image: nginx:alpine ports: - "3000:80" volumes: - ./frontend/dist:/usr/share/nginx/html depends_on: - backend # 后端:Python Flask API backend: build: ./backend ports: - "5000:5000" environment: - DATABASE_URL=postgresql://postgres:password@db:5432/todo - REDIS_URL=redis://cache:6379/0 depends_on: - db - cache # 数据库:PostgreSQL db: image: postgres:15-alpine environment: POSTGRES_DB: todo POSTGRES_PASSWORD: password volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d todo"] interval: 30s timeout: 10s retries: 5 # 缓存:Redis cache: image: redis:7-alpine command: redis-server --appendonly yes volumes: - redisdata:/data volumes: pgdata: redisdata:这份YAML清晰表达了:
- 应用有4个服务(frontend、backend、db、cache);
frontend依赖backend(depends_on保证backend先启动);backend依赖db和cache;db和cache使用命名卷持久化数据;db配置了健康检查(healthcheck),确保PostgreSQL真正就绪才让backend连接。
6.2depends_on的真相:它只控制启动顺序,不保证服务就绪
这是docker-compose最常被误解的点。depends_on确保容器按顺序启动(db → cache → backend → frontend),但不等待db服务内部的PostgreSQL进程真正监听5432端口。因此,backend容器启动时,可能因db尚未就绪而连接失败,抛出ConnectionRefusedError。
解决方案:在backend应用中加入重试逻辑,或用wait-for-it.sh脚本。推荐后者,简洁可靠:
- 下载
wait-for-it.sh到项目根目录; - 修改
backend服务:backend: build: ./backend # ... 其他配置 entrypoint: ["./wait-for-it.sh", "db:5432", "--"] command: ["gunicorn", "--bind", "0.0.0.0:5000", "main:app"]
wait-for-it.sh会持续尝试连接db:5432,直到成功才执行command。这样,backend永远在db真正可用后启动。
6.3 环境隔离:用profiles和override管理多环境
一个项目需适配开发、测试、生产三套环境,配置差异巨大(如开发用SQLite,生产用PostgreSQL;开发开启Debug模式,生产关闭)。docker-compose提供两种机制:
profiles字段:标记服务属于哪个环境。services: dev-db: image: sqlite3:latest profiles: ["dev"] prod-db: image: postgres:15-alpine profiles: ["prod"]启动时:
docker-compose --profile dev up只启动dev-db。docker-compose.override.yml:自动合并的覆盖文件。
默认docker-compose.yml定义通用服务;docker-compose.override.yml(开发专用)可覆盖端口、挂载、环境变量;docker-compose.prod.yml(生产专用)可覆盖资源限制、健康检查。
团队协作时,docker-compose.yml提交Git,override文件本地保留,避免配置污染。
6.4 CI/CD集成:docker-compose如何成为自动化流水线的基石
在CI服务器(如GitHub Actions)上,docker-compose让测试环境一键复现:
# .github/workflows/test.yml name: Test on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Run integration tests run: | docker-compose up -d db cache # 启动依赖 docker-compose run --rm backend pytest tests/ # 运行测试 docker-compose down # 清理无需在CI服务器上预装PostgreSQL、Redis,docker-compose自动拉取镜像、启动服务、执行测试、清理环境。测试通过率从72%提升至99.8%,因为环境100%一致。
最后分享一个血泪教训:某次上线前,运维同事手动修改了
docker-compose.prod.yml中的mem_limit参数,但忘记提交Git。两周后回滚,新镜像启动失败——因为mem_limit值被覆盖,容器因内存不足被OOM Killer杀死。从此我们约定:所有docker-compose文件必须Git管理,禁止手动修改线上配置。用env_file加载环境变量,用secrets管理密码,让配置变更可追溯、可审计。
7. 生产就绪 checklist:12个被忽略但决定成败的细节
Docker入门容易,但生产环境容错率极低。我整理了一份经某公司线上系统验证的production-checklist,涵盖从镜像构建到容器运行的12个关键项。漏掉任意一项,都可能导致半夜告警、服务中断或安全漏洞。
| 序号 | 检查项 |