本教程侧重于命令实践和理解,提供可在本地环境测试的实例,每章结束都有总结要点。

目录

前边篇章介绍了

  1. Docker 基础概念和安装
  2. Docker 常用命令实践
  3. Docker 网络机制详解
  4. Docker 数据卷和挂载

本篇内容:

  1. Dockerfile 编写和镜像构建
  2. Docker Compose 多容器编排

本系列内容较多,建议在侧边栏根据需要点击目录进行跳转。

第5章:Dockerfile 编写和镜像构建

5.1 Dockerfile 基础

Dockerfile 是一个文本文件,包含了构建 Docker 镜像的所有指令。通过 Dockerfile,我们可以自动化地创建自定义镜像。

Dockerfile 基本结构

# 基础镜像
FROM ubuntu:20.04

# 维护者信息
LABEL maintainer="your-email@example.com"

# 设置工作目录
WORKDIR /app

# 复制文件
COPY . .

# 安装依赖
RUN apt-get update && apt-get install -y python3

# 暴露端口
EXPOSE 8080

# 启动命令
CMD ["python3", "app.py"]

第一个 Dockerfile

# 创建项目目录
mkdir -p /tmp/docker-tutorial/first-dockerfile
cd /tmp/docker-tutorial/first-dockerfile

# 创建应用文件
cat > app.py << 'EOF'
#!/usr/bin/env python3
print("Hello from my first Docker image!")
print("This is a simple Python application.")
EOF

# 创建 Dockerfile
cat > Dockerfile << 'EOF'
FROM python:3.11-slim

WORKDIR /app

COPY app.py .

CMD ["python", "app.py"]
EOF

# 构建镜像
docker build -t my-first-image .

# 运行容器
docker run --rm my-first-image

# 查看镜像
docker images my-first-image

5.2 Dockerfile 指令详解

FROM - 基础镜像

# 使用官方镜像
FROM python:3.11-slim

# 使用特定版本
FROM node:18.17.0-alpine

# 使用多阶段构建
FROM golang:1.21 AS builder
FROM alpine:latest AS runtime

RUN - 执行命令

# 单个命令
RUN apt-get update

# 多个命令(推荐)
RUN apt-get update && \
    apt-get install -y \
        curl \
        vim \
        git && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# 使用 shell 形式
RUN echo "Hello World"

# 使用 exec 形式
RUN ["echo", "Hello World"]

COPY 和 ADD

# COPY - 复制文件(推荐)
COPY app.py /app/
COPY requirements.txt /app/
COPY . /app/

# ADD - 复制文件(支持 URL 和自动解压)
ADD https://example.com/file.tar.gz /tmp/
ADD archive.tar.gz /app/

# 复制并设置权限
COPY --chown=1000:1000 app.py /app/

WORKDIR - 工作目录

# 设置工作目录
WORKDIR /app

# 相对路径(基于当前 WORKDIR)
WORKDIR subdir

# 绝对路径
WORKDIR /var/log

ENV - 环境变量

# 设置环境变量
ENV NODE_ENV=production
ENV PORT=3000
ENV DATABASE_URL=postgresql://localhost/mydb

# 一次设置多个
ENV NODE_ENV=production \
    PORT=3000 \
    DEBUG=false

EXPOSE - 暴露端口

# 暴露单个端口
EXPOSE 8080

# 暴露多个端口
EXPOSE 8080 8443

# 指定协议
EXPOSE 53/udp
EXPOSE 80/tcp

CMD 和 ENTRYPOINT

# CMD - 默认命令(可被覆盖)
CMD ["python", "app.py"]
CMD python app.py

# ENTRYPOINT - 入口点(不可被覆盖)
ENTRYPOINT ["python", "app.py"]

# 组合使用
ENTRYPOINT ["python"]
CMD ["app.py"]

为方便解释区别,以组合为例,比如:

FROM python:3.9
ENTRYPOINT ["python"]
CMD ["app.py"]

运行效果:

# 默认运行
docker run myapp
# 实际执行:python app.py

# 运行不同的 Python 脚本 | CMD 命令会被替换
docker run myapp test.py
# 实际执行:python test.py

# 传递参数给脚本
docker run myapp app.py --debug
# 实际执行:python app.py --debug

5.3 实践练习

练习1:Python Web 应用

# 创建 Python Web 应用项目
mkdir -p /tmp/docker-tutorial/python-webapp
cd /tmp/docker-tutorial/python-webapp

# 创建应用代码
cat > app.py << 'EOF'
from flask import Flask, jsonify
import os
import socket

app = Flask(__name__)

@app.route('/')
def hello():
    return jsonify({
        'message': 'Hello from Python Web App!',
        'hostname': socket.gethostname(),
        'environment': os.environ.get('ENVIRONMENT', 'development')
    })

