Dockerfile 编写与镜像构建

Dockerfile 编写与镜像构建

Dockerfile 是什么?

Dockerfile 是一个文本文件,包含一系列指令,Docker 根据这些指令自动构建镜像。可以理解为 “镜像的菜谱”—— 告诉 Docker 一步步如何做出一个镜像。


Dockerfile 指令速查

指令用途示例
FROM指定基础镜像(必须是第一条指令)FROM python:3.11-slim
WORKDIR设置工作目录WORKDIR /app
COPY复制文件到镜像COPY . /app
ADD复制文件(支持 URL 和自动解压)ADD app.tar.gz /app
RUN构建时执行命令RUN pip install -r requirements.txt
CMD容器启动时的默认命令CMD ["python", "app.py"]
ENTRYPOINT容器启动时的入口命令ENTRYPOINT ["python"]
ENV设置环境变量ENV APP_ENV=production
EXPOSE声明容器监听的端口(文档用途)EXPOSE 8080
VOLUME声明数据卷挂载点VOLUME ["/data"]
ARG构建时的参数ARG VERSION=1.0
LABEL元数据标签LABEL maintainer="darren"
USER指定运行用户USER appuser
HEALTHCHECK健康检查HEALTHCHECK CMD curl -f http://localhost/

CMD vs ENTRYPOINT

这是初学者最容易混淆的两个指令:

CMD —— 默认命令,可被覆盖

FROM ubuntu
CMD ["echo", "Hello World"]
docker run myimage           # 输出: Hello World
docker run myimage echo Hi   # 输出: Hi(CMD 被覆盖)

ENTRYPOINT —— 入口命令,不易被覆盖

FROM ubuntu
ENTRYPOINT ["echo"]
CMD ["Hello World"]
docker run myimage           # 输出: Hello World(ENTRYPOINT + CMD)
docker run myimage Hi        # 输出: Hi(ENTRYPOINT + 覆盖的 CMD)

最佳实践

  • ENTRYPOINT: 定义容器的 “身份”(它是什么)
  • CMD: 定义默认参数(可以被用户覆盖)
ENTRYPOINT ["python"]
CMD ["app.py"]
# docker run myimage → python app.py
# docker run myimage test.py → python test.py

构建上下文与 .dockerignore

构建上下文

执行 docker build 时,Docker 会将指定目录的所有文件发送给 Docker Daemon:

docker build -t myapp .
#                     ^ 这个点就是构建上下文(当前目录)

.dockerignore

.gitignore 类似,告诉 Docker 哪些文件不需要发送到构建上下文。以下是完整的配置示例及每个忽略项的作用说明:

# .dockerignore
# 1. Git 版本控制目录:包含仓库元数据,镜像构建无需依赖,排除可减少上下文体积
.git
# 2. Node.js 依赖目录:通常体积大且可通过 npm install 重新安装,排除后避免重复传输
node_modules
# 3. Python 编译缓存目录:运行时自动生成,无需带入镜像
__pycache__
# 4. Python 编译后的字节码文件:同上,排除可减少上下文大小
*.pyc
# 5. 环境变量文件:包含敏感信息(如密钥)或本地配置,不应进入镜像
.env
# 6. Python 虚拟环境目录:本地开发依赖,镜像内可重新创建
.venv
# 7. 项目说明文档:仅用于阅读,与镜像运行无关
README.md
# 8. Docker Compose 配置文件:用于容器编排,非镜像构建必需
docker-compose*.yml
# 可选扩展:根据项目类型补充
# log/                # 日志目录:本地日志无需带入镜像
# build/              # 编译产物目录:如前端打包后的dist可按需复制,而非整个build
# *.log               # 日志文件:避免本地日志污染镜像
# .DS_Store           # MacOS 系统文件:无业务意义
# .vscode/            # 编辑器配置文件:仅本地开发使用

为什么重要:如果项目有大量不相关的文件(比如 node_modules),不使用 .dockerignore 会导致构建上下文传输缓慢,甚至可能将敏感文件意外带入镜像,同时增加镜像体积。


镜像分层原理(深入)

Dockerfile 中的每条指令都会创建一个新的镜像层:

