Docker 入门与实战
- 1Docker 入门与实战本文
- 2Docker Compose 多容器编排实战
- 3Kubernetes 核心概念与实战入门
- 4CI/CD 持续集成与部署实战
应用交付的核心矛盾只有一个:构建它的环境和运行它的环境不一致。Docker 用镜像把应用及其运行时依赖打包成不可变的交付物,用容器把它跑起来,让”在我机器上能跑”这句话不再有存在的理由。
一、为什么要用 Docker
传统部署把依赖直接装在宿主机上:Python 版本、JDK 版本、系统动态库、环境变量,全都散落在服务器上。当同一台机器跑多个服务,或者开发、测试、生产由不同的人维护时,环境差异必然变成故障来源。典型症状:开发机是 Ubuntu 22.04、服务器是 CentOS 7,编译出的二进制跑不起来;本地 Node 18、CI 上 Node 16,锁文件解析结果不同;服务器上有人手工升级过 OpenSSL,另一个服务因此挂掉;新人配环境配一整天,还是缺某个 libssl。
Docker 的答案是:把应用和依赖一起打包成镜像,镜像在任何装了 Docker 的机器上以容器形式运行。它解决的不是性能问题,而是交付一致性问题。
容器与虚拟机的区别
虚拟机通过 Hypervisor 虚拟出整套硬件,每个实例都带一个完整操作系统和自己的内核;容器直接复用宿主机内核,只隔离进程、文件系统、网络等资源。这个根本差异决定了两者在启动速度和资源占用上的量级差别。
| 对比维度 | 虚拟机 (VM) | 容器 (Container) |
|---|---|---|
| 启动速度 | 数十秒到分钟级 | 毫秒到秒级 |
| 资源占用 | 每个实例独占一套 OS,内存通常 GB 级 | 共享内核,内存通常 MB 级 |
| 隔离级别 | 硬件级隔离,更彻底 | 内核级隔离(namespace + cgroup),相对较弱 |
| 镜像体积 | 通常数 GB | 通常几十 MB 到几百 MB |
| 适用场景 | 强隔离需求、异构内核、完整 OS 环境 | 微服务、CI/CD、快速交付与弹性伸缩 |
容器不是”轻量级虚拟机”,它本质上是被限制了资源、换了视角的一组 Linux 进程。判断标准是能不能共享内核:能共享就用容器,必须在 Linux 上跑 Windows 才用虚拟机。
二、三个核心概念
- 镜像 (Image):只读模板,等价于”类”或”安装包”。它由一层层只读文件系统叠加而成,每层对应 Dockerfile 中的一条构建指令。
- 容器 (Container):镜像的运行实例,等价于”对象”。启动时在镜像最上层加一个可写层,容器内所有写入都落在可写层,镜像本身永不变。
- 仓库 (Registry):集中存放和分发镜像的服务,等价于”应用商店”。默认是 Docker Hub,企业内常用 Harbor、云厂商的 ACR、TCR。
Dockerfile --docker build--> Image --docker push--> Registry ^ | |---- docker pull ----|Container <--docker run-- Image <----+由此可以直接推出几条结论:删容器不影响镜像,删镜像也不影响仓库里的副本;基于同一个镜像可以启动无数个互不干扰的容器;容器内改文件默认不持久,删容器即丢失,这正是数据卷要解决的问题;镜像分层是共享的,本地十个基于 node:20-alpine 的镜像,基础层在磁盘上只存一份。
三、安装与基本配置
Linux(Ubuntu / Debian)
用 Docker 官方源安装,不要用发行版仓库里的 docker.io,后者版本通常明显落后。
sudo apt-get update && sudo apt-get install -y ca-certificates curl gnupg && sudo install -m 0755 -d /etc/apt/keyringscurl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpgecho "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/nullsudo apt-get update && sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginsudo usermod -aG docker $USER # 免 sudo,需重新登录生效测试环境也可用一键脚本 curl -fsSL https://get.docker.com | sudo sh,生产建议走仓库方式以便版本管理。
Windows(WSL2 后端)
管理员 PowerShell 执行 wsl --install,重启后 wsl --set-default-version 2;安装 Docker Desktop 时勾选 “Use WSL 2 instead of Hyper-V”;在 Settings → Resources → WSL Integration 中启用对应发行版。关键建议:代码放在 WSL2 文件系统内(如 ~/projects),不要放 /mnt/c/... 下——跨文件系统 IO 慢好几倍,node_modules 安装会慢到无法忍受。
macOS
用 brew install --cask docker 安装 Docker Desktop,或 brew install orbstack 装更轻量的 OrbStack。Apple Silicon 默认拉取 arm64 镜像;目标镜像只有 amd64 版本时需显式指定平台,性能会有损失:docker pull --platform linux/amd64 legacy-image:1.0。
配置国内镜像加速器
编辑 /etc/docker/daemon.json,Windows/macOS 在 Docker Desktop 的 Settings → Docker Engine 中编辑同样的 JSON,然后重启服务:
{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://dockerproxy.com", "https://mirror.ccs.tencentyun.com" ], "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" }}sudo systemctl daemon-reload && sudo systemctl restart docker加速器地址的可用性随时间变化,docker pull 仍失败就先换地址再排查。
验证安装
docker version && docker info && docker run --rm hello-world # 版本 / 环境信息 / 冒烟测试四、镜像操作命令
docker pull nginx:1.27-alpine # 不写标签默认 latest,生产禁用docker pull --platform linux/amd64 openjdk:17-slimdocker images # -f dangling=true 只看悬空镜像docker history nginx:1.27-alpine # 构建历史与每层大小docker rmi nginx:1.27-alpine # 删除;image prune -a 清理未使用镜像docker tag myapp:1.0 registry.example.com/team/myapp:1.0 # 不复制数据,只加引用名docker login registry.example.com && docker push registry.example.com/team/myapp:1.0docker inspect nginx:1.27-alpine # 完整元数据docker inspect -f '{{json .Config.ExposedPorts}}' nginx:1.27-alpine # Go 模板抽取字段导入导出要区分两对命令:save/load 传输镜像(保留分层与标签,用于离线环境),export/import 传输容器文件系统(丢失历史分层,一般只用于制作极简基础镜像)。
docker save -o myapp-1.0.tar myapp:1.0 && docker load -i myapp-1.0.tardocker export -o container-fs.tar my-running-container && docker import container-fs.tar myapp:from-container五、容器生命周期命令
生命周期是:创建 → 启动 → 运行 → 停止 → 重启 → 删除。多数时候 docker run 一步到位,但排查时每个阶段都用得上。
docker run -d --name web nginx:1.27-alpine # 创建并启动;docker create 只创建不启动docker ps # 运行中;-a 含已退出,-aq 只输出 IDdocker start web && docker stop web # stop 先发 SIGTERM,默认 10 秒后 SIGKILLdocker kill web # 直接强杀;docker restart 重启docker rm web && docker rm -f web # -f 强制删除运行中的容器docker container prune # 清理已退出的容器docker top web && docker port web && docker diff web # 进程 / 端口映射 / 文件系统改动run 的常用参数
docker run -d --name my-api \ -p 8080:80 -v /data/api:/app/data \ -e TZ=Asia/Shanghai \ --restart unless-stopped --network app-net \ --memory 512m --cpus 1.0 \ registry.example.com/team/my-api:1.2.0| 参数 | 含义 | 实践建议 |
|---|---|---|
-d | 后台运行 | 服务类容器必加,不加会占住终端 |
-p 8080:80 | 端口映射,左为宿主机,右为容器 | 与 EXPOSE 无强绑定 |
-v 宿主:容器 | 挂载数据卷或绑定目录 | 数据必须落在这里 |
-e KEY=VALUE | 注入环境变量 | 密钥不要写命令行,用 --env-file |
--name | 指定容器名 | 不指定会随机生成,后续操作很难受 |
--restart | no / on-failure / always / unless-stopped | 生产用 unless-stopped |
--network | 加入指定网络 | 自定义网络内可用容器名互访 |
--rm / -it | 退出后自动删除 / 交互式 TTY | 一次性任务加上;调试时配合 sh |
--memory / --cpus / --user | 资源上限与运行用户 | 防止吃光宿主机;加固时用非 root |
最常踩的坑:-p 8080:80 与 -p 80:8080 方向相反,左边永远是宿主机。
exec、logs、stats、cp
docker exec -it web sh # alpine 没有 bash;-u root 可提权进入docker logs -f --tail 100 -t web # -t 带时间戳,-f 持续跟踪docker stats # 实时资源占用,Ctrl+C 退出;--no-stream 只采样一次docker cp web:/etc/nginx/nginx.conf ./nginx.conf.bak # 双向复制,参数顺序反过来即可不要用 docker cp 去宿主机 /var/lib/docker/containers/... 捞日志,那样既易损坏文件也难解析;正确做法是限制 log-driver 大小,再用 docker logs 或集中式日志系统采集。
六、Dockerfile 详解
Dockerfile 是一串指令,每条指令产生一个镜像层。理解”在构建期还是运行期生效”是写好它的前提。
| 指令 | 作用 | 生效阶段 |
|---|---|---|
FROM | 指定基础镜像,必须是第一条有效指令 | 构建期 |
RUN | 构建时执行命令,结果固化为新层 | 构建期 |
COPY | 从构建上下文复制文件 | 构建期 |
ADD | 类似 COPY,额外支持 URL 和自动解压 tar | 构建期 |
WORKDIR | 设置工作目录,自动创建 | 构建期并影响运行期 |
ENV | 环境变量,构建期与运行期都可见 | 两者 |
ARG | 构建参数,仅构建期可见 | 构建期 |
EXPOSE | 声明监听端口,元数据性质 | 元数据 |
VOLUME | 声明匿名卷挂载点 | 运行期 |
USER | 指定后续指令和容器的运行用户 | 两者 |
CMD | 默认启动命令,可被覆盖 | 运行期 |
ENTRYPOINT | 固定入口,参数可被追加 | 运行期 |
HEALTHCHECK / LABEL | 健康检查命令 / 镜像元数据 | 运行期 / 元数据 |
常用指令组合
FROM node:20.11-alpine3.19 # 固定版本与架构标签,禁止 latestARG NPM_REGISTRY=https://registry.npmmirror.com # 仅构建期可见ENV NODE_ENV=production TZ=Asia/Shanghai # 会写进镜像,运行期可见WORKDIR /app # 自动创建目录COPY --chown=node:node package.json package-lock.json ./COPY --from=builder /app/dist ./dist # 从多阶段构建的前序阶段复制产物ADD app.tar.gz /app/ # 额外支持 URL 与自动解压,非必要不用EXPOSE 8080VOLUME ["/app/data"]RUN addgroup -S app && adduser -S app -G appUSER app # 切换到非 root 用户ARG 与 ENV 的差别:ARG 仅构建期可见、不留在最终镜像;ENV 会持久化。但 ARG 的值仍会出现在 docker history 里,真正的密钥要用 BuildKit 的 secret 挂载。VOLUME 也有个隐蔽陷阱:在 VOLUME 声明之后对该目录执行 RUN 写入的数据会丢失,因为声明后它被当成挂载点——要么把数据准备放在声明之前,要么运行期用 -v 显式挂载。
CMD 与 ENTRYPOINT 的区别
一句话概括:ENTRYPOINT 定义”这个镜像是什么”,CMD 定义”默认怎么运行它”。
| 对比项 | CMD | ENTRYPOINT |
|---|---|---|
| 定位 | 默认命令或默认参数 | 固定入口命令 |
| 能否被覆盖 | docker run 后跟命令即覆盖 | 不能直接覆盖,需 --entrypoint |
| 追加参数的语义 | 追加参数会替换整个 CMD | 追加参数会追加到 ENTRYPOINT 之后 |
| 一个 Dockerfile 中数量 | 只有最后一条生效 | 只有最后一条生效 |
| shell 形式 | 走 /bin/sh -c,能解析变量但收不到信号 | 同左 |
| exec 形式 | 直接执行,进程是 PID 1,能收到信号 | 同左 |
| 组合用法 | 搭配 ENTRYPOINT 时作为默认参数 | 定义入口,CMD 提供参数默认值 |
CMD ["node", "server.js"] # 单独用时:docker run myimg ls 会绕过应用ENTRYPOINT ["node", "server.js"] # 单独用时:run 的追加参数会被追加到其后ENTRYPOINT ["node", "server.js"] # 组合,推荐CMD ["--port", "8080"] # 默认参数,命令行传参时整体替换组合写法下,docker run myimg --port 3000 时 CMD 整体被替换,最终执行 node server.js --port 3000。另一个必须注意的点:优先用 exec 形式(JSON 数组)。写成 shell 形式 CMD node server.js 时 PID 1 是 /bin/sh,docker stop 的 SIGTERM 会被 sh 吞掉,只能等满 10 秒被 SIGKILL,优雅关闭逻辑永远不执行。
七、构建镜像
docker build -t myapp:1.0 . # 当前目录为上下文docker build -f docker/Dockerfile.prod -t myapp:1.0 . # 指定 Dockerfiledocker build --build-arg NPM_REGISTRY=https://registry.npmmirror.com -t myapp:1.0 .docker build --target builder -t myapp:debug . # 只构建指定阶段docker build --no-cache -t myapp:1.0 . # 强制重建,不走缓存docker buildx build --platform linux/amd64,linux/arm64 -t registry.example.com/team/myapp:1.0 --push .构建上下文与 .dockerignore
命令最后那个 . 不是”去哪找 Dockerfile”,而是”把什么作为构建上下文交给构建器”。上下文内所有内容(.dockerignore 排除之后)都会被发送,COPY 只能引用上下文内的文件。上下文过大会直接拖慢构建:带 node_modules 和 .git 的项目上下文可能几百 MB,每次构建都要先传输一遍。也可以用管道方式执行 docker build -t app:1.0 -,从 stdin 读取 Dockerfile,此时不会发送任何构建上下文。
node_modules.gitdist.env*.logDockerfileREADME.md注意 .dockerignore 的路径相对上下文根目录,**/ 前缀才匹配任意层级。
分层缓存
缓存失效规则:某条指令的内容或其依赖的前一层发生变化,该层及其之后所有层全部重建。因此指令顺序决定构建速度——变化频率低的放前面、高的放后面。下面两个 Dockerfile 执行结果相同,但后者改源码时不需要重装依赖:
# 慢:任何文件变动都让后面的层全部失效FROM node:20-alpineWORKDIR /appCOPY . .RUN npm ciCMD ["npm", "start"]
# 快:依赖清单没变就命中缓存,改代码只重建最后一层FROM node:20-alpineWORKDIR /appCOPY package.json package-lock.json ./RUN npm ciCOPY . .CMD ["npm", "start"]八、数据持久化
容器可写层随容器生命周期存亡,删容器即丢数据。持久化有两条路:数据卷 (volume) 与绑定挂载 (bind mount)。
docker volume create pgdatadocker run -d --name pg -v pgdata:/var/lib/postgresql/data postgres:16-alpine # 数据卷docker run -d --name web -v /opt/site:/usr/share/nginx/html:ro nginx:1.27-alpine # 绑定挂载,:ro 只读docker run -d --tmpfs /tmp:rw,size=64m nginx:1.27-alpine # tmpfs,只在内存| 对比项 | 数据卷 (volume) | 绑定挂载 (bind mount) |
|---|---|---|
| 存储位置 | Docker 管理的目录,如 /var/lib/docker/volumes/pgdata | 宿主机上任意指定路径 |
| 创建方式 | docker volume create 或 -v 卷名:路径 自动创建 | 路径必须已存在 |
| 生命周期 | 独立于容器,删容器不删卷 | 完全由用户管理 |
| 跨平台一致性 | 一致,各平台都由虚拟机内管理 | Windows/Linux 路径与权限语义差异大 |
| 性能 / 可移植性 | 高且原生,可被多个容器复用 | Linux 原生高,跨文件系统慢,与宿主机路径强绑定 |
| 适用场景 | 数据库数据、应用持久数据、多容器共享 | 开发时挂载源码热更新、挂载配置文件 |
选型结论:生产环境的数据一律用数据卷(生命周期清晰、跨平台一致、便于备份);开发环境挂载源码或配置文件用绑定挂载(改文件立即生效,无需重建镜像)。只写容器路径不写卷名会创建匿名卷,名字随机、难以管理,应尽量避免。管理命令本身很轻:docker volume ls 列出,docker volume inspect pgdata 看挂载点与使用它的容器,docker volume rm 删除(必须先停止并删除使用它的容器),docker volume prune 清理所有未被使用的卷。
备份与恢复的标准做法是起一个临时容器,把卷和宿主机目录同时挂进去打 tar 包,不依赖宿主机上卷的实际路径:
docker run --rm -v pgdata:/data -v "$PWD":/backup alpine:3.20 tar czf /backup/pgdata-$(date +%Y%m%d).tar.gz -C /data .docker run --rm -v pgdata:/data -v "$PWD":/backup alpine:3.20 sh -c "rm -rf /data/* && tar xzf /backup/pgdata-20260911.tar.gz -C /data"数据库类容器应先保证数据一致:PostgreSQL 用 pg_dump 做逻辑备份更安全,直接打包数据目录属于物理备份,必须在数据库停止或快照一致的前提下执行。
九、网络
| 模式 | 说明 | 典型用途 |
|---|---|---|
bridge | 默认模式,接入 Docker 网桥,通过 NAT 访问外网 | 绝大多数单机场景 |
host | 直接使用宿主机网络命名空间,无端口映射,性能最好 | 高性能代理、需要绑定大量端口 |
none / container:名称 | 只有 lo 回环 / 共享另一容器的网络命名空间 | 离线批处理与安全隔离 / sidecar 模式 |
| 自定义 bridge | 用户创建,内置 DNS 服务发现 | 生产推荐,服务间用容器名通信 |
默认 bridge 下的容器只能通过 IP 互访,而 IP 随容器重建变化,因此不可用于服务间调用。自定义网络提供内置 DNS,容器名就是主机名:
docker network create --driver bridge app-net # 也可加 --subnet 172.28.0.0/16docker run -d --name db --network app-net -e POSTGRES_PASSWORD=secret postgres:16-alpinedocker run -d --name api --network app-net -p 8080:8080 my-api:1.0docker exec -it api sh -c "nc -zv db 5432" # 直接用容器名访问数据库docker network connect app-net-2 api # 把运行中的容器接入另一个网络docker network disconnect app-net api同一自定义网络内的容器用容器名或网络别名互访;不同网络的容器默认完全隔离。这也是做环境隔离的基础——把生产容器和测试容器放进不同网络。排查时常用下面这些手段:
docker run --rm --network app-net nicolaka/netshoot ping -c 2 db # 临时容器测连通性docker exec -it api cat /etc/resolv.conf # 容器内 DNS 配置docker inspect -f '{{json .NetworkSettings.Networks}}' api端口映射原理
-p 8080:80 背后是 iptables(或 nftables)的 DNAT 规则:宿主机 8080 收到流量,DOCKER 链把目标地址改写为容器 IP 的 80 端口,回程由 conntrack 自动反向 NAT。由此可直接推出几个现象:端口被占用时启动失败,报 bind: address already in use;服务在容器内监听 127.0.0.1 时映射后宿主机访问不通,因为网络命名空间里 127.0.0.1 只指容器自身,服务必须监听 0.0.0.0;-p 127.0.0.1:8080:80 只绑定回环地址,可避免暴露到公网;host 模式下 -p 无效,因为没有独立网络命名空间。
十、多阶段构建
多阶段构建解决”构建时需要的工具链不该出现在运行镜像里”的矛盾:同一个 Dockerfile 里可以有多个 FROM,每个 FROM 开启一个新阶段,阶段之间用 COPY --from= 传递产物。单阶段写法把完整工具链、npm 缓存、devDependencies 和 .git 全部塞进镜像,通常 1GB 以上;改为多阶段后运行镜像仅保留生产依赖与构建产物:
FROM node:20.11-alpine3.19 AS builderWORKDIR /appCOPY package.json package-lock.json ./RUN npm ci --include=dev && npm cache clean --forceCOPY . .RUN npm run build
FROM node:20.11-alpine3.19 AS runnerWORKDIR /appENV NODE_ENV=production TZ=Asia/ShanghaiCOPY package.json package-lock.json ./RUN npm ci --omit=dev && npm cache clean --force # 只装生产依赖COPY --from=builder /app/dist ./dist # 只复制构建产物RUN addgroup -S nodejs && adduser -S nodejs -G nodejs && chown -R nodejs:nodejs /appUSER nodejsEXPOSE 3000CMD ["node", "dist/server.js"]体积可降到 150MB 左右;换成 distroless 基础镜像后能压到 100MB 以内。Java 应用同理:单阶段的 Maven 构建镜像会把 Maven、本地仓库、JDK 全带进去(通常 800MB 到 1GB),改为 Maven 构建、JRE 运行,并用 Spring Boot 的 layertools 把 fat jar 拆成四层,业务代码改动只让最后一层失效,依赖层几乎总能命中缓存:
FROM maven:3.9-eclipse-temurin-17 AS builderWORKDIR /buildCOPY pom.xml .RUN mvn -B dependency:go-offline # 依赖先行,利用层缓存COPY src ./srcRUN mvn -B clean package -DskipTestsRUN java -Djarmode=layertools -jar target/app.jar extract
FROM eclipse-temurin:17-jre-alpine AS runnerRUN addgroup -S spring && adduser -S spring -G springWORKDIR /appUSER spring:springCOPY --from=builder --chown=spring:spring /build/extracted/dependencies/ ./COPY --from=builder --chown=spring:spring /build/extracted/spring-boot-loader/ ./COPY --from=builder --chown=spring:spring /build/extracted/snapshot-dependencies/ ./COPY --from=builder --chown=spring:spring /build/extracted/application/ ./ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -Duser.timezone=Asia/Shanghai"ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher"]ENTRYPOINT 里的 exec 前缀是必要的,否则 java 进程不是 PID 1,收不到 SIGTERM,优雅停机不会执行。
十一、实战:容器化一个 React (Vite) 应用
目标是把 Vite 构建的 SPA 打包成 Nginx 静态托管镜像,支持前端路由 history 模式,并暴露健康检查端点。项目根目录下放 Dockerfile、nginx.conf 和 .dockerignore(内容见第七节)。
# ---------- 阶段一:构建静态资源 ----------FROM node:20.11-alpine3.19 AS builderWORKDIR /appARG VITE_API_BASE_URL=/api # Vite 只把 VITE_ 前缀的变量内联进产物ENV VITE_API_BASE_URL=$VITE_API_BASE_URLCOPY package.json package-lock.json ./RUN npm ci && npm cache clean --forceCOPY . .RUN npm run build
# ---------- 阶段二:Nginx 托管 ----------FROM nginx:1.27-alpine AS runnerRUN rm -f /etc/nginx/conf.d/default.confCOPY nginx.conf /etc/nginx/conf.d/app.confCOPY --from=builder /app/dist /usr/share/nginx/html# 非 root 用户不能监听 1024 以下端口,容器内改听 8080,对外端口由宿主机决定RUN chown -R nginx:nginx /usr/share/nginx/html /var/cache/nginx /var/log/nginx && touch /tmp/nginx.pid && chown nginx:nginx /tmp/nginx.pidUSER nginxEXPOSE 8080HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD wget -qO- http://127.0.0.1:8080/healthz || exit 1CMD ["nginx", "-g", "daemon off;"]nginx.conf:
server { listen 8080; server_name _; root /usr/share/nginx/html;
gzip on; gzip_comp_level 5; gzip_types text/plain text/css application/json application/javascript image/svg+xml;
location ^~ /assets/ { # 带 hash 的资源可长期缓存 expires 1y; add_header Cache-Control "public, immutable"; } location = /index.html { # 不缓存,保证发版后拿到最新资源引用 add_header Cache-Control "no-cache, no-store, must-revalidate"; } location = /healthz { # 健康检查端点 add_header Content-Type text/plain; return 200 "ok\n"; } location / { # SPA history 模式回退 try_files $uri $uri/ /index.html; } location /api/ { # 反向代理后端,避免浏览器跨域 proxy_pass http://backend:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }
}构建、运行与验证:
docker build -t my-react-app:1.0 .docker run -d --name my-react-app -p 8080:8080 --restart unless-stopped my-react-app:1.0curl -I http://127.0.0.1:8080/healthz # 健康检查端点curl -I http://127.0.0.1:8080/some/deep/route # 验证 history 路由回退用 Compose 编排前后端,proxy_pass http://backend:8080/ 中的 backend 就是服务名,同一自定义网络内由内置 DNS 解析,不需要写死 IP:
同一套镜像也可以用 Compose 编排,proxy_pass http://backend:8080/ 中的 backend 就是服务名,由内置 DNS 解析,不需要写死 IP:
services: backend: image: registry.example.com/team/my-api:1.2.0 networks: [app-net] frontend: build: { context: ., args: { VITE_API_BASE_URL: /api } } ports: ["80:8080"] depends_on: backend: { condition: service_healthy } networks: [app-net]networks: app-net: { driver: bridge }至于体积收益:同样的应用单阶段打包是 1.1GB(node:20),多阶段换 node:20-alpine 后约 150MB,改成 Nginx 托管静态产物只需 45MB(nginx:1.27-alpine),再裁剪到 distroless 可压到 30MB。
十二、生产最佳实践
镜像瘦身。 优先用 alpine、slim 或 distroless 基础镜像,distroless 连 shell 都没有,攻击面最小,代价是排障时无法 docker exec sh,只能靠日志与健康检查。nginx:1.27-alpine 只有 45MB 而 nginx:1.27 有 190MB,差距全部来自基础镜像。安装与清理必须放在同一条 RUN 里,否则清理动作落在单独一层而失效,标准写法是 RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/*。
非 root 用户。 默认以 root 运行意味着容器内进程逃逸后直接拥有宿主机 root 权限。基础镜像自带非 root 用户时直接 USER node;否则自己创建(alpine 用 RUN addgroup -S app && adduser -S app -G app,Debian 用 useradd -r -u 10001 -m appuser),再 USER app。运行期也能用 --user 1000:1000 覆盖,但要注意挂载卷的属主,卷属主是 root 时会报权限错误。
固定基础镜像版本。 禁止 FROM node:latest,内容会漂移,今天构建成功明天可能失败。至少固定到次版本(node:20.11.1-alpine3.19),最严格是锁定 digest,形如 FROM node@sha256:7f5c9b3a...,代价是安全更新也不会自动进来,需要配合自动化的依赖升级流程。
分层缓存优化。 依赖清单先行、源码最后复制;不常变的 ENV、ARG 放前面;apt-get install 集中写在一条指令里。CI 中启用 BuildKit 的缓存与 secret 挂载,依赖下载目录可跨构建复用且不会进入镜像层:在 Dockerfile 首行加 # syntax=docker/dockerfile:1.7,把安装命令写成 RUN --mount=type=cache,target=/root/.npm --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci --registry=https://registry.example.com,构建时用 docker build --secret id=npmrc,src=$HOME/.npmrc -t app:1.0 . 传入私服凭证。
健康检查。 --interval 检查间隔、--timeout 单次超时、--start-period 启动宽限期(期内失败不计入 --retries)、--retries 连续失败多少次标记 unhealthy。典型写法 HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 CMD curl -fsS http://127.0.0.1:8080/health || exit 1。健康检查只标记状态,Docker 本身不会重启 unhealthy 的容器,需要配合 Compose 的 depends_on: condition: service_healthy 或编排平台的探针来驱动重启与流量摘除。
日志处理。 默认的 json-file 驱动不做轮转,长时间运行会把 /var/lib/docker 撑满磁盘,这是最常见的生产事故。在 daemon.json 中把 log-driver 设为 json-file 并加上 log-opts 的 max-size: 50m 与 max-file: 3 即可限制单容器日志体积。更完整的方案是接 fluentd、gelf 或 syslog 驱动,把日志送到 ELK、Loki 之类的集中式系统。
其他容易忽略的点。 docker stop 默认超时 10 秒,应用优雅关闭更久时应显式 docker stop -t 30。容器内只跑一个前台进程,用 supervisord 塞多进程会带来信号转发和僵尸进程问题,正确做法是拆成多个容器。需要写入的目录声明为 VOLUME 或挂载卷,保持容器无状态才能随时重建。用 --read-only 加 --tmpfs /tmp 运行可显著降低被写入攻击的风险,前提是应用不写文件系统。
十三、常见问题排查
容器启动即退出
先看退出码和日志,不要猜:
docker ps -a # 看 EXIT CODEdocker logs --tail 100 容器名docker inspect -f '{{.State.Status}} {{.State.ExitCode}} {{.State.OOMKilled}} {{.State.Error}}' 容器名docker run -it --entrypoint sh 镜像名 # 覆盖入口进容器查环境| 退出码 | 含义 | 常见原因 |
|---|---|---|
| 0 | 正常退出 | 主进程是短命令,或脚本把进程 daemon 化了 |
| 1 | 应用错误 | 启动异常、配置缺失、依赖不可用 |
| 126 / 127 | 无法执行 / 找不到命令 | 没有执行权限、缺 shebang,或命令不存在(alpine 里用了 bash) |
| 137 / 139 | 被 SIGKILL / 段错误 | 通常 OOM 被内核杀掉;或二进制架构不匹配 |
| 143 | 被 SIGTERM | 正常 docker stop 后的退出码,属预期行为 |
最常见的两条:一是 Dockerfile 用了 shell 形式的 CMD 且脚本把主进程 daemon 化,容器没有前台进程可守;二是镜像里没有 bash(alpine 只有 sh),CMD ["bash", ...] 直接 127。
端口占用
报错形如 bind: address already in use,先用 sudo ss -lntp | grep :8080(Windows 用 netstat -ano | findstr :8080)确认占用者,再换宿主机端口启动,例如 docker run -d -p 8081:80 nginx:1.27-alpine。别忘检查 docker ps -a 里状态为 Up 的旧容器,它们同样占用端口。
时区问题
容器默认时区是 UTC,日志时间会比北京时间少 8 小时。最省事的是注入 -e TZ=Asia/Shanghai,也可以挂载宿主机时区文件 -v /etc/localtime:/etc/localtime:ro;若要在 Dockerfile 中固定,写 ENV TZ=Asia/Shanghai 与 RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 两行即可。Java 应用除 TZ 外还要注意 JVM 自身参数,加 -Duser.timezone=Asia/Shanghai 更稳妥。
权限问题
症状是 Permission denied 或 Operation not permitted,通常三个来源:容器内以非 root 运行但挂载目录属主是 root,解决方式是构建时 chown 或用 -u "$(id -u):$(id -g)" 对齐属主;宿主机开启 SELinux(CentOS/RHEL)需要给挂载加 :z 或 :Z 标签;容器内进程需要特殊 capability,默认已丢弃大部分,需要用 --cap-add 但应优先考虑非特权方案。调试时先 docker exec -it 容器名 ls -aln /data 确认属主与权限。
磁盘占用清理
Docker 占用来自四处:镜像、容器、卷、构建缓存。清理顺序是先看再删:
docker system df # 总览各部分占用,-v 看明细docker system prune # 安全清理:停止的容器、未使用网络、悬空镜像、构建缓存docker system prune -a --volumes # 更彻底,含未被使用的镜像与卷docker container prune && docker image prune -a && docker volume prunedocker builder prune --keep-storage 10GBdocker system prune -a --volumes 会删除未被使用的数据卷,数据库数据可能就在里面,执行前务必确认 docker volume ls 的内容。生产环境建议把 data-root 放在独立分区并监控磁盘水位。其他高频问题:docker info | grep -A 5 "Registry Mirrors" 确认加速器生效;容器内 DNS 解析看 /etc/resolv.conf;宿主机内网与默认网段冲突时在 daemon.json 中设 "bip": "10.20.0.1/24";WSL2 磁盘膨胀需 wsl --shutdown 后压缩 vhdx。
十四、命令速查表
镜像
| 命令 | 说明 |
|---|---|
docker pull nginx:1.27-alpine | 拉取镜像 |
docker images | 列出本地镜像 |
docker rmi 镜像:标签 / docker tag 源 目标 / docker push 仓库/镜像:标签 | 删除 / 打标签 / 推送镜像 |
docker save -o a.tar 镜像 / docker load -i a.tar | 导出 / 导入镜像 |
docker history 镜像 / docker inspect 镜像 | 构建历史 / 镜像元数据 |
容器
| 命令 | 说明 |
|---|---|
docker run -d -p 8080:80 --name web nginx | 后台运行并映射端口 |
docker ps / docker ps -a / docker ps -aq | 列出运行中 / 全部 / 仅 ID |
docker start/stop/restart/kill 容器 | 启动 / 停止 / 重启 / 强杀 |
docker rm -f 容器 / docker container prune | 删除容器 / 清理已退出 |
docker exec -it 容器 sh / docker logs -f --tail 100 容器 | 进入容器 / 跟踪日志 |
docker stats / docker cp 容器:路径 本地路径 | 实时资源占用 / 复制文件 |
构建
| 命令 | 说明 |
|---|---|
docker build -t app:1.0 . | 构建镜像 |
docker build -f Dockerfile.prod --target builder -t app:1.0 . | 指定 Dockerfile 与阶段 |
docker build --build-arg K=V -t app:1.0 . | 传构建参数 |
docker buildx build --platform linux/amd64,linux/arm64 --push . | 多架构构建推送 |
卷与网络
| 命令 | 说明 |
|---|---|
docker volume create 卷名 / ls / inspect 卷名 | 创建 / 列出 / 查看数据卷 |
docker volume rm 卷名 / docker volume prune | 删除 / 清理数据卷 |
docker network create 网络名 | 创建自定义网络 |
docker network connect 网络 容器 / disconnect | 接入 / 断开网络 |
系统与清理
| 命令 | 说明 |
|---|---|
docker version / docker info | 版本 / 环境信息 |
docker system df / docker system df -v | 磁盘占用总览 / 明细 |
docker system prune | 安全清理未使用资源 |
docker compose up -d / docker compose down | 编排启停 |
docker compose logs -f 服务名 / docker compose ps | 服务日志 / 服务状态 |
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!













