POST · DevOps · 文章
Docker 从入门到进阶
2026/11/13 · by ZHANG XING YUN · 阅读 1 次 · 4251 字 · 预计阅读 11 分钟
本文覆盖 Docker 的核心概念、常用指令、Dockerfile 写法、数据与网络、Compose 编排,以及生产实践。每节都给出可直接执行的命令或配置。
- 环境:Docker 24+(本文命令在 Linux 与 Docker Desktop 上通用)
一、Docker 解决什么问题
同一份代码在不同机器上运行结果不一致,通常来自三类差异:运行时版本、系统库、配置与文件路径。Docker 把"应用 + 运行时 + 依赖 + 配置"打包成一个镜像,让同一镜像在任何安装了 Docker 的机器上以相同方式启动。
与虚拟机的区别:
| 维度 | 虚拟机 | 容器 |
|---|---|---|
| 隔离层次 | 硬件虚拟化,包含完整内核 | 进程级隔离(namespace + cgroup),共享宿主机内核 |
| 启动时间 | 分钟级 | 秒级 |
| 体积 | GB 级 | MB 级 |
| 隔离强度 | 强 | 较弱(内核共享) |
| 适用 | 需要不同内核、强隔离 | 应用打包与部署 |
容器不是"轻量虚拟机",它就是一个被限制与隔离的普通进程。因此容器内的进程是 1 号进程,负责接收信号;这直接影响优雅停机与 PID 1 的 zombie 问题(见第十二节)。
二、核心概念
| 概念 | 说明 | 类比 |
|---|---|---|
| 镜像 image | 只读模板,由多层文件系统叠加而成 | 类 / 安装包 |
| 容器 container | 镜像的运行实例,带一个可写层 | 对象 / 运行中的进程 |
| 层 layer | Dockerfile 每条指令生成一层,可被缓存复用 | 增量提交 |
| 仓库 registry | 存放镜像的服务(Docker Hub、Harbor 等) | 代码仓库 |
| 标签 tag | 镜像的版本标识,如 nginx:1.27-alpine |
git tag |
| 卷 volume | 由 Docker 管理的数据目录,生命周期独立于容器 | 外挂磁盘 |
关键性质:
- 镜像层只读,容器改动写入最上层的可写层;容器删除后可写层随之消失,所以数据必须放在卷里。
- 镜像层可复用:多个镜像共用基础层时,本地只存一份。
- 容器默认隔离网络与文件系统,但共享内核。
三、安装与验证
Linux(以 Ubuntu 为例,使用官方脚本):
curl -fsSL https://get.docker.com | sudo sh
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER" # 免 sudo,需重新登录生效
验证:
docker version # 客户端与服务端版本
docker info # 存储驱动、根目录、资源限制
docker run --rm hello-world
docker info 中需要关注的字段:
Storage Driver:overlay2为常见选择。Docker Root Dir:镜像与容器数据的存放位置,磁盘规划要看这里。Cgroup Version:v2 为现代发行版默认。
四、容器生命周期命令
docker run -d --name web -p 8080:80 nginx:1.27-alpine
docker ps # 运行中的容器
docker ps -a # 含已停止
docker logs -f web # 跟踪日志
docker exec -it web sh # 进入容器(镜像内有 sh 或 bash)
docker stop web
docker start web
docker restart web
docker rm -f web # 强制删除运行中的容器
docker run 常用参数:
| 参数 | 作用 |
|---|---|
-d |
后台运行 |
--name |
指定容器名 |
-p 宿主机端口:容器端口 |
端口映射 |
-e KEY=VALUE |
环境变量 |
--env-file .env |
从文件读取环境变量 |
-v 卷名:容器路径 |
挂载卷 |
-v ./本地目录:容器路径 |
挂载宿主机目录(bind mount) |
--network |
指定网络 |
--restart unless-stopped |
重启策略 |
--rm |
退出后自动删除容器 |
-m 512m --cpus 1 |
资源限制 |
--health-cmd |
健康检查命令 |
# 后台运行的 Node 服务,挂载代码与环境变量,限制资源
docker run -d --name api \
-p 3000:3000 \
--env-file .env \
-v "$PWD":/app -w /app \
-m 512m --cpus 1 \
--restart unless-stopped \
node:20-alpine node app.js
容器内 PID 1 默认是启动命令。若命令不是为容器设计的(如 shell 脚本不转发信号),
docker stop会等 10 秒后SIGKILL。解决方式是用exec形式启动,或引入tini/dumb-init。
五、镜像命令
docker pull nginx:1.27-alpine
docker images # 本地镜像
docker image inspect nginx:1.27-alpine # 查看镜像元数据(Entrypoint、Env、Layer)
docker tag myapp:latest registry.example.com/team/myapp:v1.2.0
docker push registry.example.com/team/myapp:v1.2.0
docker rmi registry.example.com/team/myapp:v1.2.0
docker image prune -a # 清理未被任何容器使用的镜像
docker run 与 docker exec 的区别:
run用镜像创建并启动一个新容器。exec在已运行的容器里执行命令,用于调试。
镜像 tag 规范建议:
registry/项目/服务:语义版本-构建号
registry.example.com/shop/api:1.4.2-20260113
registry.example.com/shop/api:latest # 仅供本地,不要用于生产
生产环境不要用 latest:它不可追溯,回滚时无法确定对应哪次构建。
六、数据持久化
三种挂载方式:
| 方式 | 写法 | 数据位置 | 适用 |
|---|---|---|---|
| 匿名卷 | -v /data |
Docker 管理的随机目录 | 临时数据,几乎不用 |
| 命名卷 | -v pgdata:/var/lib/postgresql/data |
Docker 管理的固定目录 | 数据库等需要持久化的数据 |
| 绑定挂载 | -v ./html:/usr/share/nginx/html |
宿主机指定目录 | 本地开发、挂载配置与静态文件 |
docker volume create pgdata
docker volume ls
docker volume inspect pgdata
docker run -d --name db -v pgdata:/var/lib/postgresql/data postgres:16-alpine
注意点:
- 命名卷在
docker rm后仍存在,docker compose down -v或docker volume rm才会删除。 - 绑定挂载的宿主机目录会覆盖容器内同路径内容;挂载空目录到代码目录会得到空目录。
- 数据库数据目录必须使用卷,否则容器重建即丢数据。
- 卷只是持久化,不是备份;备份要在应用或数据库层做。
七、网络
7.1 默认网络类型
| 网络 | 说明 |
|---|---|
bridge |
默认。容器通过 NAT 访问外网,容器间需自定义网络才能互相解析 |
host |
共享宿主机网络栈,端口不隔离(Linux) |
none |
无网络 |
| 自定义 bridge | 推荐。提供容器名 DNS 解析与隔离 |
docker network create app-net
docker run -d --name api --network app-net myapp:latest
docker run -d --name db --network app-net postgres:16-alpine
# api 容器内可以直接用 db:5432 访问数据库
7.2 端口映射
-p 8080:80 # 宿主机 8080 → 容器 80
-p 127.0.0.1:8080:80 # 只监听回环地址,外部不可访问
-p 8080:80/udp # UDP
只暴露业务需要的端口,数据库端口默认不要映射到公网。
7.3 容器访问宿主机
- Linux:使用宿主机在 bridge 网络中的网关地址(通常
172.17.0.1),或--add-host host.docker.internal:host-gateway。 - Docker Desktop(Windows/macOS):直接使用
host.docker.internal。
八、Dockerfile 指令详解
8.1 指令表
| 指令 | 作用 | 注意 |
|---|---|---|
FROM |
基础镜像 | 尽量指定精确版本;AS name 用于多阶段 |
WORKDIR |
设置工作目录 | 后续命令与 CMD 的相对路径基准 |
COPY |
复制文件 | 优先于 ADD;支持 --chown |
ADD |
复制文件或解压 tar、下载 URL | 行为不直观,除解压外不建议使用 |
RUN |
构建时执行命令 | 多条命令用 && 合并、以 \ 换行,减少层数 |
ENV |
设置环境变量 | 会进入镜像,不能放密钥 |
ARG |
构建参数 | 只在构建期有效;不要把密钥当 ARG(会留在镜像历史) |
EXPOSE |
声明端口 | 只是文档性质,不做映射 |
USER |
指定运行用户 | 生产必须使用非 root |
VOLUME |
声明数据目录 | 会创建匿名卷,需与挂载配合 |
HEALTHCHECK |
定义健康检查 | 与编排层的 healthcheck 配合 |
ENTRYPOINT |
容器入口 | 配合 CMD 提供默认参数 |
CMD |
默认命令 | 可被 docker run 参数覆盖 |
SHELL |
修改 shell | 一般不需要 |
8.2 RUN、CMD、ENTRYPOINT 的区别
RUN:构建期执行,结果写入镜像层。CMD:运行期的默认命令,可被覆盖。ENTRYPOINT:运行期的固定入口,CMD作为其默认参数。
三种组合:
# 1) 只有 CMD:docker run image xxx 会覆盖
CMD ["node", "app.js"]
# 2) 只有 ENTRYPOINT:docker run image --port 3000 追加参数
ENTRYPOINT ["node", "app.js"]
# 3) ENTRYPOINT + CMD:默认参数可被覆盖,入口不变
ENTRYPOINT ["node", "app.js"]
CMD ["--port", "3000"]
必须使用 exec 形式(JSON 数组)。shell 形式(CMD node app.js)会以 /bin/sh -c 启动,导致进程不是 PID 1,信号收不到,docker stop 只能强杀。
8.3 生产级 Dockerfile
# 阶段一:安装依赖(利用缓存,仅 package 文件变化时重装)
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# 阶段二:构建
FROM deps AS build
COPY tsconfig.json ./
COPY src ./src
RUN npm run build && npm prune --omit=dev
# 阶段三:运行
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -S app && adduser -S app -G app
COPY --from=build --chown=app:app /app/package.json ./package.json
COPY --from=build --chown=app:app /app/node_modules ./node_modules
COPY --from=build --chown=app:app /app/dist ./dist
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -qO- http://127.0.0.1:3000/health || exit 1
ENTRYPOINT ["node", "dist/main.js"]
要点:
- 依赖与源码分层复制:先
COPY package*.json再npm ci,源码变化不会触发重装。 - 多阶段构建:编译工具只存在于构建阶段,运行镜像更小、攻击面更少。
- 非 root 运行:
adduser+USER app。 --chown:避免运行用户无权读取文件。
8.4 构建缓存
缓存命中的前提是指令与其输入未变化。调整顺序即可利用缓存:
COPY package.json package-lock.json ./ # 变化少
RUN npm ci
COPY . . # 变化频繁,放后面
常见缓存失效原因:
COPY . .把无关文件(日志、临时文件)带进上下文,触发缓存失效,用.dockerignore排除。RUN apt-get update与RUN apt-get install分开写,时间久了缓存过期导致安装失败。
8.5 .dockerignore
node_modules
dist
.git
*.log
.env
.DS_Store
不排除 node_modules 会导致本地依赖覆盖容器内依赖,出现"本机正常、容器报错"。
九、镜像构建与分发
docker build -t myapp:1.0.0 .
docker build -f deploy/Dockerfile -t myapp:1.0.0 .
docker build --no-cache -t myapp:debug .
docker history myapp:1.0.0 # 查看各层大小,定位臃肿来源
docker history 是排查镜像体积的第一步:找出体积最大的层,再决定合并或清理。
推送:
docker login registry.example.com
docker tag myapp:1.0.0 registry.example.com/team/myapp:1.0.0
docker push registry.example.com/team/myapp:1.0.0
镜像体积优化清单:
- 用 alpine 或 slim 基础镜像,注意 musl 与 glibc 差异。
- 多阶段构建,运行阶段不带编译工具。
- 清理包管理器缓存:
rm -rf /var/lib/apt/lists/*、npm cache clean --force。 - 合并
RUN减少层数。 - 不要把
.git、测试数据、文档打进镜像。
十、Compose 编排
10.1 示例
services:
web:
build: ./web
ports:
- "8080:80"
depends_on:
api:
condition: service_healthy
restart: unless-stopped
api:
image: node:20-alpine
working_dir: /app
command: node server.js
volumes:
- ./api:/app
environment:
DATABASE_URL: postgres://app:secret@db:5432/appdb
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/health || exit 1"]
interval: 10s
timeout: 3s
retries: 5
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 5s
timeout: 3s
retries: 10
volumes:
pgdata:
10.2 常用字段
| 字段 | 作用 |
|---|---|
image / build |
使用现成镜像或构建 |
command / entrypoint |
覆盖镜像默认命令 |
environment / env_file |
环境变量 |
ports / expose |
端口映射 / 仅内部声明 |
volumes |
卷与绑定挂载 |
depends_on |
启动顺序,condition: service_healthy 表示等健康 |
healthcheck |
健康检查(可覆盖镜像中的) |
restart |
no / on-failure / always / unless-stopped |
deploy.resources |
资源限制(Compose v2 也支持 mem_limit、cpus) |
profiles |
分组启动,默认不启动带 profile 的服务 |
10.3 常用命令
docker compose up -d # 启动(后台)
docker compose up -d --build # 重新构建后启动
docker compose ps
docker compose logs -f api
docker compose exec api sh
docker compose restart api
docker compose stop
docker compose down # 删除容器与网络,保留卷
docker compose down -v # 同时删除卷(数据会丢)
docker compose config # 校验并输出最终配置(含变量替换)
docker compose config 是最实用的排查命令:它会把变量替换后的最终配置打印出来,能立刻发现"环境变量没生效""端口映射写错"等问题。
10.4 环境变量与覆盖
# .env(Compose 自动读取)
API_PORT=8080
POSTGRES_PASSWORD=secret
services:
web:
ports:
- "${API_PORT}:80"
区分两件事:
- Compose 文件里的
${VAR}由 Compose 从.env或 shell 环境读取,用于生成配置。 environment/env_file是传给容器内进程的变量。
两者用途不同,混用会导致"变量明明设置了但容器里读不到"。
十一、资源限制与日志
docker run -d --name api \
-m 512m --memory-swap 512m \
--cpus 1.5 \
--pids-limit 200 \
myapp:1.0.0
| 参数 | 含义 |
|---|---|
-m |
内存上限,超出会被 OOM Killer 杀掉(exit code 137) |
--memory-swap |
内存 + swap 总上限 |
--cpus |
CPU 配额 |
--pids-limit |
进程数上限,防止 fork 炸弹 |
日志驱动:
docker run -d --log-driver json-file \
--log-opt max-size=50m --log-opt max-file=3 myapp:1.0.0
不限制日志大小,长期运行会把磁盘写满。
清理:
docker system df # 查看占用
docker system prune -af # 清理未使用的镜像、容器、网络(谨慎)
docker container prune
docker image prune -a
docker volume prune
十二、常见错误与排查
| 现象 | 原因 | 处理 |
|---|---|---|
| 容器启动后立刻退出 | 主进程执行完就结束,或命令拼错 | docker logs 看输出;确认 CMD 是常驻进程 |
Exit Code 137 |
被 OOM Killer 杀,或 docker kill |
提高内存限制,或排查内存泄漏 |
Exit Code 139 |
段错误,常见于二进制与平台不匹配(arm64/amd64) | 用 --platform 指定正确架构 |
port is already allocated |
端口被占用 | ss -ltnp 查占用进程,或改端口 |
容器内 localhost 连不上服务 |
localhost 指容器自身 |
用服务名或 host.docker.internal |
permission denied 写挂载目录 |
容器用户与宿主机文件属主 UID 不一致 | 调整 --user、文件权限或使用命名卷 |
构建时 COPY failed |
文件不在构建上下文,或被 .dockerignore 排除 |
检查路径与忽略规则 |
| 本地正常、容器内报错 | node_modules 被挂载覆盖,或基础镜像版本不同 |
排除 node_modules,固定基础镜像版本 |
docker stop 要等 10 秒 |
进程不是 PID 1 或不处理信号 | 用 exec 形式启动;应用监听 SIGTERM |
| 磁盘被占满 | 镜像层、日志、卷堆积 | docker system df 定位后清理 |
排查顺序:
docker ps -a # 1. 容器状态与退出码
docker logs --tail=100 <容器> # 2. 应用日志
docker inspect <容器> | less # 3. 配置、环境变量、网络、挂载
docker exec -it <容器> sh # 4. 进容器验证网络与文件
docker stats # 5. 资源占用
十三、最佳实践
- 一个容器一个进程,不要在一个容器里跑 nginx + 应用。
- 固定基础镜像版本(
node:20.7-alpine3.20),不要用latest。 - 用多阶段构建,运行镜像不含编译工具。
- 非 root 运行,并配合只读根文件系统(
--read-only+ 临时目录)进一步提升安全。 - 配置通过环境变量或挂载注入,镜像不包含密钥。
- 数据用命名卷,备份在卷外做。
- 设置健康检查,编排层才能判断服务是否可用。
- 设置日志轮转与资源限制,避免单容器拖垮宿主机。
- 用
.dockerignore排除无关文件,减小上下文并保护缓存命中。
十四、FAQ
Q1:容器和虚拟机怎么选?
需要不同内核、强隔离或图形环境时用虚拟机;应用打包、CI、微服务部署用容器。两者可以叠加:虚拟机里跑容器。
Q2:COPY 和 ADD 有什么区别?
COPY 只复制文件。ADD 额外支持自动解压 tar 与下载 URL,行为不直观,除了解压场景外建议用 COPY。
Q3:为什么必须用 exec 形式的 CMD?
shell 形式会以 /bin/sh -c 启动,应用不是 PID 1,收不到 SIGTERM,优雅停机失效。
Q4:修改代码后镜像没变化?
构建缓存命中。确认 COPY 顺序、.dockerignore,必要时 --no-cache。
Q5:容器里的数据为什么重启就没了?
数据写在可写层,容器重建即丢。数据库、上传文件必须用卷。
Q6:docker compose down 会删数据吗?
默认不会,卷会保留;加 -v 才会删除卷。
Q7:如何减小镜像体积?
换基础镜像、多阶段构建、合并 RUN、清理缓存、排除无关文件。用 docker history 定位最大的层。
Q8:多个容器如何互相访问?
放到同一个自定义网络,用服务名(即容器名)访问,不要用 IP。
Q9:latest 为什么不能用于生产?
不可追溯,无法确定某次发布对应哪个镜像;回滚与审计都会失效。
Q10:容器时间不对怎么办?
设置 TZ 环境变量,并确认基础镜像包含时区数据(alpine 需安装 tzdata)。
附录:命令速查
# 镜像
docker pull <image>:<tag>
docker images
docker build -t <image>:<tag> .
docker history <image>:<tag>
docker rmi <image>:<tag>
docker image prune -a
# 容器
docker run -d --name <name> -p 8080:80 -e K=V -v vol:/path <image>
docker ps -a
docker logs -f --tail=100 <name>
docker exec -it <name> sh
docker inspect <name>
docker stats
docker stop <name> && docker rm <name>
# 数据与网络
docker volume ls
docker volume inspect <vol>
docker network create <net>
docker network ls
docker network inspect <net>
# Compose
docker compose up -d --build
docker compose ps
docker compose logs -f <svc>
docker compose exec <svc> sh
docker compose config
docker compose down -v
# 清理
docker system df
docker system prune -af