Docker从入门到精通

一、缘起:这世上的容器

我向来是不惮以最坏的恶意,来推测中国人的软件部署的。然而我还不料,也不信竟会繁复到这等地步。

在我所见过的大大小小的机房里,大抵是这样一副光景:一套程序,从开发的手里交出来,先要到测试的机器上跑——自然是跑不起来的;于是请来运维的同事,装环境,配依赖,改端口,直闹到天明。好容易跑通了,再搬到线上的服务器,却又是一遍同样的折腾。这套程序,在开发那里活得好好的,到了别处,竟像水土不服一般,非要折腾个半死。

这便是所谓”在我的机器上能跑”的千古名言了。

后来听说有一样东西,叫 Docker。它说:何不把程序和它赖以活着的一切——代码、库、环境、配置——统统装进一只箱子,锁好,贴上封条,然后连箱子一起搬走呢?到了哪里,箱子一开,里头的程序依旧活蹦乱跳,全然不知自己已经换了人间。

这主意,听来倒颇有几分道理。于是我便动了研究的心思。

二、容器与虚拟机:两条路

世间原本是有虚拟机的。它老实,却笨重——每造一台,必得连同一整个操作系统都搬进去,活像一个人出远门,非要把自家的房子、地基、连同脚下的泥土都打包带走不可。这样走路,自然是快的不了。启动一台虚拟机,往往要等上几分钟,其间人可以泡一壶茶,再发一阵呆。

容器却另走了一条路。它识得大家脚下的土地原是同一片,便只管把自己的营生带走,地皮是借宿主的。如此一来,身轻如燕,启动不过几秒钟的事。一台上百个容器,也跑得动。

1
2
3
4
5
6
7
8
9
10
虚拟机:连房带地一起搬             容器:只带营生,地皮借宿主
┌─────────┐ ┌───────┐
│ 应用 │ │ 应用 │
│ 依赖 │ │ 依赖 │
│ 客户OS │ └───────┘
│ 虚拟化层 │ 应用 应用 应用
└─────────┘ ┌───容器引擎───┐
│ 宿主OS │
└──────────────┘
笨重,启动慢(分钟级) 轻盈,启动快(秒级)

有人说,容器是轻量级的虚拟机。这话似是而非。虚拟机是另起炉灶,容器是同锅吃饭。一个隔得绝,一个隔得透,其间分寸,用过的人自然明白。

三、几个名词,先要弄清

学一样手艺,最怕的是满纸的行话,弄得人云里雾里。Docker 里头,真正要紧的名词,其实也就那么几个。

镜像(Image) ——这是死的。一张只读的模板,里头封着程序和它全部的家当。镜像自己不会跑,正如照片里的人不会走路。

容器(Container) ——这是活的。把镜像一启动,便成了容器。容器会跑,会停,会死,也会再拉起来。同一段程序,从一个镜像里,可以变出无数个容器来,正如同一张底片,可以洗出许多照片。

仓库(Repository) ——存放镜像的地方。最出名的是 Docker Hub,大抵相当于镜像的当铺,什么货色都有,任人取用。

Dockerfile ——一张配方,用文字写明这镜像该怎么一层层地搭起来。

数据卷(Volume) ——容器是朝生暮死的东西,一删便什么都没了。数据卷却是独立的,容器死了,它还活着。这是给程序留的一条后路。

四、安装:开工之前

工欲善其事,必先利其器。Docker 在 Linux 上跑得最是自在。

Linux(Ubuntu/Debian)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 先把旧的、残的、来路不明的卸干净
sudo apt-get remove docker docker-engine docker.io containerd runc

# 装些必需的家伙
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release

# 加上 Docker 官方的钥匙和仓库
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 这才装上正主
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

国内的网络,拉镜像常常拉不动。这事不必恼,加个加速器便是:

1
2
3
4
5
6
7
8
9
10
11
12
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://dockerproxy.com"
]
}
EOF

sudo systemctl daemon-reload
sudo systemctl restart docker

装好了,试一试:

1
docker run hello-world

若屏幕上跳出一段客客气气的英文,恭喜你,这箱子算是通了气了。

Windows 和 Mac 的朋友,下个 Docker Desktop,装上点开便是。Windows 记得开 WSL2,否则它要闹脾气。

五、常用命令:熟能生巧

Docker 的命令,乍看眼花缭乱,用熟了也不过尔尔。我在这里列一份清单,以备查阅。

镜像