@app.route('/health')
def health():
    return jsonify({'status': 'healthy'})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 5000)))
EOF

# 创建依赖文件
cat > requirements.txt << 'EOF'
Flask==2.3.3
gunicorn==21.2.0
EOF

# 创建 Dockerfile
cat > Dockerfile << 'EOF'
FROM python:3.11-slim

# 设置环境变量
ENV PYTHONUNBUFFERED=1
ENV PORT=5000
ENV ENVIRONMENT=production

# 创建应用用户
RUN groupadd -r appuser && useradd -r -g appuser appuser

# 设置工作目录
WORKDIR /app

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

# 复制应用代码
COPY app.py .

# 更改文件所有者
RUN chown -R appuser:appuser /app

# 切换到非 root 用户
USER appuser

# 暴露端口
EXPOSE 5000

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

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

# 构建镜像
docker build -t python-webapp .

# 运行容器
docker run -d --name webapp -p 5000:5000 python-webapp

# 测试应用
sleep 5
curl http://localhost:5000
curl http://localhost:5000/health

# 查看健康状态
docker ps

# 清理
docker stop webapp
docker rm webapp

练习2:Node.js 应用(多阶段构建)

# 创建 Node.js 项目
mkdir -p /tmp/docker-tutorial/nodejs-app
cd /tmp/docker-tutorial/nodejs-app

# 创建 package.json
cat > package.json << 'EOF'
{
  "name": "nodejs-docker-app",
  "version": "1.0.0",
  "description": "Node.js app with Docker",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}
EOF

# 创建服务器代码
cat > server.js << 'EOF'
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

app.get('/', (req, res) => {
    res.json({
        message: 'Hello from Node.js Docker app!',
        timestamp: new Date().toISOString(),
        environment: process.env.NODE_ENV || 'development'
    });
});

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

app.listen(port, '0.0.0.0', () => {
    console.log(`Server running on port ${port}`);
});
EOF

# 创建 .dockerignore
cat > .dockerignore << 'EOF'
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.nyc_output
coverage
.DS_Store
EOF

# 创建多阶段 Dockerfile
cat > Dockerfile << 'EOF'
# 构建阶段
FROM node:18-alpine AS builder

WORKDIR /app

# 复制 package 文件
COPY package*.json ./

# 安装所有依赖(包括开发依赖)
RUN npm ci --only=production && npm cache clean --force

# 运行阶段
FROM node:18-alpine AS runtime

# 创建应用用户
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nextjs -u 1001

WORKDIR /app

# 从构建阶段复制 node_modules
COPY --from=builder /app/node_modules ./node_modules

# 复制应用代码
COPY --chown=nextjs:nodejs server.js .
COPY --chown=nextjs:nodejs package*.json ./

# 切换到非 root 用户
USER nextjs

# 暴露端口
EXPOSE 3000

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

# 启动应用
CMD ["node", "server.js"]
EOF

# 构建镜像
docker build -t nodejs-app .

# 运行容器
docker run -d --name nodeapp -p 3000:3000 -e NODE_ENV=production nodejs-app

# 测试应用
sleep 5
curl http://localhost:3000
curl http://localhost:3000/health

# 查看镜像大小
docker images nodejs-app

# 清理
docker stop nodeapp
docker rm nodeapp

5.4 构建优化技巧

使用 .dockerignore

# 创建 .dockerignore 文件
cat > .dockerignore << 'EOF'
# Git
.git
.gitignore

# Documentation
README.md
docs/

# Dependencies
node_modules/
__pycache__/
*.pyc

# IDE
.vscode/
.idea/

# OS
.DS_Store
Thumbs.db

# Logs
*.log
logs/

# Test files
test/
tests/
*.test

# Build artifacts
dist/
build/
target/
EOF

docker 在构建镜像过程中,会忽略掉 .dockerignore 文件中指定的文件和目录。

层缓存优化

# 不好的做法 - 每次都重新安装依赖
FROM node:18-alpine
COPY . /app
WORKDIR /app
RUN npm install

# 好的做法 - 利用层缓存
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .

后者的做法利用了 Docker 的层缓存机制,只有在依赖配置文件(package.json 或 requirements.txt)发生变化时才会重新安装依赖,而业务代码的变化不会触发依赖安装。

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./    # ← 只复制依赖配置文件
RUN npm install          # ← 只有依赖变化时才重新安装
COPY . .                 # ← 最后复制业务代码

减少镜像层数

# 不好的做法 - 多个 RUN 指令
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN apt-get clean

# 好的做法 - 合并 RUN 指令
RUN apt-get update && \
    apt-get install -y curl git && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

5.5 构建上下文和高级技巧

