第 12 章 · Docker
一句话:把程序连同它要的系统库、运行时和配置打成一个镜像,在任何装了 Docker 的机器上用同一条命令跑起来。 用它还是用别的:本机跑数据库、缓存这类服务,以及把自己的应用打包交给服务器部署,都用它;一次起好几个互相连接的服务用 Docker Compose(第 13 章);只是隔离 Python 或 Node 的依赖,用虚拟环境或项目本地的
node_modules就够,不必上容器。 核对版本:Docker Desktop 4.94(Engine 29.8),2026-10
12.1 安装与版本
Section titled “12.1 安装与版本”Windows:
winget install Docker.DockerDesktopmacOS:
brew install --cask docker-desktopLinux(Debian/Ubuntu):先按 12.7「Linux 安装(Ubuntu)」添加 Docker 官方 apt 源,再安装 Docker Engine。不要装发行版自带的 docker.io,版本旧,和官方源的包会冲突:
sudo apt updatesudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginDocker Desktop 是带图形界面的整套环境,里面有 Docker Engine、命令行和 Compose。Windows 版跑在 WSL 2 上(第 4 章),装之前先在管理员 PowerShell 里运行 wsl --install 或 wsl --update,并确认 BIOS 里开了虚拟化。装完要先打开 Docker Desktop,等它显示 Engine running,docker 命令才连得上。Docker Desktop 对个人、教育和小公司免费,员工 250 人以上或年收入 1000 万美元以上的公司商用要付费订阅。
Linux 装完默认只有 root 能用 docker,要让当前用户直接用,把它加进 docker 组,然后注销重新登录:
sudo usermod -aG docker $USER查版本。docker version 分 Client 和 Server 两段,只有 Client 没有 Server 说明守护进程没在跑;docker info 列出容器数、镜像数、数据目录和镜像加速地址:
docker versiondocker info最后跑一次官方的测试镜像,看到 Hello from Docker! 就装好了:
docker run hello-world12.2 五分钟概念
Section titled “12.2 五分钟概念”- 镜像(Image)与容器(Container):镜像是只读的模板,里面是一个精简的 Linux 文件系统加你的程序;容器是镜像跑起来的一个实例,有自己的进程、文件和网络。一个镜像可以同时起很多个容器。容器里改的文件只存在这个容器里,
docker rm之后就没了。 - 镜像仓库(Registry)与标签(Tag):存放镜像的服务器,默认是 Docker Hub,
docker pull nginx就是从那里下载。镜像名的冒号后面是标签,表示版本,如postgres:17;不写标签等于:latest,它只是一个普通名字,不保证是最新版,正式用时写明版本号。 - 卷(Volume)与绑定挂载(Bind mount):让数据活得比容器长的两种办法。卷由 Docker 管理,用名字引用,如
-v pgdata:/var/lib/postgresql/data,删容器不删卷,数据库数据放这里;绑定挂载把宿主机的一个目录直接映射进容器,如-v /home/me/src:/app/src,两边改动实时同步,开发时挂源码用。 - 网络(Network)与端口映射:每个容器有自己的 IP,外面默认连不进去。
-p 8080:80把宿主机的 8080 端口转到容器的 80 端口,左边是宿主机,右边是容器。几个容器接到同一个自建网络上,就能用容器名互相访问,如应用里连db:5432。 - Dockerfile 与层(Layer)缓存:Dockerfile 是构建镜像的配方,每条指令生成一层。重新构建时,从第一条有变化的指令开始往后全部重做,之前的层直接用缓存。所以把很少变的步骤(装依赖)放前面、常变的(拷源码)放后面,改一行代码只重做最后几层,几秒就构建完。
下图是这几样东西的关系:Dockerfile 经 build 变成镜像,镜像也可以从仓库 pull 下来或 push 上去;镜像 run 起来成为容器;容器通过 -p 接到宿主机端口,通过 -v 挂上卷,同一网络里的容器按名字互访。
12.3 常用命令
Section titled “12.3 常用命令”命令里的 <容器> 写容器名或容器 ID 的前几位,<镜像> 写 名字:标签。长写法 docker image ls、docker container ls 和表里的短写法完全等价。
| 任务 | 命令 | 说明 |
|---|---|---|
| 镜像 | ||
| 下载镜像 | docker pull nginx:stable-alpine |
不写标签就是 :latest |
| 列出本地镜像 | docker images |
带大小和创建时间;<none> 的是悬空镜像,见 12.6 |
| 删镜像 | docker rmi <镜像> |
还有容器在用它时会拒绝,先删容器 |
| 构建镜像 | docker build -t myapp:1.0 . |
. 是构建上下文,即 Dockerfile 所在目录 |
| 不用缓存重新构建 | docker build --no-cache -t myapp:1.0 . |
怀疑缓存里是旧依赖时用,会慢很多 |
| 给镜像加一个名字 | docker tag myapp:1.0 <用户名>/myapp:1.0 |
推送前要改成带仓库前缀的名字 |
| 推送镜像 | docker push <用户名>/myapp:1.0 |
先 docker login |
| 看镜像每层多大 | docker history <镜像> |
找出是哪条指令把镜像撑大的 |
| 运行容器 | ||
| 后台运行并命名 | docker run -d --name web nginx:stable-alpine |
不起名会随机分配一个名字 |
| 映射端口 | docker run -d -p 127.0.0.1:8088:80 nginx:stable-alpine |
宿主机 8088 转容器 80;去掉 127.0.0.1: 局域网也能连 |
| 挂载卷 | docker run -d -e POSTGRES_PASSWORD=dev -v pgdata:/var/lib/postgresql/data postgres:17 |
卷不存在会自动创建;PostgreSQL 镜像不给口令会直接退出 |
| 挂载当前目录 | docker run -v "$(pwd)":/app <镜像> |
PowerShell 写 ${PWD}:/app |
| 传环境变量 | docker run -e POSTGRES_PASSWORD=dev postgres:17 |
多个就写多个 -e;从文件读用 --env-file .env |
| 用完即删 | docker run --rm -it python:3.12 python |
退出后容器自动删除,试东西用 |
| 交互进入新容器 | docker run -it --rm ubuntu bash |
-it 接上键盘和终端,exit 退出 |
| 管理容器 | ||
| 列出运行中的容器 | docker ps |
看名字、端口映射和状态 |
| 列出全部容器 | docker ps -a |
包括已退出的,状态列写着退出码 |
| 看日志 | docker logs -f <容器> |
-f 持续跟踪,Ctrl+C 退出;--tail 100 只看最后 100 行 |
| 进入运行中的容器 | docker exec -it <容器> sh |
有 bash 的镜像可以把 sh 换成 bash |
| 停止 | docker stop <容器> |
先发停止信号,10 秒后还没停就强杀 |
| 启动已停止的容器 | docker start <容器> |
用原来的参数再跑起来,数据还在 |
| 删除容器 | docker rm <容器> |
运行中的要先停;-f 强制删 |
| 看全部配置 | docker inspect <容器> |
JSON 输出,IP、挂载、环境变量都在里面 |
| 看资源占用 | docker stats |
每个容器的 CPU、内存、网络,实时刷新;--no-stream 只看一次 |
| 拷出文件 | docker cp <容器>:/etc/nginx/nginx.conf . |
反过来写就是拷进去 |
| 卷与网络 | ||
| 列出卷 | docker volume ls |
随书 compose 的卷名带 devtools_ 前缀 |
| 删卷 | docker volume rm <卷> |
卷里的数据随之删除 |
| 清理匿名卷 | docker volume prune |
默认只删没被用的匿名卷;加 -a 连命名卷一起删,数据库数据全没 |
| 列出网络 | docker network ls |
自带 bridge、host、none 三个 |
| 新建网络 | docker network create mynet |
容器 run --network mynet 接进来,按名字互访 |
| 清理 | ||
| 看磁盘占用 | docker system df |
镜像、容器、卷、构建缓存各占多少,可回收多少 |
| 清理无用对象 | docker system prune |
删已停止的容器、没用的网络、悬空镜像和构建缓存 |
| 彻底清理 | docker system prune -a --volumes |
慎用:没有容器在用的镜像全删,下次要重新下载;匿名卷也删 |
| 登录 | ||
| 登录 Docker Hub | docker login |
按提示在浏览器里输验证码;其他仓库写 docker login ghcr.io |
| 退出登录 | docker logout |
删掉本机保存的凭据 |
12.4 典型工作流
Section titled “12.4 典型工作流”用别人的镜像跑一个服务,进去看一眼
Section titled “用别人的镜像跑一个服务,进去看一眼”以 Nginx 为例,端口用 8088,避开随书 compose 的 8080:
- 下载镜像:
docker pull nginx:stable-alpine。 - 后台启动,只让本机访问:
docker run -d --name web -p 127.0.0.1:8088:80 nginx:stable-alpine。 - 确认在跑,
PORTS一列应是127.0.0.1:8088->80/tcp:docker ps。 - 浏览器打开
http://localhost:8088,或在终端里:curl http://localhost:8088。 - 看访问日志,刚才的请求应该在里面,
Ctrl+C退出:docker logs -f web。 - 进容器的 shell,可以
cat /etc/nginx/conf.d/default.conf看配置,输入exit回到宿主机:docker exec -it web sh。 - 停止:
docker stop web。 - 删除容器,镜像还在,下次
run不用再下载:docker rm web。
给自己的 Node 应用写 Dockerfile 并运行
Section titled “给自己的 Node 应用写 Dockerfile 并运行”项目根目录有 package.json、package-lock.json,npm run build 把代码构建到 dist/,入口是 dist/server.js,监听 3000 端口。Python 项目思路相同,换成 uv sync 和 uv.lock:
- 在项目根目录写 Dockerfile,内容照抄 12.5 的最小 Dockerfile,入口文件名按实际改。
- 写
.dockerignore,每行一个,把node_modules、dist、.git、.env排除在构建上下文之外。不写的话本机的node_modules和密钥都会被发给构建过程,COPY . .还会把它们拷进镜像。 - 构建:
docker build -t myapp:1.0 .。 - 运行:
docker run -d --name myapp -p 127.0.0.1:3000:3000 myapp:1.0。 - 访问:
curl http://localhost:3000。 - 出错时看输出:
docker logs myapp。 - 改了代码,重新构建,这次只有拷源码之后的几层会重做:
docker build -t myapp:1.1 .。 - 删掉旧容器,回到第 4 步用
myapp:1.1重新运行:docker rm -f myapp。
把镜像推到 Docker Hub
Section titled “把镜像推到 Docker Hub”镜像名的斜杠前面是仓库和用户名,Docker Hub 可以省略仓库地址,只写用户名:
- 在 hub.docker.com 注册账号,然后在本机登录:
docker login。 - 起一个带用户名的名字:
docker tag myapp:1.0 <用户名>/myapp:1.0。 - 推送,只上传仓库里还没有的层:
docker push <用户名>/myapp:1.0。 - 同一个镜像再标一个
latest:docker tag myapp:1.0 <用户名>/myapp:latest。 - 推送:
docker push <用户名>/myapp:latest。 - 在服务器上拉下来运行:
docker run -d -p 3000:3000 <用户名>/myapp:1.0。
推到 GitHub 的镜像仓库,把 <用户名>/ 换成 ghcr.io/<用户名>/,第 1 步改成 docker login ghcr.io,用户名是 GitHub 用户名,密码填带 write:packages 权限的个人访问令牌。新推的镜像在 Docker Hub 默认公开,有密钥或内部代码的先在网页上设为私有。
12.5 配置要点
Section titled “12.5 配置要点”配置文件的位置:
| 范围 | 路径 |
|---|---|
| 守护进程配置(Linux) | /etc/docker/daemon.json,默认不存在,自己新建 |
| 守护进程配置(Docker Desktop) | 设置 → Docker Engine 里直接编辑,文件存在 ~/.docker/daemon.json |
| 命令行配置(登录信息、凭据助手) | ~/.docker/config.json,Windows 为 %USERPROFILE%\.docker\config.json |
| 数据目录(Linux) | /var/lib/docker |
| 数据目录(Docker Desktop) | 设置 → Resources → Advanced → Disk image location |
| WSL 2 虚拟机的内存上限(Windows) | %USERPROFILE%\.wslconfig |
Linux 改完 daemon.json 要 sudo systemctl restart docker 才生效,正在跑的容器会随之停止;Docker Desktop 在设置页点 Apply & restart。
值得改的项:
| 配置项 | 建议值 | 效果 |
|---|---|---|
registry-mirrors |
云厂商分配给你的加速地址 | 从 Docker Hub 拉镜像时先走这个地址,只对 Docker Hub 生效 |
log-driver |
"json-file" |
容器日志写成宿主机上的文件,也是默认值 |
log-opts.max-size |
"10m" |
单个日志文件到 10 MB 就切换;默认不限,长跑的容器能写满磁盘 |
log-opts.max-file |
"3" |
每个容器最多留 3 个日志文件;数字也要写成字符串 |
data-root |
大容量盘上的目录,如 "/data/docker" |
镜像和容器挪出系统盘;改之前先停 Docker 把原目录拷过去 |
proxies |
http-proxy、https-proxy 两项 |
守护进程拉镜像时走代理;Docker Desktop 忽略此项,在设置的 Resources → Proxies 里配 |
.wslconfig 的 memory |
默认已是物理内存的一半;嫌占得多就调小,如 4GB |
WSL 2 虚拟机连同 Docker 一共能用的内存上限,写在 [wsl2] 下面,wsl --shutdown 后生效 |
关于镜像加速:国内直连 Docker Hub 经常超时,早几年流行的公开加速地址大多已停止服务。可靠的做法是用云厂商容器镜像服务给你账号分配的专属加速地址,或者给 Docker 配代理。日志选项只对改完之后新建的容器生效,老容器要删掉重建。
daemon.json 的最小配置。JSON 不允许写注释,加了注释 Docker 会启动失败,各项含义见上表:
{ "registry-mirrors": ["https://<加速地址>"], "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }}最小 Dockerfile,Node 应用,两阶段构建(multi-stage build)。Dockerfile 的注释必须单独一行,写在指令行尾会被当成参数,所以每条注释写在它说明的一两行上面:
# 第一阶段起名 build:装全部依赖并构建FROM node:22-slim AS build# 之后的指令都在容器里的 /app 下执行WORKDIR /app# 先只拷依赖清单,代码改了这一层的缓存也不失效COPY package.json package-lock.json ./# 按锁文件装依赖,含构建要用的开发依赖RUN npm ci# 再拷源码,构建出 dist/,然后删掉开发依赖COPY . .RUN npm run build && npm prune --omit=dev# 第二阶段是最终镜像,同样以 /app 为工作目录FROM node:22-slimWORKDIR /app# 只从 build 阶段拿运行要用的文件,源码不进来COPY --from=build /app/package.json ./COPY --from=build /app/node_modules ./node_modulesCOPY --from=build /app/dist ./dist# 容器的主进程,它退出容器就停CMD ["node", "dist/server.js"]最终镜像只有第二阶段的内容:第一阶段的源码、开发依赖和构建缓存都留在 build 阶段,不会进最终镜像,镜像通常小一半以上。
12.6 注意事项
Section titled “12.6 注意事项”Bind for 0.0.0.0:8080 failed: port is already allocated。 原因:宿主机这个端口已被本机的服务或另一个容器占用。处理:docker ps看是不是自己的容器占着;或改左边的宿主机端口,如-p 8081:80;compose 的端口冲突见前言。docker ps里没有刚run -d的容器,docker ps -a显示Exited (1)。 原因:主进程一结束容器就停,程序启动报错或命令执行完就退出了。处理:docker logs <容器>看最后几行输出;要进去排查用docker run -it --rm <镜像> sh。- 改了代码,容器里跑的还是旧版。 原因:代码是构建时拷进镜像的,没重新 build,或 build 了但还在用旧容器。处理:
docker build后docker rm -f <容器>再docker run;开发阶段用-v把源码目录挂进容器。 - 磁盘越来越满,
docker system df显示镜像和构建缓存占了几十 GB。 原因:每次 build 留下旧层和悬空镜像(名字是<none>),停掉的容器也不会自动删。处理:docker system prune,不够再加-a;临时容器用--rm运行。 - Windows 上 Docker Desktop 起不来,报
Docker Desktop - Unexpected WSL error或没检测到虚拟化。 原因:WSL 2 没装或太旧,或 BIOS 没开虚拟化。处理:管理员 PowerShell 运行wsl --update(第 4 章);任务管理器「性能 → CPU」看虚拟化是否已启用。 - Linux 上报
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?原因:docker 服务没起。处理:sudo systemctl start docker;Windows、macOS 打开 Docker Desktop。 - Linux 上不加
sudo运行docker ps,报permission denied,后面跟着unix:///var/run/docker.sock。 原因:当前用户不在docker组。处理:sudo usermod -aG docker $USER,注销重新登录。注意docker组的成员等于拥有 root 权限。 docker pull卡很久后报Client.Timeout exceeded while awaiting headers。 原因:国内直连 Docker Hub 不稳定。处理:daemon.json配registry-mirrors(12.5);或配代理,Docker Desktop 在设置的 Resources → Proxies。- 容器里中文显示成问号,日志时间比北京时间慢 8 小时。 原因:精简镜像没设语言环境,时区默认是 UTC。处理:运行时加
-e TZ=Asia/Shanghai -e LANG=C.UTF-8,或写进 Dockerfile 的ENV;有的镜像还要先装tzdata时区才生效。
12.7 深入阅读
Section titled “12.7 深入阅读”- Docker 官方文档:从入门指南到 Engine、Desktop、Build 的完整文档,先看 Get started。
- Dockerfile 参考:每条指令的写法和细节,FROM、COPY、RUN、CMD 的区别查这里。
- Docker CLI 参考:每条 docker 子命令的全部参数和示例。
- Docker Hub:找官方镜像,看每个镜像支持哪些标签和环境变量。
- Linux 安装(Ubuntu):添加 Docker 官方 apt 源再安装 Docker Engine 的完整步骤,装完接着看 Linux 安装后步骤。