1
2
3
4
5
6
7
8
9
10
docker images                     # 列出本地镜像
docker search nginx # 到 Docker Hub 上寻一寻
docker pull nginx:1.25 # 拉一个下来
docker rmi nginx:1.25 # 不要了,删掉
docker image prune -a # 清扫无用的镜像
docker tag nginx:1.25 mynginx:v1 # 改个名,贴个标签
docker save -o nginx.tar nginx:1.25 # 打包带走
docker load -i nginx.tar # 解包装回
docker history nginx:1.25 # 看这镜像是怎么一层层堆起来的
docker inspect nginx:1.25 # 看它的根底

容器

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
# 新起一个容器
docker run -d --name web \
-p 8080:80 \
-v /data/html:/usr/share/nginx/html \
--restart=always \
nginx:1.25

# 几个要紧的参数:
# -d 后台跑,不占着终端
# --name 给它起个名字,免得 Docker 自己瞎起
# -p 宿主:容器 端口打通
# -v 宿主:容器 目录挂上
# -e KEY=VALUE 传个环境变量进去
# --restart=always 死了就拉起来
# -it 交互式,常用来钻进容器

docker ps # 看哪些还活着
docker ps -a # 连死掉的也看看
docker start web # 启动
docker stop web # 停止
docker restart web # 重启
docker kill web # 直接打死
docker rm web # 删掉(得先停)
docker rm -f $(docker ps -aq) # 一锅端,全删

docker exec -it web bash # 钻进容器里看看(推荐)
docker attach web # 也进得去,但退出时容器会跟着停,不甚好用

docker logs -f --tail 100 web # 看它说些什么,实时跟最后100行

docker cp web:/etc/nginx/nginx.conf ./ # 从容器里往外掏
docker cp ./app.conf web:/etc/nginx/conf.d/ # 往容器里塞

docker stats # 看各家占了多少 CPU、内存
docker top web # 看容器里有几个进程
docker inspect web # 看底细

系统

1
2
3
docker system df            # 看磁盘占了多少
docker system prune -a # 大扫除,无用的全清
docker info # 看整体情况

命令多,不必死记。用得着的时候翻一翻,三五次后自然就记住了。世上原没有记熟的命令,敲得多了,也便成了肌肉记忆。

六、Dockerfile:造一张镜像的方子

Dockerfile 是一张方子,一行一行,交代清楚这镜像该怎么造。每写一行 RUN,便是往上叠一层。层叠得多了,镜像便臃肿,所以能合并的,还是合并了好。

常用指令

指令 说些什么
FROM 从哪个基础镜像起头,必是第一条
LABEL 贴个标签,写明作者之类
RUN 造的时候执行一条命令
CMD 容器起头时默认干的事,可被覆盖
ENTRYPOINT 容器起头时干的事,不大容易被覆盖
EXPOSE 声明要开的端口
ENV 设环境变量
COPY 把宿主的文件抄进镜像
ADD 同 COPY,但能自动解 tar,也容易出乱子
VOLUME 声明一处放数据的地方
WORKDIR 设个工作目录
USER 指定以哪个用户来跑
HEALTHCHECK 定期查一查它还活着没有

CMD 与 ENTRYPOINT 的纠葛

这两个,初学的人最容易搅浑。说白了:

1
2
3
4
5
6
7
8
9
CMD         ——容器起头要干啥。你在 docker run 后头加参数,整个 CMD 就被替掉了。
ENTRYPOINT ——容器起头的入口。你加的参数,是凑到它后头当尾巴使的。

两个搭着用,最是妥当:
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]

→ docker run mynginx → nginx -g "daemon off;"
→ docker run mynginx -t → nginx -t

实例:造一个 SpringBoot 的镜像

我见过许多人,造 Java 镜像时把整套 Maven 和 JDK 都塞进去,一个镜像动辄七八百兆。这其实不必。编译归编译,运行归运行,分作两段,便是所谓”多阶段构建”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 第一段:只管编译
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline # 先把依赖下全,后头好省时间
COPY src ./src
RUN mvn clean package -DskipTests

# 第二段:只管运行,带着编译好的 jar 便走
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
ENV TZ=Asia/Shanghai \
JAVA_OPTS="-Xms256m -Xmx512m"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
1
2
docker build -t myapp:1.0 .
docker run -d --name app -p 8080:8080 myapp:1.0

造镜像的几条规矩