构建参数 (ARG)

ARG NODE_VERSION=18
FROM node:${NODE_VERSION}-alpine

ARG BUILD_DATE
ARG VERSION
LABEL build_date=${BUILD_DATE}
LABEL version=${VERSION}

# 构建时传递参数
# docker build --build-arg NODE_VERSION=16 --build-arg VERSION=1.0.0 .

留意 ARG 和 ENV 使用场景的区别:

  • ARG = Argument(参数)→ 构建时的"参数",构建时用完就扔
  • ENV = Environment(环境)→ 运行时的"环境变量",运行时也能用

多平台构建

# 创建构建器
docker buildx create --name multiplatform --use

# 多平台构建
docker buildx build --platform linux/amd64,linux/arm64 -t my-app:latest .

# 推送到仓库
docker buildx build --platform linux/amd64,linux/arm64 -t my-app:latest --push .

注:大多数容器化应用都是 Linux 容器,通过 Docker Desktop 使用 manifest list (多架构清单) 来实现跨平台支持。

构建缓存

# 使用构建缓存
docker build --cache-from my-app:latest -t my-app:new .

# 禁用缓存
docker build --no-cache -t my-app .

# 查看构建历史
docker history my-app

5.6 安全最佳实践

使用非 root 用户

FROM alpine:latest

# 创建用户
RUN addgroup -g 1001 -S appgroup && \
    adduser -u 1001 -S appuser -G appgroup

# 设置工作目录权限
WORKDIR /app
RUN chown appuser:appgroup /app

# 切换用户
USER appuser

COPY --chown=appuser:appgroup . .

最小化攻击面

# 使用最小基础镜像
FROM alpine:latest

# 只安装必要的包
RUN apk add --no-cache ca-certificates

