“我这儿能跑啊”——部署最老的敌人就是环境差异:你机器上有的依赖、库版本、系统配置,服务器上不一定有。Docker 的解法是把程序连同它的整个运行环境(系统库、依赖、配置)打包成一个可移植的”盒子”,在哪台机器上跑,环境都一模一样。

Docker 是什么:三个核心概念

Docker 是容器引擎——用比虚拟机轻得多的方式隔离和运行程序。上手前必须分清三个概念,绝大多数新手困惑都源于把前两个混为一谈:

  • 镜像(image):只读的模板,包含程序和它的完整运行环境——类比面向对象里的”类”
  • 容器(container):镜像跑起来的实例——类比”对象”,同一个镜像可以同时跑出多个互不干扰的容器
  • 仓库(registry):集中存放镜像的服务,默认是 Docker Hub,docker pull 从这里拉镜像
flowchart LR
    D[Dockerfile] -->|docker build| I[镜像 image]
    I -->|docker run| C1[容器 1]
    I -->|docker run| C2[容器 2]
    R[(仓库 registry)] -->|docker pull| I
    I -->|docker push| R

安装:一条命令

1
2
3
4
5
6
curl -fsSL https://get.docker.com | sh
# Linux 上的官方便捷安装脚本——-sSL 这套管道安装的参数含义在 curl 那篇讲过
# -f:HTTP 出错时直接失败,不把错误页当脚本喂给 sh

docker run hello-world
# 装完跑这个验证:能输出欢迎信息说明引擎正常

macOS/Windows 用 Docker Desktop 图形化安装(macOS 上轻量替代 OrbStack 也很流行),装完命令行用法完全一致。

docker run:参数拆解

跑一个 nginx,把最常用的参数一次讲清:

1
2
3
4
5
6
7
docker run -d -p 8080:80 --name web nginx
# nginx:镜像名,本地没有会自动去仓库拉取
# -d:detached,后台运行,不占着当前终端
# -p 8080:80:端口映射,方向是 宿主机:容器——
# 把宿主机的 8080 映射到容器里的 80,浏览器访问 localhost:8080 就到了容器里的 nginx
# 方向记反是最常见的新手错误:冒号左边永远是你机器上的端口
# --name web:给容器起名,后续操作用名字代替随机生成的 ID

容器生命周期:从跑起来到删干净

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
docker ps
# 列出正在运行的容器

docker ps -a
# -a:包含已退出的容器——容器停了不等于没了,还占着名字和磁盘

docker logs -f web
# 看容器的输出日志,-f 实时跟踪(和 tail -f 一个手感)

docker exec -it web sh
# 进入运行中的容器内部开个 shell,排查问题用
# -i 保持输入、-t 分配终端,基本固定连用;exit 退出不影响容器运行

docker stop web
# 停止容器(先发 SIGTERM 优雅退出,超时再强杀——和 systemctl stop 同款逻辑)

docker rm web
# 删除已停止的容器;运行中的要先 stop 或用 rm -f

docker run --rm -it golang:1.26 bash
# --rm:容器退出后自动删除自己——临时起个环境试点东西,用完不留垃圾

一个必须建立的认知:容器不是轻量虚拟机,它的生命周期跟着主进程走——docker run 启动时指定(或镜像默认)的那个进程退出了,容器就停了。跑个 docker run ubuntu 发现容器”秒退”,不是坏了,是它的默认命令执行完就结束了。

数据坑:容器删了,数据跟着没了

容器内部写的文件属于容器自己的可写层,docker rm 之后整层蒸发。数据库这类有状态的服务,必须把数据目录挂载到宿主机:

1
2
3
4
5
6
7
docker run -d --name pg \
-e POSTGRES_PASSWORD=devpass \
-v /data/pgdata:/var/lib/postgresql/data \
postgres:17
# -e:设置容器内的环境变量,镜像文档会列出支持哪些
# -v 宿主路径:容器路径:把宿主机目录挂进容器——
# 数据实际写在宿主机 /data/pgdata,容器删了重建,数据还在