FROM python:3.11-slim          # Layer 1: 基础镜像(约 120MB)
WORKDIR /app                   # Layer 2: 设置工作目录(几乎 0)
COPY requirements.txt .        # Layer 3: 复制依赖文件(几 KB)
RUN pip install -r requirements.txt  # Layer 4: 安装依赖(可能几十 MB)
COPY . .                       # Layer 5: 复制应用代码
CMD ["python", "app.py"]       # Layer 6: 元数据(0 size)

缓存机制

Docker 构建时会利用缓存加速:

  • 如果某一层的指令和上下文没有变化,直接使用缓存
  • 一旦某一层缓存失效,它之后的所有层都会重新构建

所以:把变化频率低的指令放前面,变化频率高的放后面

# ✗ 不好:每次代码改变都要重新安装依赖
COPY . .
RUN pip install -r requirements.txt

# ✓ 好:只有 requirements.txt 变了才重新安装依赖
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

实战示例

示例 1:Python Flask 应用

完整项目结构
flask-app/
├── .dockerignore       # 参考上文配置
├── Dockerfile
├── app.py              # 应用代码
└── requirements.txt    # 依赖清单
requirements.txt
flask==2.3.3
gunicorn==21.2.0  # 生产环境 WSGI 服务器
app.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return "Hello Docker! 🐳"

@app.route('/health')  # 健康检查接口
def health():
    return {"status": "ok"}, 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
Dockerfile
# 使用精简的基础镜像
FROM python:3.11-slim

# 设置工作目录
WORKDIR /app

# 复制依赖文件并安装(利用缓存)
COPY requirements.txt .
# --no-cache-dir:避免pip缓存,减少镜像体积
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用代码
COPY . .

# 声明端口(文档用途,实际映射需靠docker run -p)
EXPOSE 5000

# 健康检查配置
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD curl -f http://localhost:5000/health || exit 1

# 生产环境推荐使用gunicorn,而非内置服务器
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
实现步骤
# 1. 进入项目目录
cd flask-app

# 2. 构建 Docker 镜像
docker build -t flask-app:v1 .

# 3. 启动容器(后台运行,端口映射 5000:5000)
docker run -d --name flask-container -p 5000:5000 flask-app:v1

# 4. 查看容器运行状态
docker ps

# 5. 访问应用测试
curl http://localhost:5000
curl http://localhost:5000/health

# 6. 查看容器日志
docker logs flask-container

# 7. 停止并删除容器(结束使用)
docker stop flask-container
docker rm flask-container

示例 2:Node.js 应用

完整项目结构
node-app/
├── .dockerignore       # 参考上文配置
├── Dockerfile
├── server.js           # 应用代码
├── package.json        # 依赖清单
└── package-lock.json   # 依赖锁定文件
package.json
{
  "name": "docker-node-app",
  "version": "1.0.0",
  "dependencies": {
    "express": "^4.18.2"
  },
  "scripts": {
    "start": "node server.js"
  }
}
server.js
const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('Hello Docker Node.js! 🚀');
});

app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

app.listen(port, '0.0.0.0', () => {
  console.log(`App running on port ${port}`);
});
Dockerfile
# 使用alpine精简镜像,体积更小
FROM node:20-alpine

# 设置工作目录
WORKDIR /app

# 复制依赖清单(利用缓存)
COPY package*.json ./
# npm ci:严格按照package-lock.json安装,比npm install更稳定
RUN npm ci --only=production

# 复制应用代码
COPY . .

# 声明端口
EXPOSE 3000

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD wget -qO- http://localhost:3000/health || exit 1

# 启动应用
CMD ["npm", "start"]
实现步骤
# 1. 进入项目目录
cd node-app

# 2. 构建镜像
docker build -t node-app:v1 .

# 3. 启动容器(后台运行,端口映射 3000:3000)
docker run -d --name node-container -p 3000:3000 node-app:v1

# 4. 查看运行状态
docker ps

# 5. 访问测试
curl http://localhost:3000
curl http://localhost:3000/health

# 6. 查看日志
docker logs node-container

# 7. 停止并删除容器
docker stop node-container
docker rm node-container