# 删除不必要的文件
RUN rm -rf /var/cache/apk/* /tmp/*

# 使用特定版本而非 latest
FROM node:18.17.0-alpine

本章总结

在本章中,我们深入学习了 Dockerfile 编写和镜像构建:

  1. Dockerfile 基础:掌握了 Dockerfile 的基本语法和常用指令
  2. 指令详解:深入理解了 FROM、RUN、COPY、WORKDIR、ENV、EXPOSE、CMD、ENTRYPOINT 等指令
  3. 实践练习:通过 Python 和 Node.js 应用构建加深理解
  4. 构建优化:学会了使用 .dockerignore、层缓存、减少层数等优化技巧
  5. 高级技巧:了解了构建参数、多平台构建、构建缓存等高级特性
  6. 安全实践:掌握了非 root 用户、最小化攻击面等安全最佳实践

关键概念总结

  • 多阶段构建:减少最终镜像大小,分离构建和运行环境
  • 层缓存:合理安排指令顺序,提高构建效率
  • 构建上下文:理解 Docker 构建过程中的文件传输机制
  • 镜像优化:通过各种技巧减少镜像大小和提高安全性

下一章我们将学习 Docker Compose,实现多容器应用的编排和管理。

第6章:Docker Compose 多容器编排

6.1 Docker Compose 简介

Docker Compose 是一个用于定义和运行多容器 Docker 应用的工具。通过 YAML 文件配置应用的服务,然后使用单个命令创建并启动所有服务。

为什么需要 Docker Compose

# 传统方式启动多容器应用(繁琐且容易出错)
docker network create app-network
docker run -d --name database --network app-network -e MYSQL_ROOT_PASSWORD=root mysql
docker run -d --name redis --network app-network redis
docker run -d --name web --network app-network -p 8080:80 nginx

# 使用 Docker Compose(简单且可重复)
docker-compose up -d

安装 Docker Compose

# 检查是否已安装
docker-compose --version

# macOS (通过 Docker Desktop 自带)
# 已包含在 Docker Desktop 中

# Linux 安装
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

# 验证安装
docker-compose --version

6.2 Docker Compose 文件结构

基本语法

version: '3.8'

services:
  service_name:
    image: image_name
    ports:
      - "host_port:container_port"
    environment:
      - ENV_VAR=value
    volumes:
      - host_path:container_path

networks:
  network_name:

volumes:
  volume_name:

6.3 Compose 文件详解

服务配置选项

version: '3.8'

services:
  app:
    # 使用镜像
    image: nginx:alpine
    
    # 或者构建镜像
    build:
      context: .
      dockerfile: Dockerfile
      args:
        - BUILD_ARG=value
    
    # 容器名称
    container_name: my-app
    
    # 端口映射
    ports:
      - "8080:80"
      - "443:443"
    
    # 环境变量
    environment:
      - NODE_ENV=production
      - DEBUG=false
    
    # 环境变量文件
    env_file:
      - .env
      - .env.local
    
    # 数据卷
    volumes:
      - ./data:/app/data
      - app_logs:/app/logs
    
    # 网络
    networks:
      - frontend
      - backend
    
    # 依赖关系
    depends_on:
      - database
      - redis
    
    # 重启策略
    restart: unless-stopped
    
    # 资源限制
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'
    
    # 健康检查
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost"]
      interval: 30s
      timeout: 10s
      retries: 3

这里包括几个关键概念:

  • image:使用的镜像名称(必填,除非使用 build 构建)
  • build:构建镜像的上下文和 Dockerfile 路径
  • container_name:容器名称,(可选,容器的唯一标识,默认为 {项目名}_{服务名}_{序号}
  • ports:端口映射
  • environment:环境变量
  • env_file:环境变量文件(默认读取 .env 文件)
  • volumes:数据卷挂载
  • networks:网络连接
  • depends_on:依赖服务(可选,确保服务启动顺序)
  • restart:重启策略(默认 no,可选 alwayson-failureunless-stopped
  • deploy:用于配置资源约束****、副本数量健康检查等部署相关设置。

网络配置

version: '3.8'

services:
  web:
    image: nginx
    networks:
      - frontend
  
  api:
    image: node:alpine
    networks:
      - frontend
      - backend
  
  database:
    image: mysql
    networks:
      - backend

networks: # 这里定义了两个网络
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # 内部网络,无法访问外网

此外,可以通过 external: true 参数来指定外部已存在的网络。留意二者的区别:

  • internal: true:只能和同一网络内的其他容器通信,无法访问外网。
  • external: true:声明这是一个已经存在的外部网络,在 docker-compose down 时不会删除这个网络。

数据卷配置

version: '3.8'

services:
  app:
    image: nginx
    volumes:
      - app_data:/app/data          # 命名卷
      - ./config:/app/config:ro     # 绑定挂载(只读)
      - /tmp:/app/tmp               # 绑定挂载

volumes:
  app_data:
    driver: local
  
  external_volume:
    external: true  # 使用外部已存在的卷

6.4 Compose 命令详解

基本命令

# 启动服务
docker-compose up                    # 前台启动
docker-compose up -d                 # 后台启动
docker-compose up --build            # 重新构建并启动
docker-compose up service_name       # 启动指定服务

# 停止服务
docker-compose stop                  # 停止所有服务
docker-compose stop service_name     # 停止指定服务
docker-compose down                  # 停止并删除容器、网络
docker-compose down -v               # 同时删除数据卷

# 查看状态
docker-compose ps                    # 查看服务状态
docker-compose logs                  # 查看所有日志
docker-compose logs -f service_name  # 实时查看指定服务日志

# 执行命令
docker-compose exec service_name command    # 在运行的容器中执行命令
docker-compose run service_name command     # 运行一次性命令

# 扩展服务
docker-compose up -d --scale web=3          # 扩展 web 服务到 3 个实例

配置管理

# 验证配置文件,展开默认值
docker-compose config

# 使用多个配置文件
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up

# 指定项目名称
docker-compose -p myproject up

# 使用环境变量,优先级高于 .env 文件
export COMPOSE_PROJECT_NAME=myapp
docker-compose up

6.6 高级特性

服务扩展和负载均衡

version: '3.8'

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - web

  web:
    build: .
    expose:
      - "3000"
    environment:
      - NODE_ENV=production

  database:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=rootpass
# 扩展 web 服务
docker-compose up -d --scale web=3

# nginx 配置负载均衡
upstream backend {
    server web_1:3000;
    server web_2:3000;
    server web_3:3000;
}

健康检查和依赖管理

version: '3.8'

services:
  web:
    image: nginx
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    depends_on:
      api:
        condition: service_healthy

  api:
    build: .
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

本章总结

在本章中,我们全面学习了 Docker Compose 多容器编排:

  1. Compose 基础:理解了 Docker Compose 的作用和基本概念
  2. 文件结构:掌握了 docker-compose.yml 的语法和配置选项
  3. 服务配置:学会了配置服务、网络、数据卷等各种选项
  4. 实践应用:通过完整的 Web 应用栈和开发环境配置加深理解
  5. 命令操作:掌握了 Compose 的各种命令和使用技巧
  6. 高级特性:了解了服务扩展、负载均衡、健康检查等高级功能

Compose vs 手动管理对比

  • 简化操作:一个命令启动整个应用栈
  • 配置管理:声明式配置,易于版本控制
  • 环境一致性:确保开发、测试、生产环境一致
  • 服务发现:自动的服务名称解析
  • 扩展性:轻松扩展服务实例数量

最后一章我们将学习 Docker 镜像管理和仓库操作,包括镜像的推送、拉取、私有仓库搭建等内容。

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