外观
Docker 系统学习文档
从零开始掌握 Docker,涵盖 Docker Desktop 和 Linux Docker
从基础概念到项目开发、部署、CI/CD 全流程
目录
第一篇:Docker 基础概念
第二篇:安装与环境搭建
第三篇:Docker 基础操作
第四篇:Dockerfile 编写指南
第五篇:Docker Compose
第六篇:项目开发到部署全流程
第七篇:CI/CD 集成
第八篇:进阶话题
第一篇:Docker 基础概念
1.1 什么是 Docker
Docker 是一个开源的容器化平台,让你可以将应用程序及其依赖打包到一个轻量级、可移植的容器中,然后在任何 Linux 或 Windows 机器上运行。
核心标语:"Build once, run anywhere"(一次构建,到处运行)
能解决什么问题?
在没有 Docker 之前,开发环境经常面临这些问题:
markdown
问题 1:"在我机器上能跑啊"
→ 开发者的系统、依赖版本、配置和生产环境不同
→ 代码部署后各种报错
问题 2:环境配置繁琐
→ 新同事加入,花 2 天配置开发环境
→ 安装 Node.js、MySQL、Redis、Nginx……
→ 版本冲突、系统兼容性问题
问题 3:多项目隔离
→ 项目 A 需要 Node 20,项目 B 需要 Node 22
→ 安装、卸载、切换版本……Docker 的解决方案:
yaml
# 用 Docker 描述项目环境,一行命令启动
docker compose up
# Docker 会自动:
# 1. 拉取 Node 22 镜像
# 2. 拉取 MySQL 8.0 镜像
# 3. 创建网络让它们通信
# 4. 挂载代码目录
# 5. 启动项目
# 新同事加入:只需装 Docker → git clone → docker compose up1.2 容器 vs 虚拟机
这是 Docker 初学者最需要理解的核心概念。
┌─────────────────────────────────────┐
│ 虚拟机架构 │
├─────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ │
│ │ 应用 A │ │ 应用 B │ │
│ ├──────────┤ ├──────────┤ │
│ │ Bins/Lib │ │ Bins/Lib │ │
│ ├──────────┤ ├──────────┤ │
│ │ Guest OS │ │ Guest OS │ ← 每个 VM 一个完整 OS
│ ├──────────┤ ├──────────┤ │
│ │ Hypervisor (VMware/VirtualBox) │
│ ├──────────────────────────────────┤
│ │ Host OS (Windows/Linux) │
│ └──────────────────────────────────┘
│ │
│ 容器架构 │
├─────────────────────────────────────┤
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ 应用A │ │ 应用B │ │ 应用C │ │
│ ├──────┤ ├──────┤ ├──────┤ │
│ │ Bins │ │ Bins │ │ Bins │ │ ← 共享主机内核
│ └──┬───┘ └──┬───┘ └──┬───┘ │
│ └─────────┼──────────┘ │
│ Docker Engine │
│ ┌──────────────────────────────────┤
│ │ Host OS │
│ └──────────────────────────────────┘核心区别对比:
| 特性 | 虚拟机 | Docker 容器 |
|---|---|---|
| 操作系统 | 每个 VM 有完整的 Guest OS | 共享主机内核 |
| 启动时间 | 分钟级(需启动整个 OS) | 秒级(只启动进程) |
| 占用空间 | GB 级(完整的 OS 镜像) | MB 级(只含应用和依赖) |
| 性能 | 有损耗(虚拟化硬件) | 接近原生 |
| 隔离性 | 强(完全隔离) | 中等(共享内核) |
| 移植性 | 差(不同 hypervisor 不同格式) | 好(OCI 标准) |
容器隔离的内部机制:Namespace + Cgroups + UnionFS
容器本质上只是主机上一个被"加固"的普通进程,没有 Hypervisor、没有 Guest 内核。Linux 内核提供三套机制拼出了"像个独立机器"的错觉:
| 机制 | 负责什么 | 效果 |
|---|---|---|
| Namespace(命名空间) | 隔离:PID、NET、MNT、UTS、IPC、USER 六类 | 容器里 ps 只能看到自己的进程(PID 1),看不到主机进程 |
| Cgroups(控制组) | 限额:CPU、内存、IO、进程数 | --memory=512m 超了就被 OOM Kill,主机不受牵连 |
| UnionFS(如 OverlayFS) | 文件系统:镜像层 + 容器可写层叠加 | 见 1.3 的分层镜像与 copy-on-write |
⚠️ 理解了"共享内核"就理解了容器与虚拟机的安全边界差异:容器逃逸攻击(如内核漏洞 CVE-2019-5736 针对 runc)的目标是主机内核,而虚拟机被 Hypervisor 硬隔离。这就是多租户场景(如给客户跑不可信代码)依然推荐 microVM 方案(Kata/Firecracker)的原因。
什么时候用 Docker,什么时候用虚拟机?
- 需要 Linux 桌面环境 → 虚拟机(Docker 没有 GUI)
- Windows 上跑 Linux 应用 → Docker Desktop(或 WSL2 + Docker)
- 需要完整的内核隔离 → 虚拟机
- 微服务部署 → Docker
- 开发和测试环境 → Docker
1.3 Docker 三大核心概念
1. 镜像(Image)
镜像是一个只读的模板,包含运行应用所需的文件系统和配置。
yaml
# 镜像类比:
# 镜像 = 类(Class)—— 是定义和模板
# 容器 = 实例(Instance)—— 是运行中的镜像
# 镜像的层级结构:
FROM node:22-alpine # 基础镜像(包含 Alpine Linux + Node 22)
WORKDIR /app # 设置工作目录
COPY package*.json ./ # 复制依赖文件
RUN npm install # 安装依赖(在镜像中创建一个新层)
COPY . . # 复制源码
EXPOSE 3000 # 声明端口
CMD ["node", "app.js"] # 默认启动命令
# 每一行指令都会创建一个新层(Layer)
# 层可以被缓存和复用层的内部机制(为什么镜像又小又快):
- 每一层是内容寻址的:层的内容算出 SHA256 摘要作为 ID,内容变一点、ID 全变——所以同一个
node:22-alpine基础层在本机被 10 个镜像共享,只存一份。 - 运行容器时,Docker(存储驱动 overlay2)在所有只读层顶上叠一个可写层(copy-on-write):改文件时先把文件复制进可写层再改,删除只是打"白out 标记"。这就是
docker rm容器后数据消失、docker commit会把可写层固化成新层的原因。 - 构建缓存同理:
docker build时逐条指令算缓存,某条指令(或其前面的 COPY 内容)变了,从这层开始往后全部重建——这就是 4.3 强调"变化少的指令放前面"的底层原因。 - 拉取镜像时按层并行下载、校验摘要、已存在就跳过,所以第二次
docker pull大部分层显示Already exists。
镜像仓库:Docker Hub(官方)、阿里云 ACR、GitHub Container Registry、Harbor(私有)
2. 容器(Container)
容器是镜像的运行实例。你可以对容器进行启动、停止、删除等操作。
bash
# 从镜像启动容器
docker run -d -p 3000:3000 --name my-app node-app
# 查看运行中的容器
docker ps
# 进入容器内部
docker exec -it my-app /bin/sh
# 查看容器日志
docker logs -f my-app3. 仓库(Repository)
仓库用于存储和分发镜像。类似于 Git 仓库管理代码,Docker 仓库管理镜像。
bash
# 拉取镜像
docker pull nginx:latest
# 推送镜像
docker push myusername/my-app:latest
# 搜索镜像
docker search nginx
# 常用仓库地址
# Docker Hub: docker.io/library/nginx
# GitHub: ghcr.io/username/repo
# 私有仓库: registry.example.com/my-app1.4 Docker 架构
┌──────────────────────────────────────────────────┐
│ Client (docker CLI) │
│ docker build / pull / run / push ... │
│ │ │
│ │ REST API (Unix socket / TCP) │
│ ▼ │
│ Docker Daemon (dockerd) │
│ │ │
│ ├── 管理镜像 (Image Management) │
│ │ └── 存储驱动 (overlay2/aufs/...) │
│ ├── 管理容器 (Container Management) │
│ │ └── 运行时 (runc / containerd) │
│ └── 管理网络 (Network Management) │
│ └── CNM (Container Network Model) │
└──────────────────────────────────────────────────┘Docker 使用 客户端-服务器架构:
- Docker Daemon(dockerd):后台服务进程,管理镜像、容器、网络、数据卷
- Docker Client(docker CLI):用户交互的命令行工具
- Docker Registry:镜像仓库
containerd 是 Docker 使用的底层容器运行时。自 Docker 1.11+ 起,Docker Daemon 不再直接管理容器生命周期,而是通过 containerd 来实现:
docker build → dockerd → containerd → runc → 创建容器
docker run → dockerd → containerd → runc → 启动容器runc 是 OCI(Open Container Initiative)标准的参考实现,它负责实际创建和运行容器。
第二篇:安装与环境搭建
2.1 Docker Desktop(Windows/Mac)
Docker Desktop 是官方提供的桌面应用,包含 Docker Engine、Docker CLI、Docker Compose、Kubernetes 等所有组件。
Windows 安装 Docker Desktop
markdown
前置条件:
1. 64 位 Windows 10/11 Pro/Enterprise/Education(推荐)
2. 开启 WSL2(Windows Subsystem for Linux 2)
安装步骤:
1. 下载 Docker Desktop Installer.exe(from docs.docker.com/get-docker)
2. 双击安装,勾选 "Use WSL 2 based engine"(推荐)
3. 安装完成后重启
4. 打开 Docker Desktop,等待 Engine 启动
5. 任务栏出现 Docker 图标(鲸鱼图标)→ 安装完成
验证安装:bash
docker version
# Client: Docker Engine - Community
# Server: Docker Engine - Community
# 看到 Client 和 Server 都正常输出即可
docker run hello-world
# Hello from Docker!
# 表示安装成功Mac 安装 Docker Desktop
markdown
1. 下载 Docker Desktop for Mac(Intel / Apple Silicon 版本不同!)
2. 将 Docker.app 拖到 Applications 文件夹
3. 打开 Docker.app,等待启动
4. 菜单栏出现 Docker 图标 → 完成配置镜像加速
国内访问 Docker Hub 可能较慢,可配置镜像加速器。
注意:自 2024 年 6 月起,国内大量公共 Docker Hub 镜像加速器(Docker 中国、中科大、南京大学等)已陆续关停或转为私有,网上流传的免费加速地址大多已失效;更稳妥的方案是为 Docker daemon 配置代理或自建加速服务。
Docker Desktop Settings → Docker Engine → 编辑 registry-mirrors:
json
{
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://dockerproxy.com"
]
}2.2 Linux Docker 安装
Ubuntu(推荐)
bash
# 1. 卸载旧版本
sudo apt remove docker docker-engine docker.io containerd runc
# 2. 安装依赖
sudo apt update
sudo apt install -y ca-certificates curl gnupg lsb-release
# 3. 添加 Docker 官方 GPG 密钥
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# 4. 添加稳定版仓库
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 5. 安装 Docker Engine
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# 6. 启动 Docker
sudo systemctl enable docker
sudo systemctl start docker
# 7. 将当前用户加入 docker 组(避免每次 sudo)
sudo usermod -aG docker $USER
# 退出重新登录生效,或 newgrp dockerCentOS / RHEL
bash
# 1. 安装 yum-utils
sudo yum install -y yum-utils
# 2. 添加 Docker 仓库
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# 3. 安装
sudo yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# 4. 启动
sudo systemctl enable docker
sudo systemctl start docker验证 Linux Docker 安装
bash
# 查看 Docker 服务状态
sudo systemctl status docker
# 运行测试容器
docker run hello-world
# 查看版本信息
docker info # 包含容器数、镜像数、存储驱动等详细信息Docker Desktop vs Linux Docker 关键区别
markdown
Docker Desktop(Windows/Mac):
- 自带 Docker Compose、Kubernetes
- 包含图形界面(Dashboard)
- 自带容器镜像加速
- 使用虚拟机运行 Linux 容器
- 更适合开发和测试
Linux Docker:
- 仅 Docker Engine(轻量)
- 无图形界面
- 本地运行(无需虚拟化层)
- 性能更好(无虚拟化损耗)
- 更适合生产环境
注意:
- 无 Docker Engine → 无法运行 Docker
- 有 Docker Engine → 可以运行 Docker
- Docker Desktop ≠ Docker Engine(Desktop = Engine + GUI + 工具集)2.3 WSL2 集成(Windows 最佳实践)
在 Windows 上用 Docker 的最佳方式是 Docker Desktop + WSL2 后端。WSL2 让 Windows 上可以直接运行 Linux 内核,Docker 可以利用 WSL2 来运行 Linux 容器。
安装 WSL2
powershell
# PowerShell(管理员)中执行:
# 1. 启用 WSL
wsl --install
# 2. 设置 WSL2 为默认版本
wsl --set-default-version 2
# 3. 安装 Linux 发行版(推荐 Ubuntu)
wsl --install -d Ubuntu
# 4. 查看已安装的 WSL 发行版
wsl -l -v
# 5. 登录 WSL
wslDocker Desktop 使用 WSL2 后端
安装 Docker Desktop 时勾选 "Use WSL 2 based engine",或安装后在 Settings → Resources → WSL Integration 中启用。
markdown
启用后:
1. Docker 命令可以直接在 WSL 终端中使用
2. WSL 文件系统的性能远优于 Hyper-V 虚拟机
3. 可以在 WSL 中直接运行 docker 命令
推荐工作流:
- VS Code 连接 WSL(Remote - WSL 插件)
- 在 WSL 中操作 Docker
- 代码保存在 WSL 文件系统中(性能最好)bash
# 在 WSL Ubuntu 终端中:
docker ps
# 直接可用!不需要再安装 Docker
# 或者:只安装 Docker CLI(推荐在 WSL 中操作)
# 这样 WSL 中可以用 docker 命令,但实际 Docker Engine 运行在 Windows 上WSL2 vs 纯 Linux 虚拟机对比
| 特性 | WSL2 | 虚拟机(VirtualBox/VMware) |
|---|---|---|
| 启动速度 | 秒级 | 分钟级 |
| 内存占用 | 按需分配(~1GB) | 固定分配(~4GB) |
| 文件系统性能 | 跨 OS 文件操作慢 | 共享文件夹性能中等 |
| 集成度 | 高(可直接在 VS Code 中使用) | 低(需要 SSH) |
| GPU 支持 | 支持 CUDA | 需要特殊配置 |
| 桌面环境 | 不支持 | 支持 |
2.4 验证安装
bash
# 1. 版本信息
docker --version
# 2. 详细版本(确认 Client 和 Server 都正常)
docker version
# 3. Docker 系统信息
docker info
# 4. 运行测试容器
docker run hello-world
# 5. 列出镜像
docker images
# 6. 列出容器
docker ps -a
# 7. Docker Compose 版本
docker compose version版本策略速查(看懂 Docker 的版本号)
| 要点 | 说明 |
|---|---|
| 命名规则 | 自 18.03(2018)起改为 MAJOR.MINOR.PATCH 纯版本号,不再有 17.12→18.01 那种日期式版本 |
| 当前主线 | 截至 2026-08,Docker Engine 处于 29.x 系列(29.7.2,2026-08-05 发布,见 docs.docker.com/engine/release-notes) |
| 升级红线 | 29.0 起 daemon 要求客户端 API ≥ v1.44(Docker 25.0+),过老的 Client/SDK 连不上新 daemon;且 全新安装默认启用 containerd image store(老 overlay2 存储不变,已有安装不自动切换) |
| 升级建议 | 生产环境锁 minor 版本,升级前通读 release notes 的 breaking changes 一节(29.0 带 CAUTION 标注),先 docker version 比对 Client/Server API 版本差 |
⚠️ BuildKit 默认化的历史坑:Docker Engine 23.0 起 Linux 上
docker build默认走 BuildKit/Buildx(旧版需DOCKER_BUILDKIT=1,更早的 17.05-22.x 要手动开)。老文档里的DOCKER_BUILDKIT=0回退方式在新版本已不建议依赖;docker build是docker buildx build的别名,多平台构建(--platform linux/arm64)只能用 buildx 子命令。
第三篇:Docker 基础操作
3.1 镜像管理
拉取镜像
bash
# 从 Docker Hub 拉取
docker pull nginx:latest # 指定标签
docker pull nginx:1.25 # 指定版本
docker pull nginx # 不写标签默认为 latest
docker pull node:22-alpine # Alpine 版(更小,推荐)
# 从私有仓库拉取
docker pull registry.example.com/my-app:v1.0💡 比 tag 更可靠的定位方式——镜像摘要(digest):
docker pull nginx@sha256:3f8a...。tag 是可变的(今天1.25和明天可能是两个东西),digest 是内容哈希、永不变。生产部署和 7.1 的 CI 里做"精确回滚/精确复现"时应记录 digest(docker images --digests查看)。
查看镜像
bash
# 列出所有镜像
docker images
# 或
docker image ls
# 查看镜像详细信息
docker image inspect nginx:latest
# 按条件过滤
docker images --filter "dangling=true" # 虚悬镜像
docker images --filter "before=nginx:1.25"
docker images --filter "label=maintainer=team-a"构建镜像
bash
# 从 Dockerfile 构建
docker build -t my-app:v1.0 . # -t 指定名称:标签
docker build -t my-app:v1.0 -f Dockerfile.prod . # 指定 Dockerfile 文件名
# 从容器构建(不推荐,自愈性差)
docker commit container-name new-image:tag删除镜像
bash
# 删除特定镜像
docker rmi nginx:latest
# 删除虚悬镜像
docker image prune
# 删除所有未使用的镜像
docker image prune -a
# 强制删除
docker rmi -f nginx:latest镜像标签
bash
# 给镜像打标签
docker tag my-app:v1.0 myusername/my-app:v1.0
docker tag my-app:v1.0 myusername/my-app:latest
# 推送标签到仓库
docker push myusername/my-app:v1.0
docker push myusername/my-app:latest3.2 容器管理
启动容器
bash
# 创建并启动容器
docker run nginx # 前台运行(终端被占用)
docker run -d nginx # 后台运行(-d = detach)
docker run -d --name web-server nginx # 指定容器名
docker run -d -p 8080:80 nginx # 端口映射(主机:容器)
docker run -d -p 8080:80 -p 8443:443 nginx # 多个端口
# 挂载卷
docker run -d -v /host/data:/container/data nginx # 绑定挂载
docker run -d -v my-volume:/data nginx # 卷挂载
# 设置资源限制
docker run -d --memory="512m" --cpus="1.5" nginx
# 设置环境变量
docker run -d -e DB_HOST=localhost -e DB_PORT=3306 my-app
# 进入交互模式
docker run -it ubuntu /bin/bash # -it = 交互式终端查看容器
bash
# 运行中容器
docker ps
# 所有容器(包括已停止的)
docker ps -a
# 最近创建的容器
docker ps -l
# 按条件过滤
docker ps --filter "status=exited"
docker ps --filter "name=web"
docker ps --filter "ancestor=nginx" # 从某镜像创建的容器生命周期管理
bash
# 停止容器
docker stop container-name
docker stop container-id
# 启动已停止的容器
docker start container-name
# 重启容器
docker restart container-name
# 暂停/恢复
docker pause container-name # 暂停进程
docker unpause container-name # 恢复进程
# 删除容器
docker rm container-name # 删除已停止的容器
docker rm -f container-name # 强制删除运行中的容器
docker container prune # 删除所有已停止的容器
# 自动重启策略(docker run --restart)
docker run -d --restart=unless-stopped my-app
# no(默认):不重启
# always:退出就重启;Docker 守护进程重启后,曾被 stop 的容器也会重新拉起
# unless-stopped:同 always,但若停止是人为 docker stop 所致,守护进程重启后不再拉起
# on-failure[:max-retries]:仅退出码非 0 时重启内部机制:容器的"生死"由谁控制?
- PID 1 问题:容器内主进程是 PID 1,享有特殊语义——默认不响应没注册 handler 的信号。
docker stop先发SIGTERM(默认等 10s,-t可调),超时再发SIGKILL。如果镜像用 Shell 形式启动(CMD node app.js→ PID 1 是/bin/sh),SIGTERM 根本传不到 node,等于每次都被强杀。这就是 4.1 强调 Exec 形式的原因;语言侧无信号处理时,可用docker run --init(或 Compose 的init: true)注入 tini 作为 PID 1 转发信号。 - 数据在可写层:容器删除后可写层即销毁(1.3 的 copy-on-write),任何要留下的数据必须落在 Volume/Bind Mount(见 3.3)。
- restart 策略 ≠ 健康检查:
--restart=always只管"进程退出后拉起",进程活着但假死(如数据库连接池耗尽卡住)不会触发重启——那需要 HEALTHCHECK(见 4.3),unhealthy 状态可配合 autoheal 类工具或编排系统处理。
进入容器
bash
# 进入运行中的容器(推荐)
docker exec -it container-name /bin/sh # Alpine 等最小镜像用 sh
docker exec -it container-name /bin/bash # Ubuntu 等用 bash
# 进入并执行命令
docker exec container-name cat /etc/hosts
docker exec container-name ls -la /app
# 附着到容器(一般用于调试)
docker attach container-name日志与监控
bash
# 查看容器日志
docker logs container-name # 查看日志
docker logs -f container-name # 实时跟踪(--follow)
docker logs --tail 100 container-name # 只看最后 100 行
docker logs -t container-name # 显示时间戳
# 查看容器进程
docker top container-name
# 查看容器资源使用
docker stats # 所有容器实时监控
docker stats container-name # 特定容器
# 查看容器详细信息
docker inspect container-name
# 查看容器的端口映射
docker port container-name文件复制
bash
# 从主机复制到容器
docker cp /host/file.txt container-name:/app/
# 从容器复制到主机
docker cp container-name:/app/data.json ./data.json3.3 数据卷(Volume)
容器是短暂且无状态的——容器删除后,内部数据会丢失。数据卷提供了持久化存储方案。
三种挂载方式
bash
# 1. 卷(Volume)- 由 Docker 管理,推荐
docker volume create my-volume # 创建卷
docker run -v my-volume:/app/data my-app # 使用卷
docker volume ls # 查看卷列表
docker volume inspect my-volume # 查看卷详情
docker volume prune # 删除未使用的卷
# 2. 绑定挂载(Bind Mount)- 使用主机路径
docker run -v /absolute/host/path:/app/data my-app
# 3. tmpfs 挂载 - 临时内存存储
docker run --tmpfs /app/temp my-app三种方式的区别
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ │ Volume │ Bind Mount │ tmpfs │
├──────────────┼──────────────┼──────────────┼──────────────┤
│ 存储位置 │ Docker管理 │ 主机任意路径 │ 内存 │
│ │ /var/lib/ │ │ │
│ │ docker/vols │ │ │
├──────────────┼──────────────┼──────────────┼──────────────┤
│ 生命周期 │ Docker管理 │ 手动管理 │ 容器停止清空 │
├──────────────┼──────────────┼──────────────┼──────────────┤
│ 持久化 │ 是 │ 是 │ 否 │
├──────────────┼──────────────┼──────────────┼──────────────┤
│ 共享 │ 多个容器 │ 多个容器 │ 不可共享 │
├──────────────┼──────────────┼──────────────┼──────────────┤
│ 适用场景 │ 数据库数据 │ 开发源码 │ 临时缓存 │
│ │ 配置文件 │ 配置文件 │ session │
│ │ 持久化数据 │ 热重载调试 │ │
└──────────────┴──────────────┴──────────────┴──────────────┘卷的内部机制与几个高频坑
- 卷数据存在哪:命名卷默认在
/var/lib/docker/volumes/<卷名>/_data(Linux);docker volume inspect my-volume的Mountpoint字段就是主机真实路径。卷由 Docker 全权管理,不要在主机上直接改 Mountpoint 里的文件。 - 首次挂载的空卷会把镜像内容"灌"进卷:如果命名卷是空的,把卷挂到容器内一个已有文件的目录(如
/var/lib/mysql)时,Docker 会先把镜像里该目录的内容复制到卷中;Bind Mount 则直接遮盖主机目录,没有这个行为。 - ⚠️ 匿名卷陷阱:
docker run -v /container/path(只写容器路径不写主机路径)会创建一个匿名卷,docker run频繁测试后卷会越积越多,docker volume ls -f dangling=true检查、docker volume prune清理。 - ⚠️ Windows/macOS 跨文件系统性能:Bind Mount 主机目录(尤其 Windows 盘挂载进 WSL2 容器)经过 9P/virtiofs 协议,IO 比命名卷慢一个数量级。热重载开发时把代码放在 WSL2 文件系统内或用命名卷,别用
-v C:\code:/app直挂。 - 💡 匿名卷的配套语法
-v /app/node_modules(5.2 开发配置里出现过):主机源码挂载到/app会遮盖镜像里构建好的node_modules,再声明一个匿名卷挂到同路径,容器内就用镜像里的依赖、主机目录不受污染——这是 Node 容器热重载的标准写法。
3.4 网络管理
Docker 提供多种网络驱动,让容器之间可以互相通信,也可以与外部通信。
网络驱动类型
bash
# 网络驱动对比
# bridge(默认):单机容器间通信,有 NAT
# host:容器直接使用主机网络,无隔离
# none:无网络
# overlay:跨主机通信(Swarm 集群)
# 查看网络
docker network ls
# 创建网络
docker network create my-network
# 查看网络详情
docker network inspect my-networkbridge 网络(最常用)
bash
# 创建自定义 bridge 网络(推荐用自定义而不是默认的 bridge)
docker network create app-network
# 在自定义网络中启动容器
docker run -d --name api --network app-network my-api
docker run -d --name db --network app-network mysql:8.0
# 容器间通过容器名通信(DNS 自动解析)
# api 容器内能通过 db:3306 访问 MySQL,不需要 IP 地址
# 将现有容器加入网络
docker network connect app-network api
# 断开网络
docker network disconnect app-network api端口映射
bash
# 将容器端口映射到主机端口
docker run -p 8080:80 nginx # 主机 8080 -> 容器 80
docker run -p 127.0.0.1:8080:80 nginx # 只监听本地(安全)
docker run -p 8080:80 -p 8443:443 nginx # 多个端口映射
docker run -P nginx # 随机端口映射(不推荐)网络内部机制:DNS、iptables 与常见坑
- 自定义 bridge 才有容器名 DNS:默认
bridge网络里的容器只能用 IP 互访(--link是废弃机制);自定义 bridge 网络内置 DNS(127.0.0.11),容器名和服务名自动解析。这是 5.2 里DB_HOST=postgres能直接用的原因。 - 端口映射的底层是 iptables/nftables NAT:
-p 8080:80实际在主机 netfilter 里加了 DNAT 规则。⚠️ 推论:主机上的防火墙规则(UFW/firewalld)管不住已映射的 Docker 端口——Docker 直接改 iptables,ufw deny 8080可能照样放行。想限制访问,用127.0.0.1:8080:8080绑定回环地址或不发布端口。 - 容器间通信不经过 -p:同一自定义网络内的容器互访走 bridge 内部转发,与端口映射无关——
-p只服务于"主机/外部 → 容器"方向。所以 5.2 的 postgres 开发时映射 5432 是"方便本机连",生产 compose 里数据库可以不写 ports。 - ⚠️ 改网络配置要重启:
docker network connect/disconnect即时生效,但修改 compose 网络定义(subnet、driver 参数)需docker compose down && up -d,up -d不会重连已运行容器的网络。 - 💡 host 网络(
--network host)跳过 NAT 与虚拟网卡,性能最好但零隔离、端口直接占用主机端口,且 Docker Desktop(Windows/Mac)上不适用——容器本来就跑在虚拟机里,host 模式只是 VM 的 host。
第四篇:Dockerfile 编写指南
4.1 Dockerfile 基础指令
总览
dockerfile
# Dockerfile 是从上到下逐条执行的构建脚本
# 每条指令创建一个镜像层(Layer)
# ===== 基础设置 =====
FROM node:22-alpine # 基础镜像(必须第一行)
# ===== 元数据 =====
LABEL maintainer="team@example.com"
LABEL version="1.0"
# ===== 工作目录 =====
WORKDIR /app # 设置工作目录(相当于 cd /app)
# ===== 文件操作 =====
COPY package*.json ./ # 复制文件到镜像
ADD source.tar.gz ./ # 自动解压压缩包
# ===== 运行命令 =====
RUN npm install # 在构建时执行命令(创建新层)
# ===== 环境变量 =====
ENV NODE_ENV=production
ENV PORT=3000
# ===== 暴露端口(仅声明,不实际映射) =====
EXPOSE 3000
# ===== 启动命令 =====
CMD ["node", "app.js"] # 容器启动时的默认命令(可以被覆盖)
ENTRYPOINT ["node"] # 固定入口(不可覆盖,但可接收参数)每条指令的详细说明
FROM:指定基础镜像
dockerfile
# 常用基础镜像
FROM node:22-alpine # Node.js 最小镜像(~120MB)
FROM node:22-slim # Node.js 精简版(~180MB)
FROM node:22 # Node.js 完整版(~900MB)
FROM ubuntu:22.04 # Ubuntu 系统
FROM alpine:3.18 # Alpine Linux(~5MB,极简)
FROM python:3.11-slim # Python
FROM nginx:alpine # Nginx
# 多阶段构建
FROM node:22-alpine AS builder
FROM nginx:alpine AS runnerRUN:构建时执行命令(重要:每个 RUN 创建一个新层)
dockerfile
# ✅ 好写法:一条 RUN 做完所有操作(减少层数)
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
git \
vim && \
rm -rf /var/lib/apt/lists/*
# ❌ 坏写法:每个操作一个 RUN(层数多,镜像大)
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN apt-get install -y vimCOPY vs ADD:
dockerfile
# COPY:只复制,不解压(推荐)
COPY package.json .
COPY . .
# ADD:可解压压缩包 + 支持 URL(有安全隐患,除非必要不用)
ADD source.tar.gz /app/
ADD https://example.com/file.tar.gz /app/ # 不推荐CMD vs ENTRYPOINT:
dockerfile
# CMD:可以被 docker run 命令覆盖
CMD ["node", "app.js"]
# docker run my-image → node app.js
# docker run my-image ls → ls(CMD 被覆盖)
# ENTRYPOINT:固定入口
ENTRYPOINT ["node"]
CMD ["app.js"]
# docker run my-image → node app.js
# docker run my-image server.js → node server.js(CMD 被覆盖,ENTRYPOINT 不变)
# Shell 形式 vs Exec 形式
CMD node app.js # Shell 形式:/bin/sh -c "node app.js"(支持变量展开)
CMD ["node", "app.js"] # Exec 形式:直接运行(推荐,信号传递正常)构建上下文(build context)机制:执行 docker build . 时,Docker 先把当前目录整个发给 daemon,之后 Dockerfile 里的 COPY 只引用这份上下文副本——所以:
- 被
.dockerignore排除的文件即使COPY . .也不会进镜像,且不改变上下文指纹(改 README 不会让 COPY 层缓存失效,前提是被 ignore 了)。 COPY引用不了上下文之外的路径(COPY ../other /app非法),多项目共享文件时用docker build -f ... <公共根目录>调整上下文根。- 上下文过大(忘记排除
node_modules、.git)会让每次 build 先卡几十秒传输——这是"build 慢在第一步"的最常见原因。
完整 Node.js Dockerfile 示例
dockerfile
# 生产环境多阶段构建
# ============ 阶段1:构建应用 ============
FROM node:22-alpine AS builder
WORKDIR /app
# 先复制依赖文件(利用 Docker 缓存层)
COPY package.json package-lock.json ./
RUN npm ci # 完整安装(构建阶段需要 devDependencies)
# 再复制源码(源码变化时不会破坏上面的缓存)
COPY . .
RUN npm run build
# ============ 阶段2:运行阶段 ============
FROM node:22-alpine AS runner
WORKDIR /app
# 从 builder 阶段复制构建产物
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
# 非 root 用户运行(安全)
USER node
CMD ["node", "dist/main.js"]4.2 多阶段构建
为什么需要多阶段构建?
dockerfile
# ❌ 单阶段构建的问题:
# 开发工具(TypeScript、Git)包含在最终镜像中 → 镜像臃肿
# 需要手动清理构建依赖 → 容易遗漏
FROM node:22
WORKDIR /app
COPY . .
RUN npm install # 包含 devDependencies
RUN npm run build # 需要 TypeScript
RUN npm prune --omit=dev # 手动清理 devDeps
# 最终镜像 ≈ 1.2GB
# ✅ 多阶段构建:
# 阶段1:安装依赖、编译
# 阶段2:只复制编译产物和 runtime 依赖
# 最终镜像只包含运行所需的最小文件
# 最终镜像 ≈ 150MB(减少 85%+)多阶段构建实战
dockerfile
# ===== 阶段1:依赖安装 =====
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# ===== 阶段2:构建 =====
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# ===== 阶段3:生产运行 =====
FROM node:22-alpine AS runner
WORKDIR /app
# 复制构建产物和依赖
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
USER node
CMD ["node", "dist/main.js"]dockerfile
# 多阶段也可以用于不同环境
# dev 阶段包含调试工具和热重载
# prod 阶段只包含运行文件
FROM node:22-alpine AS base
WORKDIR /app
COPY package.json package-lock.json ./
FROM base AS dev
RUN npm ci
COPY . .
CMD ["npm", "run", "dev"]
FROM base AS build
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine AS prod
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
CMD ["node", "dist/main.js"]
# docker build --target dev -t my-app:dev .
# docker build --target prod -t my-app:prod .4.3 Dockerfile 最佳实践
dockerfile
# 1. 使用 .dockerignore 排除不必要文件
# .dockerignore 内容:
# node_modules
# .git
# .env
# *.md
# Dockerfile
# .dockerignore
# 2. 优先使用官方 Alpine 镜像(更小、更安全)
FROM node:22-alpine # ✅
FROM node:22 # ❌(大一倍)
# 3. 合并 RUN 指令减少层数
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/* # ✅ 清理缓存
# 每个 RUN、COPY、ADD 都创建一个层
# 4. 利用构建缓存(把变化少的指令放前面)
COPY package.json ./ # ✅ 先复制依赖文件
RUN npm install # ✅ 只有 package.json 变化时才重新安装
COPY . . # ✅ 源码变化不会影响上面缓存的层
# 5. 使用非 root 用户
RUN addgroup -g 1001 appuser && \
adduser -u 1001 -G appuser -s /bin/sh -D appuser
USER appuser
# 6. 使用健康检查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# 7. 使用精确的基础镜像版本
FROM node:22-alpine # ✅ 锁定大版本,避免意外升级
FROM node:alpine # ❌ latest 标签,可能意外更新
# 8. 不要安装不必要的包
RUN npm install --omit=dev # ✅ 只安装运行时依赖BuildKit 进阶加速(现代 Docker 的隐藏福利)
自 Docker 23.0 起默认构建器就是 BuildKit(见 2.4),它提供一批经典构建器没有的能力,官方文档在 docs.docker.com/build/:
1. 缓存挂载(cache mount)——包管理器缓存跨构建复用,且不进镜像层:
dockerfile
# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/root/.npm \
npm ci --no-audit --no-fund
# apt 版:RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
# apt-get install -y curl普通 RUN npm ci 每次依赖清单变化都要重新从 registry 下载 tarball;cache mount 让下载缓存常驻 BuildKit,重建时只安装不解包网络。⚠️ 注意 sharing=locked 可避免并行构建争抢同一缓存目录。
2. 构建缓存导出到远端——CI 里最实用,解决"每次 runner 都是新机器、缓存全丢":
bash
docker buildx build \
--cache-from type=registry,ref=my-org/my-app:buildcache \
--cache-to type=registry,ref=my-org/my-app:buildcache,mode=max \
-t my-app:1.2.3 .3. 查看/清理构建缓存:docker buildx du(看占用)、docker buildx prune --keep-storage 10GB(限额清理)。BuildKit 缓存默认也会自动 GC,但上限不小——CI 机磁盘被 build cache 撑爆是常见事故。
4. Secrets 挂载——构建时注入密钥且不留层(详见 8.1):
dockerfile
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci第五篇:Docker Compose
5.1 Compose 文件结构
Docker Compose 用于定义和运行多容器 Docker 应用。用一个 docker-compose.yml 文件描述所有服务。
docker-compose.yml 基础结构
yaml
version: "3.8" # Compose 文件格式版本
services: # 定义应用的服务
web: # 服务名(同时也是容器 DNS 名)
build: . # 从 Dockerfile 构建
image: my-app:latest # 或者直接使用已存在的镜像
ports:
- "3000:3000"
environment:
- NODE_ENV=production
depends_on:
- db
db:
image: mysql:8.0
volumes:
- db-data:/var/lib/mysql
volumes: # 定义卷
db-data:
networks: # 定义网络
app-network:Compose 文件版本
yaml
# Docker Compose v2(docker compose,推荐)
# 不再需要 version 字段(已废弃)
services:
web:
image: nginx
# docker compose up 即可
# Docker Compose v1(docker-compose,旧版)
version: "3.8"
services:
web:
image: nginx
# docker-compose up(有连字符)depends_on 的真相:只控制"启动顺序",不控制"就绪顺序"
初学者最常见的误解:depends_on: - db 之后后端启动时数据库就一定能连上。不是——短语法只保证 db 容器先创建并启动,Postgres 镜像的初始化(建库、跑 initdb)往往还要几秒到几十秒,后端一连就是 connection refused。
Compose 规范给出三种 condition(长语法):
| condition | 等待条件 | 典型用途 |
|---|---|---|
service_started | 依赖容器已启动(等价短语法) | 只要顺序,应用自己会重试连接 |
service_healthy | 依赖容器的 healthcheck 通过 | 数据库/缓存等重型依赖(5.2 的 postgres 用法) |
service_completed_successfully | 依赖容器运行到退出码 0 | 一次性任务:migration、seed 数据 |
yaml
services:
migrate:
build: .
command: npm run migrate # 跑完就退出
backend:
build: .
depends_on:
migrate:
condition: service_completed_successfully # 等迁移成功完成
db:
condition: service_healthy # 等数据库健康⚠️
service_healthy依赖被等待方定义了 healthcheck,否则 Compose 报配置错误;且 healthcheck 必须"最终能变 healthy",写错的探测命令会让up永远卡在 waiting。应用侧仍建议保留连接重试逻辑——condition 只覆盖compose up时刻,运行期数据库重启不保证重新排序。
5.2 常用配置示例
完整的 Web 应用
yaml
services:
# ==== 前端服务 ====
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "80:80"
depends_on:
- backend
networks:
- app-network
# ==== 后端 API ====
backend:
build:
context: ./backend
target: runner # 多阶段构建指定阶段
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DB_HOST=postgres
- DB_PORT=5432
- REDIS_HOST=redis
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
volumes:
- ./backend:/app # 开发时挂载源码实现热重载
- /app/node_modules # 排除 node_modules
networks:
- app-network
# ==== 数据库 ====
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD: ${DB_PASSWORD} # 从 .env 文件读取
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
# ==== 缓存 ====
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
networks:
- app-network
# ==== 消息队列 ====
rabbitmq:
image: rabbitmq:3-management-alpine
ports:
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: admin
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
volumes:
- rabbitmq-data:/var/lib/rabbitmq
volumes:
postgres-data:
redis-data:
rabbitmq-data:
networks:
app-network:
driver: bridge.env 文件
bash
# .env(与 docker-compose.yml 同目录)
DB_PASSWORD=mysecretpassword
RABBITMQ_PASSWORD=anothersecret
API_KEY=dev-key-123开发环境 vs 生产环境
yaml
# docker-compose.yml(基础配置)
services:
app:
build: .
ports:
- "3000:3000"
# docker-compose.override.yml(开发环境,自动覆盖)
# docker compose up 默认会加载 override 文件
services:
app:
build:
context: .
target: dev
volumes:
- .:/app
environment:
- NODE_ENV=development
# docker-compose.prod.yml(生产环境)
# docker compose -f docker-compose.yml -f docker-compose.prod.yml up
services:
app:
build:
context: .
target: runner
environment:
- NODE_ENV=production
restart: always5.3 Compose 实战:全栈项目
项目结构
my-fullstack-app/
├── frontend/
│ ├── Dockerfile
│ ├── package.json
│ └── src/
├── backend/
│ ├── Dockerfile
│ ├── package.json
│ └── src/
├── nginx/
│ └── nginx.conf
├── docker-compose.yml
├── docker-compose.prod.yml
└── .env开发环境 Compose
yaml
# docker-compose.yml(开发环境)
services:
# 前端(Vite 开发服务器,热重载)
frontend:
build:
context: ./frontend
target: dev
ports:
- "5173:5173"
volumes:
- ./frontend:/app
- /app/node_modules
depends_on:
- backend
# 后端 API
backend:
build:
context: ./backend
target: dev
ports:
- "3000:3000"
volumes:
- ./backend:/app
- /app/node_modules
environment:
- DATABASE_URL=postgresql://myapp:${DB_PASSWORD}@db:5432/myapp
- REDIS_URL=redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
# 数据库
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD: ${DB_PASSWORD}
ports:
- "5432:5432" # 方便本地连接
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp"]
interval: 5s
timeout: 5s
retries: 5
# 缓存
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
pgdata:开发时常用 Compose 命令
bash
# 启动所有服务(前台)
docker compose up
# 后台启动
docker compose up -d
# 查看日志
docker compose logs -f
# 查看特定服务日志
docker compose logs -f backend
# 重新构建并启动
docker compose up --build
# 停止所有服务
docker compose down
# 停止并删除卷(数据也会删!)
docker compose down -v
# 查看运行中的服务
docker compose ps
# 进入某个服务容器
docker compose exec backend /bin/sh
# 执行一次性命令
docker compose run --rm backend npm test第六篇:项目开发到部署全流程
6.1 本地开发环境搭建
使用 Docker 搭建开发环境的最大优势是:新成员加入 5 分钟即可开始编码。
开发工作流
bash
# 1. 克隆代码
git clone git@github.com:team/my-project.git
cd my-project
# 2. 首次启动(构建并启动所有服务)
docker compose up -d # 后台启动
# 3. 启动时自动执行:
# - 拉取/构建镜像
# - 创建网络
# - 创建卷
# - 按依赖顺序启动服务
# 4. 查看服务是否启动成功
docker compose ps
# 5. 查看日志确认
docker compose logs -f backend
# 6. 数据库迁移
docker compose exec backend npm run migrate
# 7. 开始开发!
# 修改源码 → 自动热重载(开发模式挂载了卷)
# 8. 测试
docker compose exec backend npm test
# 9. 停止
docker compose down开发环境的关键配置
yaml
# docker-compose.override.yml(开发环境自动加载)
services:
backend:
# 挂载源码实现热重载
volumes:
- ./backend:/app
- /app/node_modules
# 开发模式端口暴露
ports:
- "9229:9229" # Node.js 调试端口
# 开发环境变量
environment:
- NODE_ENV=development
- DEBUG=true6.2 生产环境镜像构建
生产构建策略
bash
# 版本号策略:
# - latest:当前最新版本
# - 语义版本:1.2.3
# - Git SHA:git-abc1234(可追溯)
# - 时间戳:20240101-1200
# 推荐使用双重标签
docker build -t my-app:1.2.3 -t my-app:latest .
docker push my-app:1.2.3
docker push my-app:latestNode.js 生产镜像优化
dockerfile
# Dockerfile(生产,多阶段构建)
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# 只复制生产需要的文件
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./
USER nodejs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/main.js"]镜像体积优化对比
bash
# 不同基础镜像大小
node:22 # ~950MB
node:22-slim # ~180MB ✅ 常用
node:22-alpine # ~120MB ✅ 推荐
# 多阶段构建效果
# 单阶段(包含构建工具):~1.2GB
# 多阶段(只包含运行文件):~150MB
# 多阶段 + Alpine + 压缩:~80MB
# 查看镜像层级
docker history my-app:latest6.3 镜像仓库与分发
Docker Hub(公共仓库)
bash
# 登录
docker login
# 打标签并推送
docker tag my-app:latest username/my-app:1.0.0
docker push username/my-app:1.0.0
# 拉取
docker pull username/my-app:1.0.0GitHub Container Registry(GHCR)
bash
# 登录(需要 GitHub Token)
echo $GITHUB_TOKEN | docker login ghcr.io -u $USERNAME --password-stdin
# 打标签
docker tag my-app:latest ghcr.io/my-org/my-app:1.0.0
# 推送
docker push ghcr.io/my-org/my-app:1.0.0私有仓库(Harbor)
yaml
# docker-compose.yml 搭建 Harbor(企业级镜像仓库)
services:
harbor:
image: goharbor/harbor-core:v2.8.0
# Harbor 安装需要 docker-compose,此处仅示意镜像清理策略
bash
# 本地清理
docker image prune -a --filter "until=24h" # 清理 24 小时前的镜像
# 定期删除
# crontab 定时任务
0 3 * * * docker image prune -a -f --filter "until=168h" # 每周清理多架构镜像(x86 与 ARM 一份 tag 通吃)
Apple Silicon、树莓派、AWS Graviton 都是 arm64;传统服务器是 amd64。用 buildx 一次构建、一次推送,仓库里以 manifest list(镜像索引) 把同一个 tag 指向多架构变体,客户端 docker pull 时按本机平台自动选层:
bash
docker buildx create --use # 首次:建一个支持多平台的 builder
docker buildx build --platform linux/amd64,linux/arm64 \
-t ghcr.io/my-org/my-app:1.0.0 --push .⚠️ 多平台构建时
--load(导入本地 docker)只能单平台,多平台必须--push或用--output type=oci;QEMU 模拟跨架构编译很慢,重项目考虑原生 runner 或 remote builder。docker buildx ls查看当前 builder 支持的平台。
6.4 服务器部署
生产环境 Compose 配置
yaml
# docker-compose.prod.yml
services:
app:
build:
context: .
dockerfile: Dockerfile
image: my-app:latest
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
- REDIS_URL=${REDIS_URL}
restart: always # 自动重启
deploy:
resources:
limits:
memory: 512M
cpus: "0.5"
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
- certbot-data:/var/www/certbot
depends_on:
- app
restart: always
volumes:
certbot-data:部署流程
bash
# 手动部署(简单项目)
# 1. 本地构建并推送
docker build -t my-app:1.0.0 .
docker tag my-app:1.0.0 registry.example.com/my-app:1.0.0
docker push registry.example.com/my-app:1.0.0
# 2. SSH 到服务器
ssh user@your-server.com
# 3. 拉取新镜像
docker pull registry.example.com/my-app:1.0.0
# 4. 启动新版本
docker compose -f docker-compose.prod.yml up -d
# 零停机部署(滚动更新)
# docker compose up -d 会自动替换容器,但需要负载均衡Nginx 反向代理配置
nginx
# nginx.conf
upstream app {
server app:3000;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ws {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}第七篇:CI/CD 集成
7.1 GitHub Actions + Docker
完整 CI/CD 流水线
yaml
# .github/workflows/deploy.yml
name: Build, Test, and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# ===== 1. 测试 =====
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_DB: test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: "npm"
- run: npm ci
- run: npm run lint
- run: npm run test -- --coverage
# ===== 2. 构建并推送镜像 =====
build-and-push:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=sha,format=long
type=ref,event=branch
type=raw,value=latest
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ===== 3. 部署到服务器 =====
deploy:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Deploy to production
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
script: |
cd /app
docker compose pull
docker compose up -d --remove-orphans
docker system prune -af --filter "until=24h"这条流水线里值得深挖的三个机制
secrets.GITHUB_TOKEN与permissions:每个 job 自动获得一个短期 JWT。默认权限是只读的,所以推 GHCR 必须显式声明packages: write——这是最小权限原则在 CI 里的落地,比配置长期 PAT 安全得多(GHCR 匿名拉取公开镜像不需要登录)。cache-from/cache-to: type=gha:把 BuildKit 层缓存存进 GitHub Actions 的缓存服务(每个仓库有 10GB 配额,LRU 逐出),下个 job 构建时恢复。mode=max会缓存所有中间阶段而不只是最终镜像层——多阶段构建的 deps/builder 阶段产物也能命中,通常能把重建时间从分钟级压到十几秒。--remove-orphans:docker compose up -d只处理当前 compose 文件里声明的服务;如果上一版 compose 里有个worker服务这次被删了,它会作为"孤儿容器"继续运行占资源。带-d的 up 加上--remove-orphans才会把它停掉——做"配置即部署"时这是个容易被忽略的一致性缺口。
⚠️
docker compose pull+up -d的组合依赖镜像 tag 不变(如:latest)。如果按 6.2 的双重标签策略部署不可变 tag,服务器上的 compose 文件里 image 字段需要同步更新(常见做法:CI 用sed/compose -f环境变量注入新版本号),否则 pull 了新版但 up 起的还是旧 tag 容器。
7.2 GitLab CI + Docker
yaml
# .gitlab-ci.yml
stages:
- test
- build
- deploy
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
services:
- docker:dind # Docker-in-Docker
# ===== 测试 =====
test:
stage: test
image: node:22-alpine
services:
- postgres:15-alpine
- redis:7-alpine
variables:
DATABASE_URL: postgresql://test:test@postgres:5432/test
REDIS_URL: redis://redis:6379
script:
- npm ci
- npm run lint
- npm run test
# ===== 构建 =====
build:
stage: build
image: docker:24
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $IMAGE_TAG .
- docker push $IMAGE_TAG
# ===== 部署 =====
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
script:
- ssh $DEPLOY_USER@$DEPLOY_HOST "
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY &&
docker pull $IMAGE_TAG &&
docker tag $IMAGE_TAG $CI_REGISTRY_IMAGE:latest &&
docker compose up -d
"
only:
- main7.3 自动化部署流水线
完整的部署策略
markdown
1. 开发流程
开发者 push 到 feature 分支
→ GitHub Actions 运行测试
→ 测试通过后,创建 Pull Request
→ Code Review 通过后,合并到 main
2. 持续集成(CI)
合并到 main 后自动触发:
→ 运行完整测试套件(单元测试 + 集成测试 + E2E)
→ 构建 Docker 镜像
→ 推送镜像到 GHCR / Harbor
→ 打版本标签(git tag)
3. 持续部署(CD)
推送新镜像后:
→ 通过 SSH 连接到生产服务器
→ 拉取最新镜像
→ 使用 docker compose 启动新容器
→ 健康检查(Healthcheck)
→ 如果检查失败,自动回滚到上一个版本
4. 蓝绿部署(可选)
同时保留两个环境:
- blue(当前生产)
- green(新版本)
切换负载均衡指向 green
验证通过后,移除 blue
green 变为新的 blueDocker 部署脚本
bash
#!/bin/bash
# deploy.sh - 服务器部署脚本
set -e
APP_NAME="my-app"
IMAGE_NAME="ghcr.io/my-org/$APP_NAME:latest"
COMPOSE_FILE="docker-compose.prod.yml"
SERVICE="app" # Compose 文件中的服务名
echo "=== 开始部署 $APP_NAME ==="
# 1. 备份当前运行版本(在拉取新镜像之前)
echo "备份当前版本..."
docker tag $IMAGE_NAME ${IMAGE_NAME}:backup || true
# 2. 拉取最新镜像
echo "拉取镜像..."
docker pull $IMAGE_NAME
# 3. 启动新版本
echo "启动新版本..."
docker compose -f $COMPOSE_FILE up -d --no-deps $SERVICE
# 4. 健康检查(Compose v2 容器名为 <项目名>-<服务名>-<编号>,用 ps -q 获取容器 ID)
echo "健康检查..."
CONTAINER=$(docker compose -f $COMPOSE_FILE ps -q $SERVICE)
for i in {1..30}; do
STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$CONTAINER" 2>/dev/null || echo "starting")
if [ "$STATUS" = "healthy" ]; then
echo "健康检查通过!"
break
fi
echo "等待服务启动... ($i/30)"
sleep 2
done
if [ "$STATUS" != "healthy" ]; then
echo "健康检查失败!回滚到上一个版本..."
docker tag ${IMAGE_NAME}:backup $IMAGE_NAME
docker compose -f $COMPOSE_FILE up -d --no-deps $SERVICE
exit 1
fi
# 5. 清理旧镜像
echo "清理旧镜像..."
docker image prune -af --filter "until=168h"
echo "=== 部署完成 ==="第八篇:进阶话题
8.1 Docker 安全最佳实践
dockerfile
# 1. 使用非 root 用户
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001 -G nodejs
USER nodejs
# 2. 使用只读文件系统
# docker run --read-only --tmpfs /tmp my-app
# 镜像文件系统只读,写入需要 tmpfs 或 volume
# 3. 限制容器资源
# docker run --memory="256m" --cpus="0.5" --pids-limit=100 my-app
# 4. 不要泄露 secrets
# ❌ 不要写在 Dockerfile 中
ENV DB_PASSWORD=mysecret # bad
# ✅ 使用 --env-file 或 secrets
# docker run --env-file .env my-app补充:构建期密钥与运行期降权
dockerfile
# ❌ 经典反模式:构建参数会留在 docker history 里
ARG API_TOKEN && npm config set //registry.npmjs.org/:_authToken=$API_TOKEN
# ✅ BuildKit secret 挂载:文件出现在 /run/secrets/<id>,只在 RUN 期间存在,不落层
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci
# 用法:docker build --secret id=npm_token,src=$HOME/.npmrc -t my-app .bash
# 运行期最小权限(docker run,官方 runtime privilege 文档)
docker run --security-opt no-new-privileges=true \ # 禁止程序通过 setuid 提权
--cap-drop=ALL --cap-add=NET_BIND_SERVICE \ # 丢掉全部 Linux capabilities,只留绑定 80 端口
--read-only --tmpfs /tmp \
my-app⚠️
--privileged会把主机所有设备与 capability 交给容器,等价于放弃隔离,只在调试硬件/内核类场景临时使用,绝不能进生产 Compose。 💡 镜像漏洞扫描:docker scout cves my-app:latest(Docker Desktop 内置、Engine 可作为 CLI 插件安装)可直接列出镜像里的 CVE 及修复版本,接入 7.1 的 CI 做质量门禁;或离线场景用 Trivy 等工具。
8.2 容器编排(Kubernetes 简介)
Kubernetes(K8s)用于管理大规模容器集群。当你需要管理数十个以上容器时,Docker Compose 已不够用。
yaml
# Kubernetes Deployment(与 Compose 的对比)
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3 # 3 个副本
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: my-app:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
resources:
limits:
memory: "256Mi"
cpu: "500m"
---
# Service(暴露服务)
apiVersion: v1
kind: Service
metadata:
name: my-app-service
spec:
type: LoadBalancer
selector:
app: my-app
ports:
- port: 80
targetPort: 3000Docker Compose vs Kubernetes
| 特性 | Docker Compose | Kubernetes |
|---|---|---|
| 适用场景 | 单机、开发测试 | 大规模集群 |
| 容器编排 | 简单(单机) | 复杂(集群调度) |
| 高可用 | 手动管理 | 自动(负载均衡、自愈) |
| 滚动更新 | 手动 | 自动滚/回滚 |
| 学习成本 | 低 | 高 |
8.3 监控与日志
bash
# 日志管理
docker logs --tail 100 -f container-name
# 集中日志:ELK Stack(Elasticsearch + Logstash + Kibana)
# 或使用 Docker 的 logging driver 直接发送
docker run --log-driver=awslogs --log-opt awslogs-group=my-app my-image
# 监控容器指标
docker stats
# 持续监控 → Prometheus + Grafana8.4 性能调优
markdown
1. 镜像优化
- 使用 Alpine 基础镜像
- 多阶段构建
- 合并 RUN 指令
2. 容器优化
- 设置合理的内存和 CPU 限制
- 使用 --restart=always 自动重启
3. 缓存优化
- 利用 Docker 构建缓存(合理安排 COPY 顺序)
- 使用 BuildKit 加速(DOCKER_BUILDKIT=1)
4. Docker Daemon 优化
- 使用 overlay2 存储驱动(推荐)
- 配置镜像加速器
- 设置日志大小限制