示例 3:Go 应用(多阶段构建)

完整项目结构
go-app/
├── .dockerignore       # 参考上文配置
├── Dockerfile
├── go.mod              # Go 模块配置
├── go.sum              # 依赖校验文件
└── main.go             # 应用代码
go.mod
module docker-go-app

go 1.22

require github.com/gin-gonic/gin v1.9.1
main.go
package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    r.GET("/", func(c *gin.Context) {
        c.String(http.StatusOK, "Hello Docker Go! 🐹")
    })

    r.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })

    r.Run(":8080")
}
Dockerfile(多阶段构建)
# 第一阶段:构建阶段(使用完整Go镜像)
FROM golang:1.22-alpine AS builder

# 设置工作目录
WORKDIR /app

# 复制Go模块文件(利用缓存)
COPY go.mod go.sum ./
RUN go mod download

# 复制应用代码
COPY . .

# 编译Go程序:CGO_ENABLED=0 禁用CGO,生成静态二进制文件
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server .

# 第二阶段:运行阶段(使用极简alpine镜像)
FROM alpine:3.19

# 安装curl用于健康检查(alpine默认无curl)
RUN apk --no-cache add curl

# 从构建阶段复制编译后的二进制文件
COPY --from=builder /app/server /server

# 声明端口
EXPOSE 8080

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

# 启动应用
CMD ["/server"]
实现步骤
# 1. 进入项目目录
cd go-app

# 2. 构建镜像(多阶段自动完成)
docker build -t go-app:v1 .

# 3. 启动容器(后台运行,端口映射 8080:8080)
docker run -d --name go-container -p 8080:8080 go-app:v1

# 4. 查看运行状态
docker ps

# 5. 访问测试
curl http://localhost:8080
curl http://localhost:8080/health

# 6. 查看日志
docker logs go-container

# 7. 停止并删除容器
docker stop go-container
docker rm go-container

多阶段构建优势:最终镜像仅包含编译后的二进制文件,体积从几百 MB 降至几十 MB,减少攻击面,提升安全性。


构建命令

# 基本构建(指定镜像名)
docker build -t myapp .

# 指定 tag 版本
docker build -t myapp:v1.0 .

# 指定自定义Dockerfile路径
docker build -f Dockerfile.prod -t myapp:prod .

# 不使用缓存构建(强制重新构建所有层)
docker build --no-cache -t myapp .

# 传递构建参数(对应Dockerfile中的ARG)
docker build --build-arg VERSION=2.0 -t myapp .

# 查看构建的镜像
docker images myapp

# 构建后运行容器
docker run -d -p 5000:5000 --name myapp-container myapp

# 查看容器日志
docker logs myapp-container

# 检查健康状态
docker inspect --format '{{.State.Health.Status}}' myapp-container

Dockerfile 最佳实践

1. 使用精简基础镜像

# ✗ 完整镜像,约 900MB
FROM python:3.11

# ✓ slim 镜像,约 120MB(平衡体积和兼容性)
FROM python:3.11-slim

# ✓✓ alpine 镜像,约 50MB(但可能有兼容性问题,如缺少系统库)
FROM python:3.11-alpine

2. 合并 RUN 指令减少层数

# ✗ 三层(每层都会增加镜像体积)
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# ✓ 一层(用\换行,&&串联命令)
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*  # 清理apt缓存,减少镜像体积

3. 使用非 root 用户

# 创建无密码的普通用户
RUN adduser --disabled-password --gecos '' appuser
# 切换到普通用户运行容器(降低安全风险)
USER appuser

4. 添加 HEALTHCHECK

# 配置健康检查:每30秒检查一次,超时5秒,重试3次失败则标记为不健康
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

5. 利用 .dockerignore

始终创建 .dockerignore 文件排除不必要的文件(详见上文)。

6. 避免敏感信息硬编码

# ✗ 不安全:密钥硬编码
ENV DB_PASSWORD=123456

# ✓ 推荐:通过构建参数或运行时环境变量传入
ARG DB_PASSWORD
ENV DB_PASSWORD=$DB_PASSWORD
# 运行时:docker run -e DB_PASSWORD=123456 myapp

