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
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
|
装好了,试一试:
若屏幕上跳出一段客客气气的英文,恭喜你,这箱子算是通了气了。
Windows 和 Mac 的朋友,下个 Docker Desktop,装上点开便是。Windows 记得开 WSL2,否则它要闹脾气。
五、常用命令:熟能生巧
Docker 的命令,乍看眼花缭乱,用熟了也不过尔尔。我在这里列一份清单,以备查阅。
镜像
1 2 3 4 5 6 7 8 9 10
| docker images docker search nginx 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
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
docker cp web:/etc/nginx/nginx.conf ./ docker cp ./app.conf web:/etc/nginx/conf.d/
docker stats 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
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
|
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
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
|
这一纸文书,便把数据库与网页两家的关系、端口、密码、卷、网络,都交代得明明白白。日后换台机器,带着这文件同去,顷刻便能复起一套。
十、私有仓库:自家的当铺
公家的 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 <容器>
netstat -tlnp | grep :80
docker network inspect bridge sysctl net.ipv4.ip_forward
docker system df docker system prune -a docker builder prune -a
docker cp <容器>:/path /host/path
docker rm -f <容器>
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 作底子,走进去时,脚下总归是稳当些的。
参考的所在: