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

手动操作存在明显缺陷

  1. 操作过程无法记录,不可复现;

  2. 环境配置无法版本管理,团队协作易出现环境不一致;

  3. 重复部署效率极低。

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 命令,执行结果会永久固化到镜像内;

  • 小贴士:

    1. 单个 RUN 串联多条命令(&& 连接),可以减少镜像层数;

    2. --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
    
  • 格式区别:

    1. Exec 格式:主进程 PID=1,可正常接收停止信号,容器支持优雅关闭;

    2. 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 分层设计的两大优势

  1. 镜像层复用
    多个基于同一基础镜像的自定义镜像,会共享底层只读层,磁盘仅存储一份,大幅节省存储空间。

  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)

  1. 镜像:由多层只读层堆叠而成,所有层不可修改;

  2. 容器:在镜像只读层之上,新增一层可写容器层

  3. 数据读写逻辑:容器内所有文件修改、新增、删除操作,仅作用于顶层可写层,不会改动原始镜像

  4. 生命周期:删除容器时,可写层同步销毁,镜像不受影响;

  5. 数据卷(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 作为配置文件格式,核心规则:

  1. 缩进使用2 个空格,禁止使用 Tab;

  2. 键值对格式:key: value,冒号后必须加空格;

  3. 列表项使用 - 开头。

基础示例:

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:
关键配置说明
  1. build: .:基于本地 Dockerfile 构建镜像,替代 image 字段;

  2. depends_on:仅保证启动顺序,不等待目标服务完全就绪,代码内需自行实现重连逻辑;

  3. environment:注入环境变量,应用内可读取使用;

  4. 自定义网络:同网络内容器可直接使用服务名作为域名互相访问(如 web 连接 redis:6379);

  5. 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),容器间域名解析流程:

  1. 容器发起域名请求(如解析 redis);

  2. 容器内 /etc/resolv.conf 指向内置 DNS 127.0.0.11

  3. Docker DNS 根据服务名查询对应容器 IP,返回结果;

  4. 同网络容器直接通过 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/MySQLdepends_on 仅控制启动顺序,不等待服务就绪代码中添加循环重试、延时连接逻辑
端口冲突,启动失败宿主机目标端口已被其他进程占用修改宿主机映射端口;使用 lsof -i :端口号 查找占用进程
构建出的镜像体积过大基础镜像臃肿、残留缓存文件更换 slim / alpine 轻量镜像;合并 RUN 指令、清理临时文件

更多推荐