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
← 返回帖子 更多DevOps内容 →