06-Dockerfile-编写自己的镜像
Dockerfile是镜像的"配方"
前几章我们用现成的镜像,今天学习如何编写Dockerfile,创建自己的镜像。
什么是Dockerfile?
Dockerfile是一个文本文件,包含一系列指令,用于构建Docker镜像。
类比:
- Dockerfile = 烹饪食谱
- 镜像 = 做好的菜
- 容器 = 端上桌的菜
Dockerfile基础语法
基本结构
# 注释以#开头
FROM ubuntu:20.04
RUN apt-get update
CMD ["echo", "Hello, Docker!"]
指令执行顺序
Docker会按照从上到下的顺序执行指令,每执行一条指令就会创建一个新的镜像层。
常用指令详解
1. FROM - 基础镜像
# 基本用法
FROM python:3.11
FROM ubuntu:20.04
# 多阶段构建的第一阶段
FROM python:3.11 AS builder
选择基础镜像的原则:
- 官方镜像优先:
python、node、nginx等 - 版本明确:
python:3.11.7比python:3.11更明确 - 体积要小:使用alpine或slim版本
- 安全可靠:优先使用经过安全审计的镜像
常用基础镜像:
alpine:最轻量(5MB)slim:比完整版小很多latest:最新版(生产环境不推荐)
2. LABEL - 镜像元数据
LABEL maintainer="yourname@example.com"
LABEL version="1.0"
LABEL description="My awesome application"
查看镜像标签:
docker inspect myapp | grep Labels
3. RUN - 执行命令
# Shell形式(默认)
RUN apt-get update && apt-get install -y curl
# Exec形式(推荐)
RUN ["apt-get", "update"]
RUN ["apt-get", "install", "-y", "curl"]
# 组合多个命令(减少层数)
RUN apt-get update && \
apt-get install -y \
curl \
wget \
vim && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
优化建议:
- 合并RUN指令(减少层数)
- 清理缓存(减小镜像体积)
- 使用
--no-cache-dir(Python) - 使用
&&连接命令
4. COPY - 复制文件
# 复制文件
COPY requirements.txt /app/
# 复制目录
COPY . /app/
# 使用--from从其他阶段复制(多阶段构建)
COPY --from=builder /app/dist /app/
COPY vs ADD的区别:
COPY:只能复制本地文件ADD:可以复制本地文件、URL、自动解压tar文件- 推荐优先使用COPY,更明确和可控
5. ADD - 高级复制
# 复制本地文件
ADD app.py /app/
# 下载文件
ADD https://example.com/file.tar.gz /app/
# 自动解压tar文件
ADD archive.tar.gz /app/
何时使用ADD:
- 需要下载远程文件
- 需要自动解压压缩包
- 否则使用COPY
6. WORKDIR - 工作目录
# 设置工作目录
WORKDIR /app
# 相当于
RUN cd /app
优势:
- 后续指令的路径都相对于工作目录
- 自动创建不存在的目录
- 更清晰和易读
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
7. ENV - 环境变量
# 设置单个环境变量
ENV PYTHONUNBUFFERED=1
# 设置多个
ENV TZ=Asia/Shanghai \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8
# 构建时使用
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y tzdata
环境变量的用途:
- 配置应用参数
- 控制依赖安装行为
- 传递运行时配置
8. EXPOSE - 暴露端口
# 暴露端口
EXPOSE 80
EXPOSE 443
EXPOSE 8000
# 一次暴露多个
EXPOSE 80 443 8000
注意:
EXPOSE只是声明,不会真正开放端口- 真正开放端口需要
docker run -p - 作用:文档说明、Docker自动映射(-P)
9. CMD - 启动命令
# Exec形式(推荐)
CMD ["python", "app.py"]
CMD ["nginx", "-g", "daemon off;"]
# Shell形式
CMD python app.py
# 作为ENTRYPOINT的参数
CMD ["--config", "nginx.conf"]
CMD vs ENTRYPOINT:
CMD:可以被docker run的参数覆盖ENTRYPOINT:不能被覆盖,只能通过--entrypoint覆盖
10. ENTRYPOINT - 入口点
# Exec形式
ENTRYPOINT ["python", "app.py"]
# Shell形式
ENTRYPOINT python app.py
ENTRYPOINT + CMD组合:
ENTRYPOINT ["python"]
CMD ["app.py"]
运行时:
docker run myimage # 执行: python app.py
docker run myimage test.py # 执行: python test.py
11. VOLUME - 数据卷
# 声明数据卷
VOLUME /data
VOLUME ["/data", "/logs"]
# 使用匿名卷
VOLUME /var/lib/mysql
作用:
- 声明容器需要持久化的数据目录
- 提示用户挂载数据卷
- 防止容器删除时数据丢失
12. USER - 运行用户
# 切换用户
USER nginx
USER 1000:1000
# 创建用户并切换
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
安全最佳实践:
- 不要以root用户运行容器
- 创建专用用户运行应用
13. ARG - 构建参数
# 定义构建参数
ARG VERSION=1.0
ARG BUILD_DATE
# 使用构建参数
LABEL version=${VERSION}
LABEL build_date=${BUILD_DATE}
构建时传递参数:
docker build --build-arg VERSION=2.0 --build-arg BUILD_DATE=$(date) -t myapp .
ARG vs ENV:
ARG:构建时使用,不会保留到镜像ENV:运行时使用,保留到镜像
多阶段构建
为什么需要多阶段构建?
问题:构建镜像包含很多构建工具(gcc、make等),最终镜像体积很大。
解决:多阶段构建,分离构建环境和运行环境。
基本语法
# 构建阶段
FROM python:3.11 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
# 运行阶段
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]
实战示例:Go应用
# 构建阶段
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o app
# 运行阶段
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/app .
EXPOSE 8080
CMD ["./app"]
优势:
- 构建阶段:包含所有构建工具(大)
- 运行阶段:只包含可执行文件(小)
- 最终镜像体积从1GB减少到10MB!
实战:构建Python Web应用
项目结构
my-flask-app/
├── app.py
├── requirements.txt
└── Dockerfile
app.py
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/')
def hello():
return jsonify({
"message": "Hello, Docker!",
"version": "1.0"
})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
requirements.txt
flask==2.3.0
gunicorn==21.2.0
Dockerfile
# 多阶段构建
# 构建阶段
FROM python:3.11 AS builder
WORKDIR /app
# 只复制依赖文件(利用缓存)
COPY requirements.txt .
# 安装依赖到用户目录
RUN pip install --user --no-cache-dir -r requirements.txt
# 运行阶段
FROM python:3.11-slim
WORKDIR /app
# 复制依赖
COPY --from=builder /root/.local /root/.local
# 复制应用代码
COPY . .
# 设置环境变量
ENV PATH=/root/.local/bin:$PATH
ENV PYTHONUNBUFFERED=1
ENV TZ=Asia/Shanghai
# 创建非root用户
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
# 暴露端口
EXPOSE 8000
# 启动命令
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:app"]
构建和运行
# 构建镜像
docker build -t my-flask-app:latest .
# 运行容器
docker run -d -p 8000:8000 --name myflask my-flask-app
# 测试
curl http://localhost:8000
# 输出:{"message":"Hello, Docker!","version":"1.0"}
# 查看日志
docker logs -f myflask
Dockerfile最佳实践
1. 利用构建缓存
# 不好的做法(经常变化的文件放前面)
COPY . .
RUN pip install -r requirements.txt
# 好的做法(不常变化的文件放前面)
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
原理:如果Dockerfile的指令和文件没有变化,Docker会复用缓存。
2. 最小化层数
# 不好(多层)
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
# 好(一层)
RUN apt-get update && \
apt-get install -y curl && \
apt-get clean
3. 使用.dockerignore
创建 .dockerignore 文件,排除不需要的文件:
.git
.gitignore
README.md
__pycache__
*.pyc
*.pyo
*.pyd
venv
.venv
.env
tests/
docs/
作用:
- 减少构建上下文大小
- 提高构建速度
- 避免敏感信息进入镜像
4. 优化层顺序
不常变化的放前面,常变化的放后面:
# 不常变化的
FROM python:3.11-slim
WORKDIR /app
# 基本不变化
COPY requirements.txt .
RUN pip install -r requirements.txt
# 经常变化
COPY . .
5. 使用多阶段构建
减小最终镜像体积:
FROM python:3.11 AS builder
# ... 安装依赖 ...
FROM python:3.11-slim
COPY --from=builder ...
6. 不要在镜像中存储密钥
# 不好(密钥进入镜像历史)
COPY .env .
# 好(运行时注入)
COPY .env.example .
# 运行时用 -e 参数或 --env-file
7. 使用特定的镜像标签
# 不好(可能更新到不兼容的版本)
FROM python:3
# 好(明确版本)
FROM python:3.11.7
8. 清理临时文件
# 清理apt缓存
RUN apt-get update && \
apt-get install -y curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 清理pip缓存
RUN pip install --no-cache-dir -r requirements.txt
本章小结
- Dockerfile指令:FROM、RUN、COPY、ADD、WORKDIR、CMD等
- 最佳实践:利用缓存、最小化层数、多阶段构建
- 安全考虑:不存储密钥、使用特定版本、不以root运行
- 性能优化:使用.dockerignore、优化层顺序、清理缓存
现在已经会写Dockerfile了,下一章我们来学习数据持久化!
继续学下去,马上就能做实用项目了!