判断标准很简单:这个容器产生的数据丢了心不心疼?心疼就必须 -v 挂出来。

镜像管理与 tag

1
2
3
4
5
6
7
8
docker images
# 列出本地已有的镜像和它们的体积

docker pull nginx:1.27
# 拉取指定版本,冒号后面是 tag(版本标签)

docker rmi nginx:1.27
# 删除本地镜像,释放磁盘

tag 有个约定俗成的坑:不写 tag 默认拉 latest,但 latest 只是个普通标签,指向的版本会随时间漂移——今天构建和明天构建拉到的可能不是同一个东西。生产环境永远写死具体版本号,postgres:17 而不是 postgres:latest。

Dockerfile:把自己的 Go 服务打成镜像

Dockerfile 是构建镜像的”配方”。Go 程序编译成单个静态二进制,特别适合多阶段构建——编译在带完整工具链的大镜像里做,运行只带二进制本身:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 阶段一:编译。golang 官方镜像自带完整编译工具链,体积几百 MB(Debian 底座的接近 1 GB),但只在构建时用
FROM golang:1.26-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
# 先只拷贝依赖清单并下载依赖——这两层能吃缓存,
# 代码改动不会导致依赖重新下载,构建快很多
COPY . .
RUN CGO_ENABLED=0 go build -o server .
# CGO_ENABLED=0:纯静态编译,产物不依赖任何系统库

# 阶段二:运行。只把二进制拷进一个几 MB 的空底座
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/server .
# --from=builder:从上一阶段拷贝产物,工具链、源码统统不带
EXPOSE 8080
CMD ["./server"]
# CMD:容器启动时执行的命令——就是前面说的"主进程"
1
2
3
4
docker build -t myapp:1.0 .
# -t:给构建出的镜像打名字和 tag;最后的 . 是构建上下文(Dockerfile 所在目录)

docker run -d -p 8080:8080 myapp:1.0

多阶段构建的效果立竿见影:单阶段直接用 golang 镜像跑,成品接近 1 GB;两阶段下来通常只有十几 MB。

为什么差距这么大?关键在于镜像是一层层叠出来的,最终镜像 = 基础镜像的所有层 + 你新加的层,叠上去的东西删不掉。单阶段构建时基础镜像是 golang——里面装着 Go 编译器、标准库、git、gcc 这一整套构建工具(Debian 底座解压后就有 800 MB 上下),再叠上 go mod download 拉下来的依赖缓存和你的源码,成品轻松逼近 1 GB。可服务跑起来真正需要的只有那个十几 MB 的静态二进制,剩下全是”编译完就没用了”的东西。多阶段构建的本质就是把这些甩掉:FROM alpine 另起一个只有几 MB 的干净底座,只把二进制 COPY 进去——builder 阶段的所有层都不会进入最终镜像。

docker compose:多容器一把梭

服务一多(web + 数据库 + 缓存),逐个 docker run 又长又难维护。compose 把整套服务声明在一个 compose.yaml 里:

1
2
3
4
5
6
7
8
9
10
11
12
13
services:
web:
build: .
ports:
- "8080:8080"
environment:
- REDIS_ADDR=redis:6379
depends_on:
- redis
redis:
image: redis:7
volumes:
- ./redis-data:/data
1
2
3
4
5
6
7
8
9
10
docker compose up -d
# 一条命令把整套服务按依赖顺序拉起来
# 注意是 docker compose(空格)——v2 的写法,作为插件内置在现代 Docker 里
# 老文章里的 docker-compose(连字符)是 v1 独立程序,2023 年就停止更新了

docker compose logs -f
# 聚合看所有服务的日志

docker compose down
# 整套停掉并删除容器(挂载出来的数据不受影响)

compose 文件里服务名(如 redis)自动成为容器间互相访问的主机名——web 里连 redis:6379 就通,不用关心容器 IP。

排查与查看:network、inspect 与 docker info

容器出问题时光看 logs 不够,还得能看它的网络接在哪、完整配置是什么、引擎本身的状态如何。这几个都是只读的查看类命令,放心随便敲:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
docker network ls
# 列出 Docker 的网络。默认有三个:bridge(容器不指定网络就接它)、host、none

