DevOps 入门系列 :Dockerfile 与 Docker Compose
DevOps 入门系列:Dockerfile 与 Docker Compose
一、从「手动配置容器」到「用 Dockerfile 自动化」
1.1 回顾:手动操作容器的弊端
常规方式直接运行 Nginx 基础容器:
docker run -d --name mynginx -p 8080:80 nginx:alpine
若需要自定义容器内容(修改页面、新增配置、安装软件),通常会进入容器手动操作:
# 进入容器终端
docker exec -it mynginx sh
# 安装工具
apk add curl
exit
# 将容器修改保存为新镜像
docker commit mynginx mynginx-custom
手动操作存在明显缺陷:
-
操作过程无法记录,不可复现;
-
环境配置无法版本管理,团队协作易出现环境不一致;
-
重复部署效率极低。
Dockerfile 正是为解决以上问题而生:通过声明式脚本定义镜像构建流程,实现自动化、可复用、可版本管控的镜像构建。
1.2 什么是 Dockerfile
Dockerfile 是纯文本格式的构建脚本,由一系列大写指令组成。Docker 会按顺序逐行执行指令,每条指令生成一个独立镜像层,最终拼装为完整镜像。
二、Dockerfile 核心指令详解
2.1 FROM — 指定基础镜像
语法
FROM nginx:alpine
-
规则:所有 Dockerfile 必须以 FROM 开头,用于指定构建依赖的基础镜像;
-
补充:可使用
scratch(空镜像)从零构建,生产环境极少使用; -
作用:复用基础镜像的操作系统、包管理器、运行环境,无需从零搭建系统。
2.2 RUN — 构建阶段执行命令
语法
RUN apk update && apk add --no-cache curl
-
作用:镜像构建过程中执行 Shell 命令,执行结果会永久固化到镜像内;
-
小贴士:
-
单个
RUN串联多条命令(&&连接),可以减少镜像层数; -
--no-cache禁止缓存软件包索引,有效缩小镜像体积。
-
2.3 COPY 与 ADD — 向镜像内复制文件
COPY(推荐,纯文件复制)
# 把宿主机当前目录里的 index.html文件复制到容器里的 nginx 网页目录下
COPY ./index.html /usr/share/nginx/html/
-
功能:将构建上下文内的文件 / 目录复制到镜像指定路径;
-
构建上下文:执行
docker build .时,.代表当前目录,Docker 会将该目录下所有文件传输至 Docker 守护进程,仅能复制上下文内文件。
ADD(增强版复制)
# 将网络地址 https://example.com/file.tar.gz 的文件下载到容器内部的 /tmp 目录
ADD https://example.com/file.tar.gz /tmp/
-
额外能力:自动解压
tar压缩包、支持从 URL 下载文件; -
注意:远程 URL 下载的压缩包,ADD 不会自动解压;只有本地压缩包才自动解压
-
建议:无解压 / 下载需求时,优先使用
COPY,语义更清晰。
2.4 WORKDIR — 设置工作目录
WORKDIR /app
-
作用:为后续
RUN/COPY/CMD等指令指定默认工作目录,目录不存在则自动创建; -
优势:避免反复书写绝对路径,简化脚本。
2.5 CMD — 容器启动默认命令
两种写法(优先使用 Exec 格式)
# Exec 格式(推荐)
CMD ["nginx", "-g", "daemon off;"]
# Shell 格式(不推荐)
CMD nginx -g "daemon off;"
-
核心规则:一个 Dockerfile 仅最后一个
CMD生效; -
特性:
CMD可被docker run末尾的命令强制覆盖,示例:# 覆盖原 CMD,不再启动nginx,启动 /bin/sh docker run -it myimage /bin/sh -
格式区别:
-
Exec 格式:主进程 PID=1,可正常接收停止信号,容器支持优雅关闭;
-
Shell 格式:实际由
/bin/sh -c拉起进程,PID=1 为 Shell,信号传递异常。
-
2.6 ENV — 设置环境变量
ENV NODE_ENV=production
- 作用:定义环境变量,构建阶段 + 容器运行阶段均可读取使用。
2.7 EXPOSE — 声明端口(仅注释作用)
EXPOSE 80
-
作用:仅用于文档标注,告知使用者该镜像应用监听端口;
-
注意:不会自动完成端口映射,端口转发仍需在
docker run中通过-p配置。
2.8 完整实战:Flask 应用 Dockerfile
项目目录结构
myapp/
├── Dockerfile
├── app.py
└── requirements.txt
1. 业务代码
app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "Hello from Docker!"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
requirements.txt
flask==2.3.0
2. Dockerfile 编写
# 1. 基于轻量 Python 基础镜像
FROM python:3.9-slim
# 2. 设置工作目录
WORKDIR /app
# 3. 复制依赖文件(优先复制,利用构建缓存)
COPY requirements.txt .
# 4. 安装项目依赖
RUN pip install --no-cache-dir -r requirements.txt
# 5. 复制业务代码
COPY app.py .
# 6. 声明应用端口
EXPOSE 5000
# 7. 容器启动命令
CMD ["python", "app.py"]
3. 构建 & 运行命令
# 构建镜像,命名为 my-flask-app:v1
docker build -t my-flask-app:v1 .
# 后台启动容器,端口映射 5000:5000
docker run -d -p 5000:5000 --name flask-app my-flask-app:v1
访问 http://localhost:5000 即可看到页面输出。
三、镜像分层与构建缓存(核心原理)
3.1 什么是镜像分层
Docker 镜像并非单个完整文件,而是由多层只读镜像层堆叠组成。
-
规则:
RUN/COPY/ADD每一条指令,都会生成一个新镜像层; -
查看镜像分层历史:
docker history my-flask-app:v1
3.2 分层设计的两大优势
-
镜像层复用
多个基于同一基础镜像的自定义镜像,会共享底层只读层,磁盘仅存储一份,大幅节省存储空间。 -
构建缓存加速
Docker 构建时会校验每一层内容,若层级无变更,直接复用本地缓存,跳过重复执行,提升构建速度。
3.3 构建缓存使用原则
核心规则:将变动频率低的指令放在靠前位置,变动频率高的指令后置。
✅ 推荐写法(合理利用缓存)
FROM python:3.9-slim # 几乎不变
WORKDIR /app
COPY requirements.txt . # 依赖文件变更少
RUN pip install -r requirements.txt
COPY . . # 业务代码频繁改动,放最后
CMD [...]
❌ 错误写法(缓存失效)
COPY . . # 代码先行复制,代码一改后续全部重跑
RUN pip install -r requirements.txt
补充:一旦某一层发生变更,该层之后所有层级都会重新构建。
3.4 底层支撑:联合文件系统(UnionFS/OverlayFS)
-
镜像:由多层只读层堆叠而成,所有层不可修改;
-
容器:在镜像只读层之上,新增一层可写容器层;
-
数据读写逻辑:容器内所有文件修改、新增、删除操作,仅作用于顶层可写层,不会改动原始镜像;
-
生命周期:删除容器时,可写层同步销毁,镜像不受影响;
-
数据卷(Volume):独立于容器可写层的持久化存储,用于保存重要数据。
四、容器隔离底层核心技术
容器之所以轻量化、环境隔离,依赖 Linux 内核两大特性:命名空间 Namespaces + 控制组 cgroups。
4.1 命名空间(Namespaces)—— 资源视野隔离
内核提供的隔离机制,让容器进程仅能看到分配给自己的资源,模拟独立操作系统环境。
| 命名空间类型 | 隔离内容 | 实现效果 |
|---|---|---|
| PID | 进程 ID | 容器内进程 PID 从 1 开始,容器间相互独立,看不到宿主机及其他容器进程 |
| NET | 网络设备、IP、路由 | 容器拥有独立网卡、IP、网络栈 |
| MNT | 文件系统挂载 | 容器拥有独立根文件系统(rootfs) |
| UTS | 主机名、域名 | 容器可自定义 hostname,与宿主机互不干扰 |
| IPC | 进程间通信 | 隔离共享内存、消息队列等进程通信资源 |
| USER | 用户 / 用户组 ID | 容器内 root 用户可映射为宿主机普通用户,降低权限风险 |
命名空间相当于给每个容器佩戴「隔离眼镜」,每个容器都以为自己独占整台服务器。
4.2 控制组(cgroups)—— 资源配额限制
内核特性,用于限制、统计、管控一组进程的硬件资源(CPU、内存、磁盘 IO、带宽),防止单个容器耗尽宿主机资源。
示例:启动容器并限制资源(内存 512MB、CPU 0.5 核)
docker run -d --memory="512m" --cpus="0.5" nginx
- 超限处理:容器内存超出限制时,会被系统 OOM 机制终止。
总结:命名空间负责「隔离视野」,cgroups 负责「限制资源用量」。
五、Docker Compose:多容器一键编排
5.1 场景痛点
一套完整应用通常包含多个组件:Web 服务、数据库、缓存、反向代理等。
-
手动方式:逐个执行
docker run、创建网络、挂载数据卷,命令冗长、易出错、部署效率低; -
解决方案:
Docker Compose,单机多容器编排工具。
5.2 Docker Compose 简介
通过一份 docker-compose.yml 配置文件,统一定义所有服务(镜像、端口、环境变量、网络、数据卷、启动顺序),单条命令即可完成整套服务启动、停止、运维。
Compose 如同开店清单,清单写清所有设备,工具自动一次性配齐并启动。
5.3 YAML 语法极简规则
Compose 使用 YAML 作为配置文件格式,核心规则:
-
缩进使用2 个空格,禁止使用 Tab;
-
键值对格式:
key: value,冒号后必须加空格; -
列表项使用
-开头。
基础示例:
services:
web:
image: nginx
ports:
- "8080:80"
5.4 完整 Compose 配置实战
项目架构
服务组成:Flask 后端 (web) + Redis 缓存 + MySQL 数据库
目录结构:
myproject/
├── docker-compose.yml
├── Dockerfile
├── app.py
└── requirements.txt
docker-compose.yml 配置
version: '3.8'
# 定义所有服务
services:
web:
build: . # 使用当前目录 Dockerfile 构建镜像
ports:
- "5000:5000"
environment:
- REDIS_HOST=redis
- MYSQL_HOST=mysql
- MYSQL_ROOT_PASSWORD=123456
depends_on: # 依赖服务(控制启动顺序)
- redis
- mysql
networks:
- mynet
redis:
image: redis:alpine
networks:
- mynet
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: mydb
volumes:
- mysql-data:/var/lib/mysql # 命名卷,持久化数据库数据
networks:
- mynet
# 自定义网络
networks:
mynet:
driver: bridge
# 定义命名数据卷
volumes:
mysql-data:
关键配置说明
-
build: .:基于本地 Dockerfile 构建镜像,替代image字段; -
depends_on:仅保证启动顺序,不等待目标服务完全就绪,代码内需自行实现重连逻辑; -
environment:注入环境变量,应用内可读取使用; -
自定义网络:同网络内容器可直接使用服务名作为域名互相访问(如 web 连接
redis:6379); -
volumes:命名卷,实现数据持久化,容器删除后数据不丢失。
5.5 Docker Compose 常用命令
所有命令需在 docker-compose.yml 所在目录执行:
# 1. 后台启动所有服务
docker compose up -d
# 2. 查看服务运行状态
docker compose ps
# 3. 查看服务日志(-f 实时跟踪)
docker compose logs web
docker compose logs -f web
# 4. 进入指定服务容器终端
docker compose exec web sh
# 5. 停止服务,删除容器、网络(保留数据卷)
docker compose down
# 6. 停止服务,删除容器、网络、数据卷(数据清空)
docker compose down -v
# 7. 单独重启某个服务
docker compose restart web
# 8. 重新构建镜像并启动(代码/配置变更后使用)
docker compose up -d --build
5.6 内置 DNS 解析原理
Compose 自定义网络会启用 Docker 内置 DNS 服务(地址 127.0.0.11),容器间域名解析流程:
-
容器发起域名请求(如解析
redis); -
容器内
/etc/resolv.conf指向内置 DNS127.0.0.11; -
Docker DNS 根据服务名查询对应容器 IP,返回结果;
-
同网络容器直接通过 IP + 端口通信,无需宿主机端口映射(端口映射仅用于外部访问)。
六、动手实战:Compose 部署 Flask + Redis 计数器
6.1 项目文件准备
新建 counter 目录,包含 4 个文件:
1. [app.py](app.py)(业务代码)
from flask import Flask
from redis import Redis
import os
app = Flask(__name__)
redis = Redis(host=os.getenv('REDIS_HOST', 'localhost'), port=6379)
@app.route('/')
def hello():
count = redis.incr('hits')
return f'Hello! This page has been visited {count} times.'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
2. requirements.txt(依赖)
flask
redis
3. Dockerfile(镜像构建脚本)
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
4. docker-compose.yml(编排配置)
version: '3.8'
services:
web:
build: .
ports:
- "5000:5000"
environment:
- REDIS_HOST=redis
depends_on:
- redis
redis:
image: redis:alpine
6.2 运行与验证
# 进入项目目录
cd counter
# 后台启动整套服务
docker compose up -d
访问 http://localhost:5000,刷新页面,访问计数器持续累加。
6.3 服务清理
# 停止并删除容器、网络(无数据卷,Redis 数据丢失)
docker compose down
拓展:如需 Redis 数据持久化,在 redis 服务中添加数据卷配置:
volumes: - redis-data:/data
七、常见问题与排查技巧
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| docker build 报错:COPY failed: file not found | 文件不在构建上下文内 / 路径书写错误 | 核对文件路径,确保文件位于 Dockerfile 同级目录或子目录 |
| 容器启动后立刻退出 | 主进程未以前台方式运行 | 调整 CMD 命令为前台进程;执行 docker logs 容器名 查看日志 |
| web 服务启动后无法连接 Redis/MySQL | depends_on 仅控制启动顺序,不等待服务就绪 | 代码中添加循环重试、延时连接逻辑 |
| 端口冲突,启动失败 | 宿主机目标端口已被其他进程占用 | 修改宿主机映射端口;使用 lsof -i :端口号 查找占用进程 |
| 构建出的镜像体积过大 | 基础镜像臃肿、残留缓存文件 | 更换 slim / alpine 轻量镜像;合并 RUN 指令、清理临时文件 |
更多推荐
所有评论(0)