7. 清理构建缓存

# Python:禁用pip缓存
RUN pip install --no-cache-dir -r requirements.txt

# Node.js:使用npm ci并清理缓存
RUN npm ci --only=production && npm cache clean --force

# Ubuntu/Debian:清理apt缓存
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

实操练习 Lab 03:构建你的第一个镜像

练习目标

  1. 编写完整的 Dockerfile 构建 Python Flask 应用
  2. 配置 .dockerignore 优化构建上下文
  3. 构建镜像并运行容器
  4. 验证健康检查和端口映射

步骤 1:准备项目目录

# 创建项目目录
mkdir flask-docker-lab && cd flask-docker-lab

# 创建空文件
touch Dockerfile .dockerignore app.py requirements.txt

步骤 2:编写配置文件

1. requirements.txt
flask==2.3.3
gunicorn==21.2.0
2. app.py
from flask import Flask, jsonify

app = Flask(__name__)

# 首页接口
@app.route('/')
def index():
    return "<h1>My First Docker App</h1><p>Built with Dockerfile!</p>"

# 健康检查接口
@app.route('/health')
def health_check():
    return jsonify({
        "status": "healthy",
        "app": "flask-docker-lab",
        "version": "1.0"
    }), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=False)
3. .dockerignore
.git
__pycache__
*.pyc
.env
.venv
README.md
docker-compose*.yml
*.log
4. Dockerfile
# 基础镜像
FROM python:3.11-slim

# 设置工作目录
WORKDIR /app

# 创建非root用户
RUN adduser --disabled-password --gecos '' appuser

# 复制依赖文件并安装
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用代码
COPY . .

# 更改目录权限(确保普通用户可访问)
RUN chown -R appuser:appuser /app

# 切换到普通用户
USER appuser

# 声明端口
EXPOSE 5000

# 健康检查
HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
    CMD curl -f http://localhost:5000/health || exit 1

# 启动命令
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

步骤 3:构建镜像

# 构建镜像,命名为flask-lab:v1
docker build -t flask-lab:v1 .

# 查看镜像
docker images flask-lab:v1

步骤 4:运行容器

# 后台运行容器,映射端口5000:5000
docker run -d -p 5000:5000 --name flask-lab-container flask-lab:v1

# 查看容器状态
docker ps

# 查看健康状态(等待10秒后检查)
docker inspect --format '{{.State.Health.Status}}' flask-lab-container

步骤 5:验证应用

# 访问首页
curl http://localhost:5000

# 访问健康检查接口
curl http://localhost:5000/health

步骤 6:清理资源

# 停止容器
docker stop flask-lab-container

# 删除容器
docker rm flask-lab-container

# 删除镜像(可选)
docker rmi flask-lab:v1

练习拓展

  1. 尝试修改 app.py 代码,重新构建镜像,观察缓存是否生效
  2. 尝试添加环境变量(如 ENV APP_VERSION=1.0),在 app.py 中读取并返回
  3. 尝试使用 docker build --no-cache 强制重新构建,对比构建时间
  4. 尝试将基础镜像换成 alpine 版本,解决可能的依赖问题(如缺少 curl)

常见问题排查

  1. 构建上下文过大:检查 .dockerignore 是否配置正确,排除 node_modules、.venv 等大目录
  2. 缓存失效:确认指令顺序是否合理,变化频繁的指令(如 COPY . .)放在最后
  3. 权限问题:使用非 root 用户时,确保目录 / 文件权限正确(chown/chmod)
  4. 健康检查失败:确认接口路径正确,容器内已安装 curl/wget 等工具
  5. 端口映射失败:确认 EXPOSE 端口与 run -p 端口一致,容器内应用绑定 0.0.0.0 而非 127.0.0.1

本文为同步搬运内容,原创首发于个人独立博客网站:https://www.zheng-chang-ren.xyz
平台更新优先级说明:所有技术笔记、实验教程、踩坑总结均会优先发布、长期维护于个人独立博客;CSDN 仅作为辅助分发渠道。
若想查阅全部完整文集、获取最新首发内容,建议收藏并优先访问我的个人博客网站。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值