docker network inspect bridge
# 看某个网络的详情:网段、网关、接在这个网络上的容器和它们各自的 IP

docker inspect web
# 输出容器的完整配置 JSON:环境变量、挂载、端口映射、IP、重启策略全在里面
# 镜像同样能 inspect:docker inspect nginx:1.27

docker inspect -f '{{.NetworkSettings.IPAddress}}' web
# -f:format,用 Go 模板语法只取想要的字段——这个例子直接输出容器的 IP

docker port web
# 只看这个容器的端口映射,比在 inspect 的大 JSON 里翻快得多

docker info
# Docker 引擎自身的配置和状态:存储驱动、数据根目录、容器/镜像数量、cgroup 版本

docker stats
# 实时看各容器的 CPU、内存、网络占用——容器版的 top

docker system df
# 镜像、容器、数据卷各占了多少磁盘,清理前先看这个

磁盘满了怎么清理:docker system df 找大头,prune 分级清

Docker 用久了最常见的问题是磁盘被悄悄占满,而且报错往往不直接说“磁盘满了”。

一个不像磁盘问题的报错

在 Mac 上用 Docker Desktop 跑 Doris(一个分析型数据库),建表时报错:

1
2
3
ERROR 1105 (HY000): errCode = 2, detailMessage = Failed to find enough backend,
please check the replication num,replication tag and storage medium and avail capacity of backends ...
backends with tag {"location" : "default"} is [[backendId=..., host=127.0.0.1, hdd disks count={capExceedLimit=1,}, ...]]

字面意思像是副本数配置不对,真正的线索是 capExceedLimit=1:磁盘容量超限。可 Doris 自己的数据只有 19MB,进容器看磁盘:

1
2
docker exec doris df -h
# 在名为 doris 的容器里执行 df -h,看容器眼里的磁盘
1
2
Filesystem      Size  Used Avail Use% Mounted on
overlay 59G 55G 491M 100% /

满的不是 Doris,是整个 Docker。Mac 上的容器跑在 Docker Desktop 的 Linux 虚拟机里,所有镜像、容器、数据卷、构建缓存共用这台虚拟机的一块虚拟磁盘,任何一个容器看到的都是这块盘的总使用率。

Doris 有两个磁盘保护阈值:使用率 storage_flood_stage_usage_percent 和剩余空间 storage_flood_stage_left_capacity_bytes,在 4.1.3 版本上查到的值分别是 95% 和 1GB。这次使用率 99%、剩余不到 500MB,两个都超了,所以 Doris 拒绝建表。

先看谁占的:docker system df

1
2
3
4
5
docker system df
# 按类型汇总磁盘占用

docker system df -v
# -v:verbose,逐个列出镜像、容器、数据卷、构建缓存的大小

当时的输出:

1
2
3
4
5
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images 122 30 28.01GB 10.58GB (37%)
Containers 47 22 474MB 378.1MB (79%)
Local Volumes 24 11 7.893GB 435.3MB (5%)
Build Cache 144 14 10.68GB 9.902GB

几列的含义:

  1. TOTAL:总数。
  2. ACTIVE:正在被容器使用的数量。
  3. SIZE:占用的磁盘。
  4. RECLAIMABLE:清理后能腾出来的空间,也就是没被使用的部分。

这次的大头一目了然:构建缓存能回收 9.9GB,镜像能回收 10.6GB。数据卷几乎都在用,而且里面是数据库数据,不在考虑范围内。

按风险从低到高清理

以下命令和参数基于 Docker 29.8(Docker Desktop for Mac)核对过 --help 和官方文档。每个 prune 命令执行前都会列出要删什么并要求确认,不加 -f 时输入 y 才会真正删除。下面按“删错了的代价”从低到高排:

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 builder prune -f
# 删除悬空的构建缓存(docker build 每一步留下、已经没有任何构建引用的中间层)
# -f:force,跳过“确定要删除吗”的确认,直接删
# 代价:下次构建要重新下载依赖、重新编译,慢一些
# 还可以加 -a(all):删除所有未被使用的构建缓存,不只是悬空的
# 还可以加 --filter until=24h:保留最近 24 小时用过的缓存,只删更早的

