Docker 完整教程

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

目录

  1. Docker 基础概念和安装
  2. Docker 常用命令实践
  3. Docker 网络机制详解
  4. Docker 数据卷和挂载
  5. Dockerfile 编写和镜像构建
  6. Docker Compose 多容器编排
  7. Docker 镜像管理和仓库操作

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


第1章:Docker 基础概念和安装

1.1 什么是 Docker

Docker 是一个开源的容器化平台,它允许开发者将应用程序及其依赖项打包到一个轻量级、可移植的容器中。

核心概念

  • 镜像 (Image):只读的模板,包含运行应用所需的代码、运行时、库、环境变量和配置文件
  • 容器 (Container):镜像的运行实例,是一个独立的进程
  • 仓库 (Repository):存储镜像的地方,如 Docker Hub

Docker vs 虚拟机

特性 Docker 容器 虚拟机
启动速度 秒级 分钟级
资源占用
隔离级别 进程级 操作系统级
移植性 中等

1.2 Docker 安装

方法1:进入官网,下载 Docker Desktop。

macOS 可以使用 Homebrew 安装:

1
brew install --cask docker

Linux (Ubuntu) 安装

1
2
3
4
5
6
# 更新包索引
sudo apt-get update
sudo apt install docker.io -y

# 将当前用户添加到 docker 组(避免每次使用 sudo)
sudo usermod -aG docker $USER

1.3 验证安装

1
2
3
4
5
6
7
8
# 检查 Docker 版本
docker --version

# 查看 Docker 系统信息
docker info

# 运行 Hello World 容器
# docker run hello-world

1.4 Docker 架构

Docker 使用客户端-服务器架构:

1
2
3
4
5
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│ Docker │ │ Docker │ │ Docker │
│ Client │───▶│ Daemon │───▶│ Registry │
│ │ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
  • Docker Client:用户与 Docker 交互的主要方式
  • Docker Daemon:监听 Docker API 请求并管理 Docker 对象
  • Docker Registry:存储 Docker 镜像的服务

1.5 实践练习

练习1:检查 Docker 状态

1
2
3
4
5
6
7
8
9
10
11
# 查看 Docker 服务状态
docker version

# 查看当前运行的容器
docker ps

# 查看所有容器(包括停止的)
docker ps -a

# 查看本地镜像
docker images

练习2:运行第一个容器

1
2
3
4
5
6
7
8
9
# 运行一个简单的 Ubuntu 容器
docker run -it ubuntu:20.04 /bin/bash

# 在容器内执行命令
echo "Hello from inside Docker container!"
cat /etc/os-release

# 退出容器
exit

练习3:后台运行容器

1
2
3
4
5
6
7
8
9
10
11
12
13
# 后台运行一个 nginx 服务器
docker run -d --name my-nginx -p 8080:80 nginx

# 检查容器状态
docker ps

# 在浏览器中访问 http://localhost:8080

# 停止容器
docker stop my-nginx

# 删除容器
docker rm my-nginx

1.6 常见问题排查

权限问题:

1
2
3
4
5
# 如果遇到权限错误,确保用户在 docker 组中
groups $USER

# 如果没有 docker 组,重新登录或执行
newgrp docker

服务未启动:

1
2
3
4
5
# Linux 系统启动 Docker 服务
sudo systemctl start docker

# 检查服务状态
sudo systemctl status docker

本章总结

在本章中,我们学习了:

  1. Docker 基本概念:了解了镜像、容器、仓库的概念和 Docker 与虚拟机的区别
  2. 安装 Docker:在不同操作系统上安装 Docker 的方法
  3. 验证安装:通过运行 hello-world 容器验证 Docker 正常工作
  4. Docker 架构:理解客户端-服务器架构模式
  5. 基础实践:运行第一个容器,体验 Docker 的基本操作

关键要点

  • Docker 容器比虚拟机更轻量、启动更快
  • 容器是镜像的运行实例
  • 通过 docker run 命令可以快速启动容器
  • 容器具有良好的隔离性和可移植性

下一章我们将深入学习 Docker 的常用命令,掌握容器的生命周期管理。


第2章:Docker 常用命令实践

2.1 镜像管理命令

拉取镜像

1
2
3
4
5
6
7
8
# 拉取最新版本镜像
docker pull nginx

# 拉取指定版本镜像
docker pull nginx:1.21

# 从指定仓库拉取
docker pull registry.cn-hangzhou.aliyuncs.com/library/nginx

查看镜像

1
2
3
4
5
6
7
8
9
10
11
# 列出本地所有镜像
docker images

# 查看镜像详细信息
docker inspect nginx

# 查看镜像历史
docker history nginx

# 搜索镜像
docker search python

删除镜像

1
2
3
4
5
6
7
8
9
10
11
# 删除指定镜像
docker rmi nginx:latest

# 删除多个镜像
docker rmi nginx redis mysql

# 强制删除镜像
docker rmi -f nginx

# 删除所有未使用的镜像
docker image prune

2.2 容器生命周期管理

创建和启动容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 运行容器(如果镜像不存在会自动拉取)
docker run nginx

# 后台运行容器
docker run -d nginx

# 指定容器名称
docker run -d --name my-nginx nginx

# 端口映射
docker run -d -p 8080:80 --name web-server nginx

# 环境变量
docker run -d -e MYSQL_ROOT_PASSWORD=123456 mysql

# 交互式运行
docker run -it ubuntu:20.04 /bin/bash

查看容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看运行中的容器
docker ps

# 查看所有容器(包括停止的)
docker ps -a

# 查看容器详细信息
docker inspect container_name

# 查看容器资源使用情况
docker stats # [container_name]

# 查看容器进程
docker top container_name

停止和删除容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 停止容器
docker stop container_name

# 强制停止容器
docker kill container_name

# 重启容器
docker restart container_name

# 删除容器
docker rm container_name

# 强制删除运行中的容器
docker rm -f container_name

# 删除所有停止的容器
docker container prune

2.3 容器交互命令

进入容器

1
2
3
4
5
6
7
8
# 在运行的容器中执行命令
docker exec -it container_name /bin/bash

# 执行单个命令
docker exec container_name ls -la

# 以 root 用户身份进入
docker exec -it --user root container_name /bin/bash

文件操作

1
2
3
4
5
6
7
8
# 从容器复制文件到主机
docker cp container_name:/path/to/file /host/path/

# 从主机复制文件到容器
docker cp /host/path/file container_name:/path/to/

# 查看容器文件系统变化
docker diff container_name

日志查看

1
2
3
4
5
6
7
8
9
10
11
# 查看容器日志
docker logs container_name

# 实时查看日志
docker logs -f container_name

# 查看最近的日志
docker logs --tail 100 container_name

# 查看指定时间的日志
docker logs --since "2023-01-01" container_name

2.4 实践练习

练习1:部署 Web 服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 创建一个临时目录用于练习
mkdir -p /tmp/docker-tutorial
cd /tmp/docker-tutorial

# 创建一个简单的 HTML 文件
cat > index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<title>My Docker Web Server</title>
</head>
<body>
<h1>Hello from Docker!</h1>
<p>This is served by nginx running in a Docker container.</p>
</body>
</html>
EOF

# 运行 nginx 容器并挂载 HTML 文件
docker run -d \
--name my-web-server \
-p 8080:80 \
-v $(pwd):/usr/share/nginx/html \
nginx

# 测试访问
curl http://localhost:8080

# 查看容器状态
docker ps

# 查看日志
docker logs my-web-server

# 清理
docker stop my-web-server
docker rm my-web-server

练习2:数据库容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 运行 MySQL 容器
docker run -d \
--name mysql-db \
-e MYSQL_ROOT_PASSWORD=mypassword \
-e MYSQL_DATABASE=testdb \
-p 3306:3306 \
mysql:8.0

# 等待数据库启动
sleep 30

# 连接到数据库
docker exec -it mysql-db mysql -uroot -pmypassword

# 在 MySQL 中执行命令
# SHOW DATABASES;
# USE testdb;
# CREATE TABLE users (id INT, name VARCHAR(50));
# INSERT INTO users VALUES (1, 'Alice');
# SELECT * FROM users;
# EXIT;

# 从外部连接数据库(如果安装了 mysql 客户端)
# mysql -h 127.0.0.1 -P 3306 -uroot -pmypassword

# 清理
docker stop mysql-db
docker rm mysql-db

练习3:Python 应用容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 创建 Python 应用目录
mkdir python-app
cd python-app

# 创建 Python 应用
cat > app.py << 'EOF'
from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
return f"Hello from Python app running in Docker!<br>Container ID: {os.uname().nodename}"

@app.route('/health')
def health():
return "OK"

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
EOF

# 创建 requirements.txt
echo "Flask==2.3.3" > requirements.txt

# 运行 Python 容器
docker run -d \
--name python-app \
-p 5000:5000 \
-v $(pwd):/app \
-w /app \
python:3.11-slim \
bash -c "pip install -r requirements.txt && python app.py"

# 等待应用启动
sleep 10

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

# 查看日志
docker logs python-app

# 清理
docker stop python-app
docker rm python-app
cd ..

2.5 高级命令技巧

批量操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 停止所有运行的容器
docker stop $(docker ps -q)

# 删除所有停止的容器
docker rm $(docker ps -aq)

# 删除所有镜像
docker rmi $(docker images -q)

# 系统清理(删除未使用的容器、网络、镜像)
docker system prune

# 完全清理(包括数据卷)
docker system prune -a --volumes

资源限制

1
2
3
4
5
6
7
8
9
10
11
# 限制内存使用
docker run -d --memory="512m" nginx

# 限制 CPU 使用
docker run -d --cpus="1.5" nginx

# 限制 CPU 和内存
docker run -d --memory="1g" --cpus="2" nginx

# 查看资源使用情况
docker stats

网络配置

1
2
3
4
5
6
7
8
9
10
11
12
# 指定网络模式
docker run -d --network host nginx

# 创建自定义网络
docker network create my-network

# 在自定义网络中运行容器
docker run -d --network my-network --name app1 nginx
docker run -d --network my-network --name app2 nginx

# 容器间可以通过名称通信
docker exec app1 ping app2

关于网络机制,这里有很多细节需要展开,我们在 第3章 中详细讲解。

2.6 常见问题和解决方案

端口冲突

1
2
3
4
5
# 查看端口占用
sudo netstat -tulpn | grep :8080

# 使用不同端口
docker run -d -p 8081:80 nginx

容器无法启动

1
2
3
4
5
6
7
8
# 查看详细错误信息
docker logs container_name

# 检查容器配置
docker inspect container_name

# 以交互模式调试
docker run -it image_name /bin/bash

镜像拉取失败

1
2
3
4
5
6
7
8
9
# 使用国内镜像源
docker pull registry.cn-hangzhou.aliyuncs.com/library/nginx

# 配置镜像加速器(编辑 /etc/docker/daemon.json)
{
"registry-mirrors": [
"https://registry.cn-hangzhou.aliyuncs.com"
]
}

本章总结

在本章中,我们深入学习了 Docker 的常用命令:

  1. 镜像管理:掌握了镜像的拉取、查看、删除等操作
  2. 容器生命周期:学会了容器的创建、启动、停止、删除
  3. 容器交互:了解了如何进入容器、查看日志、复制文件
  4. 实践应用:通过 Web 服务器、数据库、Python 应用的部署加深理解
  5. 高级技巧:掌握了批量操作、资源限制、网络配置等高级用法

关键命令总结

  • docker run:创建并运行容器
  • docker ps:查看容器状态
  • docker exec:在容器中执行命令
  • docker logs:查看容器日志
  • docker stop/start/restart:控制容器状态

最佳实践

  • 为容器指定有意义的名称
  • 合理使用端口映射和数据卷
  • 定期清理未使用的容器和镜像
  • 使用资源限制防止容器占用过多资源

下一章我们将学习 Docker 网络机制,了解容器间如何通信。


第3章:Docker 网络机制详解

3.1 Docker 网络基础

Docker 网络是容器间通信和容器与外部世界通信的基础。理解网络机制对于构建复杂的容器化应用至关重要。

每个 Docker 容器都有自己的网络命名空间,包括:独立的网络接口,路由表,iptables 规则和端口空间。

查看网络信息

1
2
3
4
5
6
7
8
# 列出所有网络
docker network ls

# 查看网络详细信息
docker network inspect bridge

# 查看容器网络配置
docker inspect container_name | grep -A 20 "NetworkSettings"

3.2 Docker 网络驱动类型

运行 docker network ls 查看网络,比如

1
2
3
4
5
6
7
❯ docker network ls

NETWORK ID NAME DRIVER SCOPE
fef63891b2a7 bridge bridge local
150f1dc61078 nginx_default bridge local
ce0e3369b5e1 host host local
050afab805f4 none null local

可以看到存在三种类型:bridge, host, none。

1. Bridge 网络(默认)

Bridge 是 Docker 的默认网络驱动,适用于单主机上的容器通信。

  • 作用:为容器提供独立的网络命名空间,同时允许容器间通信
  • 特点:容器获得私有 IP(通常是 172.17.x.x),通过 NAT 访问外网
  • 使用场景:大部分单机容器应用的默认选择

常用命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看默认 bridge 网络
docker network inspect bridge

# 运行容器使用默认网络
docker run -d --name app1 nginx
docker run -d --name app2 nginx

# 查看容器 IP
docker inspect app1 | grep IPAddress
docker inspect app2 | grep IPAddress

# 容器间通信测试
docker exec app1 ping $(docker inspect app2 | grep IPAddress | head -1 | cut -d'"' -f4)

2. Host 网络

Host 网络模式下,容器直接使用主机的网络栈。

  • 特点:容器直接使用主机的网络栈,没有网络隔离
  • 性能:网络性能最好,没有 NAT 转换开销
  • 风险:安全性较低,容器可以直接访问主机网络
1
2
3
4
5
6
7
8
# 使用 host 网络运行容器
docker run -d --network host --name host-app nginx

# 查看网络配置(与主机相同)
docker exec host-app ip addr show

# 直接通过主机 IP 访问
curl http://localhost:80

3. None 网络

None 网络模式下,容器没有网络接口。

1
2
3
4
5
# 运行无网络容器
docker run -d --network none --name no-network alpine sleep 3600

# 查看网络接口(只有 loopback)
docker exec no-network ip addr show

4. 自定义 Bridge 网络

自定义网络提供更好的隔离性和容器间的名称解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 创建自定义网络
docker network create --driver bridge my-network

# 查看网络详情
docker network inspect my-network

# 在自定义网络中运行容器
docker run -d --network my-network --name web nginx
docker run -d --network my-network --name db mysql:8.0 -e MYSQL_ROOT_PASSWORD=password

# 容器间可以通过名称通信
docker exec web ping db
docker exec db ping web

5. 自定义 Bridge 网络的 IP 段

Docker 默认使用 172.17.0.0/16 网段,如果服务器的 172.xx 网段被占用,为避免冲突,可以通过 /etc/docker/daemon.json 修改全局默认配置,举个例子:

1
2
3
4
5
6
7
8
# 编辑 /etc/docker/daemon.json
{
"bip": "100.10.100.1/24",
"default-address-pools":
[
{"base":"100.10.0.0/16","size":24}
]
}

配置说明:

  • bip:设置默认 bridge 网络的 IP 段
  • default-address-pools:设置自定义网络的默认 IP 池
1
2
# 重启 Docker 服务使配置生效
sudo systemctl restart docker

当然,也可以为单个容器创建指定 IP 段网络,比如

1
2
3
4
5
6
```bash
# 创建开发环境网络
docker network create \
--subnet=10.10.0.0/24 \
--gateway=10.10.0.1 \
dev-network

3.3 端口映射

基本端口映射

通过 -p <host_port>:<container_port> 可以将容器端口映射到主机端口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 映射单个端口
docker run -d -p 8080:80 nginx

# 映射多个端口
docker run -d -p 8080:80 -p 8443:443 nginx

# 映射到指定 IP
docker run -d -p 127.0.0.1:8080:80 nginx

# 映射随机端口
docker run -d -P nginx

# 查看端口映射
docker port container_name

高级端口配置

1
2
3
4
5
6
7
8
# UDP 端口映射
docker run -d -p 53:53/udp nginx

# 端口范围映射
docker run -d -p 8000-8010:8000-8010 nginx

# 查看所有端口映射
docker ps --format "table {{.Names}}\t{{.Ports}}"

3.4 实践练习

练习1:Web 应用 + 数据库通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 创建自定义网络
docker network create webapp-network

# 启动数据库容器
docker run -d \
--name database \
--network webapp-network \
-e MYSQL_ROOT_PASSWORD=rootpass \
-e MYSQL_DATABASE=webapp \
mysql:8.0

# 等待数据库启动
sleep 30

# 启动 Web 应用容器
docker run -d \
--name webapp \
--network webapp-network \
-p 8080:80 \
nginx

# 测试容器间连通性
docker exec webapp ping database

# 在 Web 容器中安装网络工具并测试数据库连接
docker exec webapp apt-get update
docker exec webapp apt-get install -y telnet
docker exec webapp telnet database 3306

# 清理
docker stop webapp database
docker rm webapp database
docker network rm webapp-network

练习2:负载均衡配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 创建负载均衡网络
docker network create lb-network

# 启动多个后端服务
docker run -d --name backend1 --network lb-network nginx
docker run -d --name backend2 --network lb-network nginx
docker run -d --name backend3 --network lb-network nginx

# 创建 nginx 配置文件
mkdir -p /tmp/docker-tutorial/nginx-lb
cat > /tmp/docker-tutorial/nginx-lb/nginx.conf << 'EOF'
events {
worker_connections 1024;
}

http {
upstream backend {
server backend1:80;
server backend2:80;
server backend3:80;
}

server {
listen 80;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
EOF

# 启动负载均衡器
docker run -d \
--name load-balancer \
--network lb-network \
-p 8080:80 \
-v /tmp/docker-tutorial/nginx-lb/nginx.conf:/etc/nginx/nginx.conf \
nginx

# 测试负载均衡
for i in {1..6}; do
curl -s http://localhost:8080 | grep -o "backend[0-9]" || echo "Request $i"
done

# 清理
docker stop load-balancer backend1 backend2 backend3
docker rm load-balancer backend1 backend2 backend3
docker network rm lb-network

3.5 网络故障排查

常用网络调试命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 在容器中安装网络工具
docker exec -it container_name bash
apt-get update && apt-get install -y iputils-ping telnet curl netcat

# 测试网络连通性
ping target_host
telnet target_host port
curl http://target_host:port
nc -zv target_host port

# 查看网络接口
ip addr show
ip route show

# 查看端口监听
netstat -tulpn
ss -tulpn

常见网络问题

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1. 容器无法访问外网
docker run --rm busybox ping google.com

# 2. 容器间无法通信
docker exec container1 ping container2

# 3. 端口映射不生效
docker port container_name
netstat -tulpn | grep port_number

# 4. DNS 解析问题
docker exec container_name nslookup google.com
docker exec container_name cat /etc/resolv.conf

3.6 高级网络配置

网络别名

网络别名最大的用途是实现简单的负载均衡。多个容器可以使用同一个别名,Docker 会自动进行 DNS 轮询。

1
2
3
4
5
6
7
8
9
10
11
# 创建网络
docker network create test-network

# 为容器设置网络别名
docker run -d --name web1 --network web-cluster --network-alias webapp nginx
docker run -d --name web2 --network web-cluster --network-alias webapp nginx
docker run -d --name web3 --network web-cluster --network-alias webapp nginx

# 测试别名解析
docker run --rm --network web-cluster busybox nslookup webapp
# 会看到多个 IP 地址,每次访问会轮询到不同的容器

多网络连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 创建多个网络
docker network create frontend
docker network create backend

# 启动数据库(仅在后端网络)
docker run -d --name db --network backend mysql:8.0 -e MYSQL_ROOT_PASSWORD=pass

# 启动应用服务器(连接两个网络)
docker run -d --name app nginx
docker network connect frontend app
docker network connect backend app

# 启动前端(仅在前端网络)
docker run -d --name web --network frontend nginx

# 验证网络连接
docker exec app ping db # 应该成功
docker exec web ping app # 应该成功
docker exec web ping db # 应该失败

网络策略和安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 创建隔离网络
docker network create --internal secure-network

# 在隔离网络中的容器无法访问外网
docker run -d --name isolated --network secure-network nginx
docker exec isolated ping google.com # 应该失败

# 使用自定义 IPAM
docker network create \
--driver bridge \
--subnet=192.168.100.0/24 \
--ip-range=192.168.100.128/25 \
--gateway=192.168.100.1 \
custom-subnet

# 指定容器 IP
docker run -d --name fixed-ip --network custom-subnet --ip 192.168.100.130 nginx

本章总结

在本章中,我们全面学习了 Docker 网络机制:

  1. 网络基础:理解了 Docker 网络的基本概念和命名空间隔离
  2. 网络驱动:掌握了 Bridge、Host、None 和自定义网络的使用
  3. 端口映射:学会了各种端口映射配置和高级用法
  4. 实践应用:通过 Web 应用、微服务、负载均衡等场景加深理解
  5. 故障排查:了解了网络问题的诊断和解决方法
  6. 高级配置:掌握了网络别名、多网络连接、安全策略等高级特性

关键概念总结

  • 网络隔离:每个容器都有独立的网络命名空间
  • 服务发现:自定义网络中容器可以通过名称互相访问
  • 端口映射:将容器端口暴露到主机上
  • 网络安全:通过网络隔离实现安全边界

最佳实践要点

  • 为不同的应用创建独立的自定义网络
  • 避免使用默认 bridge 网络进行生产部署
  • 合理规划端口映射,避免端口冲突
  • 使用网络别名实现服务发现
  • 实施网络隔离提高安全性

常见应用场景

  • 前后端分离应用的网络隔离
  • 负载均衡和服务发现
  • 多环境部署的网络规划
  • 容器集群的网络管理

下一章我们将学习 Docker 数据卷和挂载,了解如何持久化容器数据。


第4章:Docker 数据卷和挂载

4.1 数据持久化的重要性

容器是无状态的,当容器删除时,容器内的数据也会丢失。为了实现数据持久化,Docker 提供了多种数据存储方案。

数据丢失问题演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 创建一个容器并写入数据
docker run -it --name temp-container ubuntu:20.04 bash

# 在容器内创建文件
echo "Important data" > /tmp/important.txt
cat /tmp/important.txt
exit

# 删除容器
docker rm temp-container

# 重新创建同名容器,数据已丢失
docker run -it --name temp-container ubuntu:20.04 bash
cat /tmp/important.txt # 文件不存在
exit
docker rm temp-container

4.2 Docker 存储类型

Docker 提供三种主要的数据存储方式:

1. Volumes(数据卷)

数据卷是 Docker 管理的存储区域,存储在主机文件系统中,但完全由 Docker 管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 创建数据卷
docker volume create my-volume

# 查看所有数据卷
docker volume ls

# 查看数据卷详细信息
docker volume inspect my-volume

# 使用数据卷运行容器
docker run -d --name web-server -v my-volume:/usr/share/nginx/html nginx

# 在另一个容器中访问同一数据卷
docker run -it --rm -v my-volume:/data ubuntu:20.04 bash

# 在容器内创建文件
echo "<h1>Hello from Volume!</h1>" > /data/index.html
exit

# 测试 Web 服务器
curl http://localhost:80 # 如果映射了端口

# 清理
docker stop web-server
docker rm web-server
docker volume rm my-volume

2. Bind Mounts(绑定挂载)

绑定挂载将主机文件系统的目录或文件直接挂载到容器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 创建主机目录
mkdir -p /tmp/docker-tutorial/html
echo "<h1>Hello from Bind Mount!</h1>" > /tmp/docker-tutorial/html/index.html

# 使用绑定挂载运行容器
docker run -d \
--name bind-web \
-p 8080:80 \
-v /tmp/docker-tutorial/html:/usr/share/nginx/html \
nginx

# 测试访问
curl http://localhost:8080

# 在主机上修改文件
echo "<h1>Updated from Host!</h1>" > /tmp/docker-tutorial/html/index.html

# 再次测试,内容已更新
curl http://localhost:8080

# 清理
docker stop bind-web
docker rm bind-web

3. tmpfs Mounts(临时文件系统挂载)

tmpfs 挂载将数据存储在主机内存中,容器停止时数据会丢失。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 使用 tmpfs 挂载
docker run -d \
--name tmpfs-test \
--tmpfs /tmp:rw,noexec,nosuid,size=100m \
nginx

# 查看挂载信息
docker exec tmpfs-test mount | grep tmpfs

# 在 tmpfs 中写入数据
docker exec tmpfs-test bash -c "echo 'Temporary data' > /tmp/temp.txt"
docker exec tmpfs-test cat /tmp/temp.txt

# 重启容器,数据丢失
docker restart tmpfs-test
docker exec tmpfs-test cat /tmp/temp.txt # 文件不存在

# 清理
docker stop tmpfs-test
docker rm tmpfs-test

4.3 数据卷管理

数据卷操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 创建数据卷
docker volume create app-data
docker volume create db-data
docker volume create logs

# 列出所有数据卷
docker volume ls

# 查看数据卷详细信息
docker volume inspect app-data

# 删除数据卷
docker volume rm logs

# 删除所有未使用的数据卷
docker volume prune

# 查看数据卷使用情况
docker system df

数据卷备份和恢复

这里 --rm 表示创建后删除停止的容器,常用语一次性任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 创建测试数据
docker volume create backup-demo
docker run --rm -v backup-demo:/data ubuntu:20.04 bash -c "
echo 'Important data 1' > /data/file1.txt
echo 'Important data 2' > /data/file2.txt
mkdir /data/subdir
echo 'Subdirectory data' > /data/subdir/file3.txt
"

# 备份数据卷
docker run --rm \
-v backup-demo:/source \
-v $(pwd):/backup \
ubuntu:20.04 \
tar czf /backup/backup-demo.tar.gz -C /source .

# 验证备份文件
ls -la backup-demo.tar.gz

# 创建新数据卷用于恢复
docker volume create restore-demo

# 恢复数据
docker run --rm \
-v restore-demo:/target \
-v $(pwd):/backup \
ubuntu:20.04 \
tar xzf /backup/backup-demo.tar.gz -C /target

# 验证恢复的数据
docker run --rm -v restore-demo:/data ubuntu:20.04 find /data -type f -exec cat {} \;

# 清理
docker volume rm backup-demo restore-demo
rm backup-demo.tar.gz

4.4 实践练习

练习1:数据库数据持久化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 创建数据库数据卷
docker volume create mysql-data

# 启动 MySQL 容器
docker run -d \
--name mysql-persistent \
-e MYSQL_ROOT_PASSWORD=mypassword \
-e MYSQL_DATABASE=testdb \
-v mysql-data:/var/lib/mysql \
-p 3306:3306 \
mysql:8.0

# 等待数据库启动
sleep 30

# 连接数据库并创建数据
docker exec -it mysql-persistent mysql -uroot -pmypassword -e "
USE testdb;
CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50));
INSERT INTO users (name) VALUES ('Alice'), ('Bob'), ('Charlie');
SELECT * FROM users;
"

# 停止并删除容器
docker stop mysql-persistent
docker rm mysql-persistent

# 使用相同数据卷重新启动容器
docker run -d \
--name mysql-restored \
-e MYSQL_ROOT_PASSWORD=mypassword \
-v mysql-data:/var/lib/mysql \
-p 3306:3306 \
mysql:8.0

# 等待启动
sleep 30

# 验证数据仍然存在
docker exec -it mysql-restored mysql -uroot -pmypassword -e "
USE testdb;
SELECT * FROM users;
"

# 清理
docker stop mysql-restored
docker rm mysql-restored
docker volume rm mysql-data

练习2:多容器数据共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 创建共享数据卷
docker volume create shared-data

# 启动数据生产者容器
docker run -d \
--name producer \
-v shared-data:/data \
ubuntu:20.04 \
bash -c "
while true; do
echo \"\$(date): Producer data\" >> /data/producer.log
sleep 5
done
"

# 启动数据消费者容器
docker run -d \
--name consumer \
-v shared-data:/data \
ubuntu:20.04 \
bash -c "
while true; do
if [ -f /data/producer.log ]; then
echo \"Consumer read: \$(tail -1 /data/producer.log)\"
fi
sleep 3
done
"

# 启动数据处理器容器
docker run -d \
--name processor \
-v shared-data:/data \
ubuntu:20.04 \
bash -c "
while true; do
if [ -f /data/producer.log ]; then
wc -l /data/producer.log > /data/stats.txt
echo \"Processed at \$(date)\" >> /data/stats.txt
fi
sleep 10
done
"

# 查看各容器日志
echo "Producer logs:"
docker logs producer --tail 5

echo "Consumer logs:"
docker logs consumer --tail 5

echo "Processor logs:"
docker logs processor --tail 5

# 查看共享数据
docker run --rm -v shared-data:/data ubuntu:20.04 ls -la /data
docker run --rm -v shared-data:/data ubuntu:20.04 cat /data/stats.txt

# 清理
docker stop producer consumer processor
docker rm producer consumer processor
docker volume rm shared-data

4.5 高级挂载选项

只读挂载

1
2
3
4
5
6
7
8
9
10
11
12
# 只读绑定挂载
mkdir -p /tmp/docker-tutorial/readonly-data
echo "Read-only data" > /tmp/docker-tutorial/readonly-data/config.txt

docker run -it --rm \
-v /tmp/docker-tutorial/readonly-data:/data:ro \
ubuntu:20.04 bash

# 在容器内尝试写入(应该失败)
# echo "new data" > /data/config.txt # Permission denied
# cat /data/config.txt # 可以读取
# exit

挂载单个文件

1
2
3
4
5
6
7
8
9
10
11
# 创建配置文件
echo "database_host=localhost" > /tmp/docker-tutorial/app.conf

# 挂载单个文件
docker run -it --rm \
-v /tmp/docker-tutorial/app.conf:/app/config/app.conf:ro \
ubuntu:20.04 bash

# 在容器内查看文件
# cat /app/config/app.conf
# exit

挂载选项

1
2
3
4
5
6
7
8
9
10
11
12
# 使用特定的挂载选项
docker run -d \
--name mount-options-test \
--mount type=bind,source=/tmp/docker-tutorial,target=/data,readonly \
nginx

# 查看挂载信息
docker inspect mount-options-test | grep -A 10 "Mounts"

# 清理
docker stop mount-options-test
docker rm mount-options-test

4.6 性能和最佳实践

性能考虑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 比较不同存储类型的性能
# 1. 数据卷性能测试
docker volume create perf-volume
docker run --rm \
-v perf-volume:/data \
ubuntu:20.04 \
bash -c "time dd if=/dev/zero of=/data/test bs=1M count=100"

# 2. 绑定挂载性能测试
mkdir -p /tmp/docker-tutorial/perf-bind
docker run --rm \
-v /tmp/docker-tutorial/perf-bind:/data \
ubuntu:20.04 \
bash -c "time dd if=/dev/zero of=/data/test bs=1M count=100"

# 3. tmpfs 性能测试
docker run --rm \
--tmpfs /data:rw,size=200m \
ubuntu:20.04 \
bash -c "time dd if=/dev/zero of=/data/test bs=1M count=100"

# 清理
docker volume rm perf-volume
rm -rf /tmp/docker-tutorial/perf-bind

三种方式比较:

存储类型 性能特点 适用场景
tmpfs 最快 临时文件、缓存、高频读写的临时数据
数据卷 (Volume) 较快 生产环境的首选
绑定挂载 (Bind Mount) 最慢 开发调试、直接访问宿主机文件

最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 1. 使用命名数据卷而不是匿名数据卷
docker volume create app-logs
docker run -d -v app-logs:/var/log/app nginx

# 2. 为不同类型的数据使用不同的数据卷
docker volume create app-data # 应用数据
docker volume create app-config # 配置文件
docker volume create app-logs # 日志文件

# 3. 定期备份重要数据卷
docker run --rm \
-v app-data:/source:ro \
-v $(pwd):/backup \
ubuntu:20.04 \
tar czf /backup/app-data-$(date +%Y%m%d).tar.gz -C /source .

# 4. 监控数据卷使用情况
docker system df -v

# 清理
docker volume rm app-logs app-data app-config

本章总结

在本章中,我们深入学习了 Docker 数据卷和挂载机制:

  1. 数据持久化重要性:理解了容器数据丢失问题和持久化的必要性
  2. 存储类型:掌握了 Volumes、Bind Mounts、tmpfs Mounts 三种存储方式
  3. 数据卷管理:学会了数据卷的创建、查看、备份、恢复等操作
  4. 实践应用:通过数据库持久化、开发环境同步、多容器数据共享等场景加深理解
  5. 高级选项:了解了只读挂载、单文件挂载、挂载选项等高级用法
  6. 性能优化:掌握了不同存储类型的性能特点和最佳实践

关键概念总结

  • 数据卷:Docker 管理的持久化存储,推荐用于生产环境
  • 绑定挂载:直接挂载主机目录,适合开发环境
  • tmpfs 挂载:内存存储,适合临时数据
  • 数据共享:多个容器可以共享同一个数据卷

存储选择指南

  • 生产数据:使用 Volumes
  • 开发调试:使用 Bind Mounts
  • 临时缓存:使用 tmpfs Mounts
  • 配置文件:使用只读 Bind Mounts
  • 日志文件:使用 Volumes 或 Bind Mounts

下一章我们将学习 Dockerfile 编写和镜像构建,了解如何创建自定义镜像。


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

5.1 Dockerfile 基础

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

Dockerfile 基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 基础镜像
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 创建项目目录
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 - 基础镜像

1
2
3
4
5
6
7
8
9
# 使用官方镜像
FROM python:3.11-slim

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

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

RUN - 执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 单个命令
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

1
2
3
4
5
6
7
8
9
10
11
# 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 - 工作目录

1
2
3
4
5
6
7
8
# 设置工作目录
WORKDIR /app

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

# 绝对路径
WORKDIR /var/log

ENV - 环境变量

1
2
3
4
5
6
7
8
9
# 设置环境变量
ENV NODE_ENV=production
ENV PORT=3000
ENV DATABASE_URL=postgresql://localhost/mydb

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

EXPOSE - 暴露端口

1
2
3
4
5
6
7
8
9
# 暴露单个端口
EXPOSE 8080

# 暴露多个端口
EXPOSE 8080 8443

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

CMD 和 ENTRYPOINT

1
2
3
4
5
6
7
8
9
10
# CMD - 默认命令(可被覆盖)
CMD ["python", "app.py"]
CMD python app.py

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

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

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

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

运行效果:

1
2
3
4
5
6
7
8
9
10
11
# 默认运行
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 应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# 创建 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 应用(多阶段构建)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# 创建 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 创建 .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 文件中指定的文件和目录。

层缓存优化

1
2
3
4
5
6
7
8
9
10
11
12
# 不好的做法 - 每次都重新安装依赖
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)发生变化时才会重新安装依赖,而业务代码的变化不会触发依赖安装。

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

减少镜像层数

1
2
3
4
5
6
7
8
9
10
11
# 不好的做法 - 多个 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)

1
2
3
4
5
6
7
8
9
10
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(环境)→ 运行时的"环境变量",运行时也能用

多平台构建

1
2
3
4
5
6
7
8
# 创建构建器
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 (多架构清单) 来实现跨平台支持。

构建缓存

1
2
3
4
5
6
7
8
# 使用构建缓存
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 用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 . .

最小化攻击面

1
2
3
4
5
6
7
8
9
10
11
# 使用最小基础镜像
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

1
2
3
4
5
6
7
8
# 传统方式启动多容器应用(繁琐且容易出错)
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

1
2
3
4
5
6
7
8
9
10
11
12
# 检查是否已安装
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 文件结构

基本语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 文件详解

服务配置选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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:用于配置资源约束****、副本数量健康检查等部署相关设置。

网络配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 时不会删除这个网络。

数据卷配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 命令详解

基本命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 启动服务
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 个实例

配置管理

1
2
3
4
5
6
7
8
9
10
11
12
# 验证配置文件,展开默认值
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 高级特性

服务扩展和负载均衡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
1
2
3
4
5
6
7
8
9
# 扩展 web 服务
docker-compose up -d --scale web=3

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

健康检查和依赖管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 镜像管理和仓库操作,包括镜像的推送、拉取、私有仓库搭建等内容。

第7章:Docker 镜像管理和仓库操作

7.1 Docker 镜像管理基础

镜像的生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 镜像生命周期概览
构建 -> 标记 -> 推送 -> 拉取 -> 运行 -> 删除

# 查看本地镜像
docker images
docker image ls

# 查看镜像详细信息
docker image inspect nginx:alpine

# 查看镜像历史
docker image history nginx:alpine

# 查看镜像大小
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"

镜像标记和命名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 镜像命名规范
[registry_host[:port]/]username/repository[:tag]

# 示例
docker.io/library/nginx:latest # 官方镜像,前缀 docker.io/library/ 可以省略
docker.io/username/myapp:v1.0 # 用户镜像,前缀 docker.io/ 可以省略
registry.example.com/team/app:latest # 私有仓库镜像

# 为镜像添加标记
docker tag nginx:alpine myregistry.com/nginx:v1.0
docker tag myapp:latest myapp:v1.0
docker tag myapp:latest myapp:production

# 查看同一镜像的多个标记,需匹配到 username/repository
docker images myapp

镜像清理和优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 删除单个镜像
docker rmi nginx:alpine
docker image rm nginx:alpine

# 删除多个镜像
docker rmi $(docker images -q ubuntu)

# 删除悬空镜像(dangling images)
docker image prune

# 删除所有未使用的镜像
docker image prune -a

# 删除指定时间前的镜像
docker image prune -a --filter "until=24h"

# 查看镜像占用空间
docker system df

# 全面清理系统
docker system prune -a --volumes

7.2 Docker Hub 操作

Docker Hub 基本操作

1
2
3
4
5
6
7
8
# 登录 Docker Hub
docker login # -u username -p password

# 登录指定仓库
docker login registry.example.com

# 查看登录状态
cat ~/.docker/config.json

注意,这里用户名的大小写敏感。一般地,建议在设置页面创建 token,通过 token 登录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 搜索镜像
docker search nginx
docker search --limit 5 --filter stars=100 nginx

# 拉取镜像
docker pull nginx
docker pull nginx:1.21-alpine
docker pull ubuntu:20.04

# 推送镜像到 Docker Hub
# 1. 构建镜像
docker build -t username/myapp:v1.0 .

# 2. 推送镜像
docker push username/myapp:v1.0

# 登出
docker logout

上边命令中的 push 镜像,以及 pull 私人镜像需要登录。

实践:发布自己的镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# 创建示例应用
mkdir -p /tmp/docker-tutorial/my-web-app
cd /tmp/docker-tutorial/my-web-app

# 创建简单的 Web 应用
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 my Docker app!',
'hostname': socket.gethostname(),
'version': os.environ.get('APP_VERSION', '1.0.0')
})

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

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
EOF

# 创建 requirements.txt
cat > requirements.txt << 'EOF'
Flask==2.3.3
EOF

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

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

EXPOSE 5000

ENV APP_VERSION=1.0.0

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

# 构建镜像(替换 username 为你的 Docker Hub 用户名)
docker build -t username/my-web-app:1.0.0 .
docker build -t username/my-web-app:latest .

# 测试镜像
docker run -d -p 5000:5000 --name test-app username/my-web-app:1.0.0

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

# 停止测试容器
docker stop test-app
docker rm test-app

# 推送到 Docker Hub(需要先登录)
# docker login
# docker push username/my-web-app:1.0.0
# docker push username/my-web-app:latest

7.3 私有镜像仓库

搭建本地 Registry

关于镜像站,我们之前详细讲过一期,可以参考 『