DevOps ·
Docker 入门:镜像、容器、网络与日常排障
Docker 已经成为现代软件交付的标准。本文从安装、Dockerfile 最佳实践、镜像构建缓存、网络/存储模型、Compose 编排,到线上 10+ 常见报错、性能分析逐一整理。
Docker 入门:镜像、容器、网络与日常排障
Docker 把程序 + 依赖 + 运行环境打包进一个”镜像”,在任何 Linux / macOS / Windows 机器上拉下来就能跑。它解决了”我机器上能跑啊”的千年难题,也是整个云原生世界的基础。
一、核心概念速览
| 概念 | 一句话说明 |
|---|---|
| Image(镜像) | 只读的”模板”,比如 nginx:1.27-alpine,由一层层文件系统叠加组成 |
| Container(容器) | 镜像”跑起来”的实例,基于写时复制(Copy-on-Write)有独立可写层 + 进程空间 |
| Registry | 镜像仓库:Docker Hub、阿里云 ACR、Harbor、GitHub GHCR |
| Dockerfile | 描述如何一步步构建镜像的文本文件 |
| Volume | 容器数据持久化方案,绕开 Union FS,直接写宿主机 |
| Bind Mount | 把宿主机某目录直接挂到容器里 |
| Network | bridge / host / none / container:xxx / 用户自定义 bridge |
| Compose | 用 YAML 一次性编排启动多个容器(Web + DB + Redis…) |
| BuildKit | Docker 23.0+ 默认构建引擎,比老 builder 快、支持缓存导出、多阶段更聪明 |
| Layer(层) | Dockerfile 每条指令生成一层,完全相同的指令会命中缓存,这就是优化构建速度的根 |
二、安装
2.1 macOS / Windows(桌面版)
直接装 Docker Desktop:https://www.docker.com/products/docker-desktop/
- 自带 Docker Engine + CLI + Compose v2 + K8s 单节点 + GUI
- macOS 用 HyperKit / Apple Virtualization;Win 用 WSL2
检查:
docker version # 看 Client / Engine 版本
docker compose version # 确认 Compose v2
docker info | head -30
2.2 Linux(Ubuntu / Debian 生产服务器)
# 1) 卸载旧版本
for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do
sudo apt remove -y $pkg
done
# 2) 加官方源
sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
# 3) 安装
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# 4) 让非 root 也能跑 docker(重新登录生效)
sudo usermod -aG docker $USER
# 5) (可选) 国内镜像加速
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json << 'EOF'
{
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker.m.daocloud.io"
],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "5"
},
"storage-driver": "overlay2"
}
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now docker
# 验证
docker run --rm hello-world
CentOS / Rocky / RHEL 换源用 download.docker.com/linux/centos 即可,步骤类似。
三、第一个容器 & 常用命令
# 拉镜像
docker pull nginx:1.27-alpine
# 看本地镜像
docker images
# 启动容器(-d 后台 -p 端口映射 --name 起名 -v 挂载)
docker run -d --name my-nginx -p 8080:80 \
-v /data/html:/usr/share/nginx/html:ro \
--restart=unless-stopped \
nginx:1.27-alpine
# 看运行中的容器
docker ps
# 看所有(包括已停止)
docker ps -a
# 日志
docker logs my-nginx
docker logs -f --tail 200 my-nginx # 实时跟 最新200行
# 进容器(推荐用 exec -it,别用 attach)
docker exec -it my-nginx /bin/sh # alpine 没 bash
# 停止 / 启动 / 删除
docker stop my-nginx
docker start my-nginx
docker rm my-nginx # 运行中的要加 -f
docker rm -f $(docker ps -aq) # 危险:删所有容器
# 磁盘占用大起底
docker system df
docker system prune -a # 清理停止的容器、无用镜像、虚悬镜像
docker builder prune --all # 清构建缓存
docker volume prune # 清没被使用的 volume
四、Dockerfile 最佳实践
4.1 模板:Node.js 多阶段构建(经典案例)
# ---------------- 阶段 1:构建 ----------------
FROM node:20-alpine AS builder
WORKDIR /app
# 先只拷贝依赖描述文件,充分利用缓存
COPY package*.json ./
RUN npm ci --only=production=false --no-audit --no-fund
# 再拷贝源码(源码经常变,放后面避免每次装依赖)
COPY . .
# 前端构建 / 编译 TS 等
RUN npm run build
# ---------------- 阶段 2:运行 ----------------
FROM node:20-alpine AS runner
ENV NODE_ENV=production \
PORT=3000
# 不要用 root 跑容器
RUN addgroup -S nodeapp && adduser -S nodeapp -G nodeapp
WORKDIR /app
RUN chown -R nodeapp:nodeapp /app
USER nodeapp
# 从 builder 阶段拷贝需要的东西,dev 依赖全丢掉
COPY --from=builder --chown=nodeapp:nodeapp /app/dist ./dist
COPY --from=builder --chown=nodeapp:nodeapp /app/package*.json ./
RUN npm ci --omit=dev --no-audit --no-fund
EXPOSE 3000
# 健康检查(可选但推荐)
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://127.0.0.1:3000/healthz || exit 1
# 用 exec 形式(JSON 数组),保证信号正确传递给 node
CMD ["node", "dist/server.js"]
4.2 构建镜像 & 运行
docker build -t myapp:v1.0.0 .
docker run -d -p 3000:3000 --name myapp myapp:v1.0.0
4.3 Dockerfile 优化规则
| 原则 | 怎么做 | 原因 |
|---|---|---|
| 命中缓存 | 变更频率低的指令放上面(apt install / npm ci / pip install) | 一层没改,下面的缓存就都命中 |
| 多阶段构建 | 构建工具全放 builder,最终 runner 只含运行时 | 镜像体积从 1GB 能减到 100MB |
| 用 slim/alpine 基础镜像 | node:20-alpine > node:20 > node:20-bookworm | 小 + 攻击面小 |
| 不要用 root | useradd + USER xxx | 容器被攻破也拿不到宿主机 root |
| 合并 RUN 指令 | RUN apt update && apt install -y ... && rm -rf /var/lib/apt/lists/* | 每层分开存,写在一个 RUN 里才会把缓存包清干净 |
| 忽略无关文件 | 写 .dockerignore,排除 node_modules、.git、日志等 | 构建上下文(build context)越小传得越快 |
| 明确版本 | node:20.17.0-alpine3.20 而不是 node:latest | latest 指不定今天是谁,构建不可复现 |
| 敏感信息 | 不要用 ENV 传密码!用构建时 --secret 或运行时环境变量 / Secret | docker history 能看到 ENV 的明文 |
一个好的 .dockerignore:
.git
.gitignore
node_modules
npm-debug.log
Dockerfile
.dockerignore
README.md
.env.*
dist
coverage
.idea
.vscode
五、网络模型
| 驱动 | 说明 | 典型场景 |
|---|---|---|
| bridge(默认) | 容器通过一个网桥(docker0)出网,互相通过 IP 访问 | 单机多容器互相访问 |
| host | 容器和宿主机共享 Network Namespace,端口不冲突就行 | 想让容器网络性能跟宿主机一样(比如压测、Nginx 大流量) |
| none | 没有网卡 | 安全隔离(容器完全断网) |
| container:id | 跟另一个容器共享网络栈 | Sidecar 模式(比如 envoy 代理 app 的流量) |
| 自定义 bridge | docker network create mynet,容器用 --net mynet 加入 | 推荐:自带 DNS,可以用容器名直接互联 |
自定义网络示例:
docker network create mynet -d bridge
docker run -d --name db --net mynet -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0
docker run -d --name web --net mynet -p 8080:80 nginx
# web 里直接 ping / 连 "db:3306" 就行,不用查 IP
六、持久化:Volume vs Bind Mount
| 方式 | 命令 | 适用场景 |
|---|---|---|
| Volume(推荐) | -v mydata:/var/lib/mysql | 数据库、独立于容器生命周期的数据。Docker 管理,portable |
| Bind Mount | -v /srv/mysql:/var/lib/mysql | 宿主机上现成的目录要给容器用(开发时挂源码、配置文件) |
备份 volume:
# 用一个临时容器把 volume 打成 tar
docker run --rm -v mydata:/data -v $(pwd):/backup alpine \
tar czf /backup/mydata-$(date +%Y%m%d).tar.gz -C /data .
七、Docker Compose
compose.yml:
services:
web:
build: .
image: myapp:v1
ports:
- "8080:3000"
environment:
NODE_ENV: production
DB_HOST: db
DB_PORT: 3306
DB_PASSWORD: ${DB_PASSWORD} # 从 .env 文件或环境变量读
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/healthz"]
interval: 30s
retries: 3
db:
image: mysql:8.0
volumes:
- dbdata:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-uroot", "-p${DB_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redisdata:/data
restart: unless-stopped
volumes:
dbdata:
redisdata:
运行:
# 启动(-d 后台)
docker compose up -d
# 看日志
docker compose logs -f web
# 扩缩容(只能扩无状态服务)
docker compose up -d --scale web=3
# 停 + 删容器
docker compose down
# 连同 volume 一起删(注意数据会丢)
docker compose down -v
八、日常排障手册
8.1 调试工具箱
| 需求 | 命令 |
|---|---|
| 容器起不来,到底在报什么 | docker ps -a 找到 ID → docker logs <ID>;看之前的日志不要只看最后 |
| 容器内进程 / 端口 / 文件 | docker exec -it <ID> sh + ps auxf / ss -lntp / du -sh /(镜像里没这些命令的话用 nsenter 或者 docker debug) |
| 镜像多大,哪层肥 | docker images;docker history myapp:v1 --no-trunc;更推荐 dive 工具 |
| 容器网络不通 | 1. docker inspect <ID> | jq '.[0].NetworkSettings.Networks' 看 IP/网关2. docker run --rm --net container:<ID> nicolaka/netshoot ss -lntp / ping / curl 工具容器3. 宿主机 iptables FORWARD 链是否 DROP |
| 磁盘莫名满 | docker system df → docker system prune -a → 看 overlay2 哪层大 → du -sh /var/lib/docker/overlay2/* | sort -hr | head |
| 退出码是什么意思 | docker inspect <ID> --format '{{.State.ExitCode}}',137=OOMKilled,143=SIGTERM,1=应用自身报错 |
| OOM Killed | dmesg | grep -i oom;docker inspect ... MemoryLimit 设小了;--memory=1g 调大或优化程序 |
| 配置文件改了容器里没生效 | bind mount 的文件如果宿主用 mv 覆盖(比如 sed -i),inode 会变。要么重启容器,要么 sed --in-place=... 改同一个文件 |
| 宿主机访问容器端口不通 | ① docker port <container> 端口映射有没有;② 防火墙 iptables/firewalld;③ SELinux;④ 容器里程序是不是只绑了 127.0.0.1(要绑 0.0.0.0) |
8.2 常见报错对照表
| 报错关键字 | 根因 / 排查 |
|---|---|
permission denied while trying to connect to the Docker daemon socket at unix:///... | 当前用户不在 docker 组。sudo usermod -aG docker $USER + 重新登录(或 newgrp docker) |
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? | dockerd 没起来。sudo systemctl start docker;检查 daemon.json 语法错误 |
Error response from daemon: pull access denied for xxx, repository does not exist or may require 'docker login' | 私有镜像要先 docker login <registry>,或者镜像名写错了 |
Error: No such container: xxx | 容器名 / ID 写错,或者已经 rm 了。docker ps -a 看看 |
port is already allocated | 宿主机端口被占了。ss -lntp | grep <PORT> 查谁占了,或者改映射 |
ERROR: failed to solve: failed to compute cache key: failed to calculate checksum of ref ...: "/xxx": not found | Dockerfile 里 COPY xxx yyy 的 xxx 在 build context 里不存在 / 被 .dockerignore 排除了 |
COPY failed: file not found in build context or excluded by .dockerignore | 同上,检查路径 / .dockerignore |
exec /bin/sh: exec format error | 平台架构不匹配:Mac M 系列(arm64)构建的镜像跑到 Linux x86_64 上了。用 docker buildx build --platform linux/amd64 . 跨平台构建 |
Temporary failure resolving 'deb.debian.org' (RUN apt update) | 容器内 DNS 不通。daemon.json 加 "dns": ["223.5.5.5","8.8.8.8"];或宿主机 /etc/resolv.conf 配置问题 |
OCI runtime create failed: container_linux.go:380: starting container process caused: exec: "xxx": executable file not found in $PATH | CMD 或 ENTRYPOINT 里写的程序文件不存在 / 路径错 / 没可执行权限 |
no space left on device | 磁盘 / inode 满。docker system prune -a --volumes;检查 overlay2 大小;docker builder prune |
HealthCheck: unhealthy | docker inspect <ID> 里 State.Health.Log[] 看最近一次命令真实输出和退出码 |
depends_on 服务起来但应用还没 ready | depends_on 默认只等”容器启动”,不等”应用可用”。用 condition: service_healthy + 给依赖服务加 healthcheck |
8.3 性能分析:容器卡 / 慢
docker stats→ 先看 CPU% / MEM 使用率 / NET I/O / DISK I/O 的实时排行- 某容器居高不下 →
docker top <ID>看里面 PID - 定位到进程号后,可以:
- 宿主机
pidstat -h -u -p <PID> 1(CPU) strace -c -p <PID>(系统调用)perf top -p <PID>(热点函数,要符号表)
- 宿主机
- 网络慢 →
docker run --rm --net host nicolaka/netshoot iperf3 .../mtr测带宽/丢包
8.4 构建慢怎么办
- 使用 BuildKit(默认了,但 CI 里确认
DOCKER_BUILDKIT=1) - RUN —mount=type=cache,target=/root/.npm:把包管理器缓存持久化
COPY package.json先于COPY .- Registry Mirror:国内源拉基础镜像
- 多平台并行构建:
buildx build --platform linux/amd64,linux/arm64 -t repo/image --push . - CI 里导出/导入缓存:
--cache-from type=gha --cache-to type=gha(GitHub Actions)
九、安全加固清单
- 不要用
:latest,明确版本号 - 不要以 root 运行,Dockerfile 里
USER nonroot dockerd只监听 Unix socket,不要绑定tcp:2375(裸 TCP 约等于给全世界 root 权限);一定要远程管理就用 TLS:2376- 敏感变量别写进
ENV/ 镜像层,用:- 运行时
--env-file /etc/default/myapp.env(文件权限0600) - Docker Secrets(Swarm)或 HashiCorp Vault / 云 KMS
- 运行时
- 开启内容信任(Notary / Cosign):
export DOCKER_CONTENT_TRUST=1 - 定期镜像扫描:Trivy / Grype 扫 CVE:
trivy image myapp:v1 - 镜像精简 + 只读根文件系统:
--read-only+tmpfs给需要写的路径
参考资料
- Docker 官方文档:https://docs.docker.com/
- Dockerfile 最佳实践:https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
- Compose 规范:https://docs.docker.com/compose/compose-file/
- Trivy 镜像安全扫描:https://github.com/aquasecurity/trivy
- Dive(镜像层分析):https://github.com/wagoodman/dive
- netshoot(网络排障瑞士军刀镜像):https://github.com/nicolaka/netshoot