docker image prune -f
# 只删“悬空镜像”(dangling):docker images 里显示为 <none>:<none> 的那些
# 用同一个名字和 tag 重新构建时,旧镜像的名字被新镜像拿走,旧的就变成悬空镜像
# 它们没有名字,也没有任何容器在用,删掉没有代价
# -f:同上,跳过确认
# 还可以加 -a(all):删除所有没有被任何容器引用的镜像,不只是悬空的
# 代价:别的项目暂时没起的镜像也会被删,下次要重新拉取或构建

docker container prune
# 删除所有已停止的容器
# 代价:别的项目停着、打算以后再启动的容器也会被删

docker volume prune
# 只删没被容器使用的匿名卷(没起名字的卷)
# 还可以加 -a(all):连有名字的卷也删。数据库数据通常就在这类卷里,删了就没了

docker system prune
# 一次清理:已停止的容器、没被使用的网络、悬空镜像、构建缓存
# 还可以加 -a(all):镜像范围扩大到所有没被容器引用的
# 还可以加 --volumes:额外删除匿名卷

docker builder prune 和 docker image prune 不加 -a 时,只删“没名字”和“可再生”的东西,日常清理用这两条就够了。这次就只跑了这两条,Doris 建表恢复正常。加了 -a 的 docker image prune、docker volume prune,以及 docker system prune,范围都大,本机还跑着其他项目时,先用 docker system df -v 确认要删的内容。

治本:调大 Docker Desktop 的磁盘上限

清理只是腾空间。经常构建镜像的话(每次重新构建都会留下旧镜像和缓存),磁盘迟早又满,可以在 Docker Desktop 的 Settings → Resources → Advanced 里调大 Disk usage limit(虚拟磁盘上限),设置项名称以官方文档为准。

常用命令速查表

命令 作用
docker run -d 镜像 后台启动容器
docker run -p 宿主:容器 端口映射,冒号左边是宿主机
docker run -v 宿主:容器 目录挂载,有状态数据必挂
docker run -e KEY=VAL 设置容器内环境变量
docker run --name 名字 给容器命名
docker run --rm 退出后自动删除,临时容器用
docker run -it 镜像 sh 交互式进入容器 shell
docker ps / ps -a 运行中的容器 / 含已退出的
docker logs -f 容器 实时跟踪容器日志
docker exec -it 容器 sh 进入运行中的容器
docker stop / rm 停止 / 删除容器
docker images 本地镜像列表
docker pull 镜像:tag 拉取镜像,生产写死具体 tag
docker rmi 镜像 删除本地镜像
docker build -t 名:tag . 按 Dockerfile 构建镜像
docker network ls 列出网络
docker network inspect 网络 网络详情:网段、网关、容器 IP
docker inspect 容器/镜像 完整配置 JSON
docker inspect -f 模板 只取 JSON 里指定的字段
docker port 容器 只看端口映射
docker info 引擎配置与状态
docker stats 实时资源占用,容器版 top
docker system df 镜像/容器/数据卷磁盘占用
docker builder prune -f 清悬空的构建缓存,-f 跳过确认;下次构建变慢
docker image prune -f 清悬空镜像(<none>),-f 跳过确认
docker system prune 一次清停止的容器、闲置网络、悬空镜像和构建缓存
docker compose up -d 按 compose.yaml 拉起整套服务
docker compose down 整套停掉并删容器
docker compose logs -f 聚合跟踪所有服务日志

写到这里

Docker 上手的关键是概念先行:镜像是模板、容器是实例、容器生死跟着主进程走、心疼的数据必须挂载出来。这四句话立住之后,命令都只是查表的事。Dockerfile 多阶段构建和 compose 是从”会用”到”用好”的两步——前者让镜像从 GB 瘦到 MB,后者让多服务环境一条命令起停。再往深走就是网络模型和编排(Kubernetes)的领域了,日常开发部署,本文这套已经够用。