1
2
3
4
5
6
7
8
1. 基础镜像挑小号的:alpine、slim、distroless
2. RUN 能合就合,层数越少越好
3. 不常变的写在前面(比如下依赖),好叫 Docker 复用缓存
4. 配一个 .dockerignore,把不相干的文件挡在外头
5. 密码、密钥万勿写进镜像
6. 用多阶段构建,给镜像减负
7. 别用 root 跑,造个普通用户来用
8. tag 写死版本号,不要图省事用 latest——latest 其实是最不靠谱的

七、数据:给程序留条后路

容器这东西,天性凉薄。你今日删了它,明日再起一个,里头的文件早已荡然无存。所以但凡要紧的数据,总得另找个稳妥的去处。

数据卷(Volume)—— 最是省心

由 Docker 替你管,存在它自家地方,容器死活与它无干。

1
2
docker volume create mydata
docker run -d --name db -v mydata:/var/lib/mysql mysql:8

绑定挂载(Bind Mount)—— 直接些

把宿主机的一个目录,直愣愣地挂进容器。

1
2
3
4
5
docker run -d --name web \
-p 80:80 \
-v /opt/nginx/html:/usr/share/nginx/html:ro \
nginx:1.25
# :ro 是只读,容器里头改不得

tmpfs —— 过眼云烟

存在内存里,容器一停便没了。适合放些不想落盘的临时物事。

1
docker run -d --name cache --tmpfs /cache redis:7

三种法子,各有所长:Volume 稳妥易迁,bind mount 直接却易有路径的牵绊,tmpfs 快却不长久。看场合挑用便是。

八、网络:让它们彼此说得上话

容器一多,便有了彼此往来的问题。Docker 自带几种网络,最常用的是 bridge。

  • bridge:默认的桥,容器间靠虚拟网桥通气。
  • host:容器直接用宿主的网络,不分彼此(也便少了隔离)。
  • none:与世隔绝。
  • overlay:跨主机连络,Swarm 集群里用。
  • macvlan:给容器一个正经的物理 MAC 地址。
1
2
3
4
5
docker network ls                      # 看有哪些网络
docker network create mynet # 自己建一个
docker network inspect mynet # 看根底
docker network connect mynet web # 把活着的容器拉进来
docker network rm mynet # 删

这里头有一桩要紧的事:默认的 bridge,容器之间只能靠 IP 寻人;而自己建的 bridge 网络,却可以用容器名相唤,仿佛有了名姓一般。所以多容器往来,总该自建一个网络:

1
2
3
4
5
6
7
docker network create app-net

docker run -d --name db --network app-net -e MYSQL_ROOT_PASSWORD=123456 mysql:8
docker run -d --name web --network app-net -p 8080:80 nginx:1.25

# web 里头,可以直接唤 db 的名字
docker exec web ping db

九、Docker Compose:一干人等的总调度

一个应用,常常不是孤零零一个容器,而是三五成群——数据库一个,缓存一个,应用一个。一个个 docker run 敲下去,既啰嗦又易错。于是有了 Compose:把这一干人等,都写进一个 YAML 文件里,一令既出,齐齐起立。

新版 Docker 已自带 compose 插件,直接 docker compose 便是。

常用命令

1
2
3
4
5
6
7
8
9
docker compose up -d              # 后台一齐启动
docker compose up -d --build # 重新造镜像再起
docker compose down # 一齐散了(连容器带网络)
docker compose down -v # 连数据卷也一并端掉
docker compose ps # 看各家状态
docker compose logs -f web # 听某家说话
docker compose restart web # 叫某家重启
docker compose exec web bash # 钻进某家
docker compose stop # 都停下(不删)

一例:搭一套 WordPress

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
version: "3.8"

services:
db:
image: mysql:8.0
container_name: wp-db
restart: always
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: wordpress
MYSQL_USER: wp
MYSQL_PASSWORD: wppass
volumes:
- db_data:/var/lib/mysql
networks:
- wp-net

wordpress:
image: wordpress:6.4-php8.2-apache
container_name: wp-web
restart: always
depends_on:
- db
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wp
WORDPRESS_DB_PASSWORD: wppass
WORDPRESS_DB_NAME: wordpress
volumes:
- wp_data:/var/www/html
networks:
- wp-net

volumes:
db_data:
wp_data:

networks:
wp-net:
driver: bridge
1
docker compose up -d

这一纸文书,便把数据库与网页两家的关系、端口、密码、卷、网络,都交代得明明白白。日后换台机器,带着这文件同去,顷刻便能复起一套。

十、私有仓库:自家的当铺

公家的 Docker Hub 固然方便,有时却不甚合用——慢,或有不能外传的镜像。这时便该开一家自家的当铺:

1
2
3
4
docker run -d -p 5000:5000 --name registry \
-v /opt/registry:/var/lib/registry \
--restart=always \
registry:2

往里头存东西,先得改个名,前缀写上仓库的地址:

1
2
3
docker tag nginx:1.25 127.0.0.1:5000/nginx:1.25
docker push 127.0.0.1:5000/nginx:1.25
docker pull 127.0.0.1:5000/nginx:1.25

若是非 HTTPS 的地址,Docker 不肯信,得在 /etc/docker/daemon.json 里说一声:

1
2
3
{
"insecure-registries": ["192.168.1.100:5000"]
}

然后重启 Docker,它便认了。

十一、安全与运维:几分警惕

容器虽轻便,却也不是全无隐患。我以为有几条,是断断不可大意的:

1
2
3
4
5
6
7
8
9
10
11
12
1. 莫用 root 跑容器,造个普通用户来用
2. 限定它吃的 CPU 与内存,免得一个容器发了疯,连累整台机器
3. 削去它不该有的权限:
--read-only 文件系统只读
--cap-drop=ALL 先把权全夺了
--cap-add=NET_BIND_SERVICE 再按需添一点
--security-opt=no-new-privileges 禁止提权
--pids-limit 100 限制进程数
4. 密钥万勿写死在镜像里
5. 常拿 trivy、docker scout 之类扫一扫漏洞
6. 基础镜像挑最小的,有漏洞也好补
7. 基础镜像记得常更新

限制资源一例:

1
2
3
4
5
6
7
8
9
docker run -d --name web \
--cpus="1.5" \
--memory="512m" \
--memory-swap="1g" \
--pids-limit 200 \
--restart=on-failure:3 \
--read-only \
--tmpfs /tmp \
nginx:1.25

日志若不管,日积月累,能撑爆磁盘。在 /etc/docker/daemon.json 里限一限:

1
2
3
4
5
6
7
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}

常见的疑难杂症

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 容器一起就退
docker logs <容器> # 看它临终说了什么
# 多半是 CMD 的命令一执行完便退了,前头没有挡住的进程

# 端口起不来
netstat -tlnp | grep :80 # 查是谁占了

# 容器连不上网
docker network inspect bridge
sysctl net.ipv4.ip_forward # 该是 1

# 磁盘满了
docker system df # 看谁占的
docker system prune -a # 大扫除
docker builder prune -a # 清构建缓存

# 想改已死容器的文件
docker cp <容器>:/path /host/path # 掏出来改了再塞回去

# 删不掉的容器或镜像
docker rm -f <容器>
# 还不动,便重启 Docker
systemctl restart docker

十二、Swarm:一群的事

若一台机器不够使,便要集群。Docker 自家有个 Swarm,虽不及 Kubernetes 那般显赫,却也够许多场合用了。

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
# 立一个头领
docker swarm init --advertise-addr 192.168.1.10

# 领入伙的口令
docker swarm join-token worker

# 在别的机器上照着念
docker swarm join --token <token> 192.168.1.10:2377

# 看有几家
docker node ls

# 派一个活,要三份
docker service create --replicas 3 --name web -p 80:80 nginx:1.25

# 看
docker service ls
docker service ps web

# 加到五份
docker service scale web=5

# 换个新版本,滚动着换
docker service update --image nginx:1.26 web

# 散了
docker service rm web

十三、速查表

列一张表,放在手边,用时翻一翻:

要做什么 念这句
拉镜像 docker pull nginx
看镜像 docker images
跑容器 docker run -d --name web -p 80:80 nginx
看容器 docker ps / docker ps -a
进容器 docker exec -it web bash
看日志 docker logs -f web
docker stop web
删容器 docker rm web
删镜像 docker rmi nginx
造镜像 docker build -t myapp:1.0 .
看占用 docker stats
大扫除 docker system prune -a
Compose 起 docker compose up -d
Compose 停 docker compose down

结尾的话

Docker 的根本道理,无非是”造一次,到处跑”这六个字。要紧的几样,翻来覆去也就是:镜像、容器、仓库、数据卷、网络;再会写 Dockerfile,会用 Compose,知道怎么加固、怎么排错,便已足以应付日常的大半。

世上本没有精通,用得多了,踩的坑多了,也就通了。

至于再往前的 Kubernetes,那是另一片天地。然而有了 Docker 作底子,走进去时,脚下总归是稳当些的。

参考的所在: