Docker 入门与实战

7184 字
36 分钟
Docker 入门与实战

应用交付的核心矛盾只有一个:构建它的环境和运行它的环境不一致。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,后者版本通常明显落后。

Terminal window
sudo apt-get update && sudo apt-get 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
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
sudo apt-get update && sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo 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" }
}
Terminal window
sudo systemctl daemon-reload && sudo systemctl restart docker

加速器地址的可用性随时间变化,docker pull 仍失败就先换地址再排查。

验证安装#

Terminal window
docker version && docker info && docker run --rm hello-world # 版本 / 环境信息 / 冒烟测试

四、镜像操作命令#

Terminal window
docker pull nginx:1.27-alpine # 不写标签默认 latest,生产禁用
docker pull --platform linux/amd64 openjdk:17-slim
docker 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.0
docker inspect nginx:1.27-alpine # 完整元数据
docker inspect -f '{{json .Config.ExposedPorts}}' nginx:1.27-alpine # Go 模板抽取字段

导入导出要区分两对命令:save/load 传输镜像(保留分层与标签,用于离线环境),export/import 传输容器文件系统(丢失历史分层,一般只用于制作极简基础镜像)。

Terminal window
docker save -o myapp-1.0.tar myapp:1.0 && docker load -i myapp-1.0.tar
docker export -o container-fs.tar my-running-container && docker import container-fs.tar myapp:from-container

五、容器生命周期命令#

生命周期是:创建 → 启动 → 运行 → 停止 → 重启 → 删除。多数时候 docker run 一步到位,但排查时每个阶段都用得上。

Terminal window
docker run -d --name web nginx:1.27-alpine # 创建并启动;docker create 只创建不启动
docker ps # 运行中;-a 含已退出,-aq 只输出 ID
docker start web && docker stop web # stop 先发 SIGTERM,默认 10 秒后 SIGKILL
docker 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 的常用参数#

Terminal window
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指定容器名不指定会随机生成,后续操作很难受
--restartno / 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#

Terminal window
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 # 固定版本与架构标签,禁止 latest
ARG 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 8080
VOLUME ["/app/data"]
RUN addgroup -S app && adduser -S app -G app
USER app # 切换到非 root 用户

ARGENV 的差别:ARG 仅构建期可见、不留在最终镜像;ENV 会持久化。但 ARG 的值仍会出现在 docker history 里,真正的密钥要用 BuildKit 的 secret 挂载。VOLUME 也有个隐蔽陷阱:在 VOLUME 声明之后对该目录执行 RUN 写入的数据会丢失,因为声明后它被当成挂载点——要么把数据准备放在声明之前,要么运行期用 -v 显式挂载。

CMD 与 ENTRYPOINT 的区别#

一句话概括:ENTRYPOINT 定义”这个镜像是什么”,CMD 定义”默认怎么运行它”。

对比项CMDENTRYPOINT
定位默认命令或默认参数固定入口命令
能否被覆盖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/shdocker stop 的 SIGTERM 会被 sh 吞掉,只能等满 10 秒被 SIGKILL,优雅关闭逻辑永远不执行。

七、构建镜像#

Terminal window
docker build -t myapp:1.0 . # 当前目录为上下文
docker build -f docker/Dockerfile.prod -t myapp:1.0 . # 指定 Dockerfile
docker 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
.git
dist
.env
*.log
Dockerfile
README.md

注意 .dockerignore 的路径相对上下文根目录,**/ 前缀才匹配任意层级。

分层缓存#

缓存失效规则:某条指令的内容或其依赖的前一层发生变化,该层及其之后所有层全部重建。因此指令顺序决定构建速度——变化频率低的放前面、高的放后面。下面两个 Dockerfile 执行结果相同,但后者改源码时不需要重装依赖:

# 慢:任何文件变动都让后面的层全部失效
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci
CMD ["npm", "start"]
# 快:依赖清单没变就命中缓存,改代码只重建最后一层
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["npm", "start"]

八、数据持久化#

容器可写层随容器生命周期存亡,删容器即丢数据。持久化有两条路:数据卷 (volume) 与绑定挂载 (bind mount)。

Terminal window
docker volume create pgdata
docker 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 包,不依赖宿主机上卷的实际路径:

Terminal window
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,容器名就是主机名:

Terminal window
docker network create --driver bridge app-net # 也可加 --subnet 172.28.0.0/16
docker run -d --name db --network app-net -e POSTGRES_PASSWORD=secret postgres:16-alpine
docker run -d --name api --network app-net -p 8080:8080 my-api:1.0
docker exec -it api sh -c "nc -zv db 5432" # 直接用容器名访问数据库
docker network connect app-net-2 api # 把运行中的容器接入另一个网络
docker network disconnect app-net api

同一自定义网络内的容器用容器名或网络别名互访;不同网络的容器默认完全隔离。这也是做环境隔离的基础——把生产容器和测试容器放进不同网络。排查时常用下面这些手段:

Terminal window
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 builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --include=dev && npm cache clean --force
COPY . .
RUN npm run build
FROM node:20.11-alpine3.19 AS runner
WORKDIR /app
ENV NODE_ENV=production TZ=Asia/Shanghai
COPY 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 /app
USER nodejs
EXPOSE 3000
CMD ["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 builder
WORKDIR /build
COPY pom.xml .
RUN mvn -B dependency:go-offline # 依赖先行,利用层缓存
COPY src ./src
RUN mvn -B clean package -DskipTests
RUN java -Djarmode=layertools -jar target/app.jar extract
FROM eclipse-temurin:17-jre-alpine AS runner
RUN addgroup -S spring && adduser -S spring -G spring
WORKDIR /app
USER spring:spring
COPY --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 模式,并暴露健康检查端点。项目根目录下放 Dockerfilenginx.conf.dockerignore(内容见第七节)。

# ---------- 阶段一:构建静态资源 ----------
FROM node:20.11-alpine3.19 AS builder
WORKDIR /app
ARG VITE_API_BASE_URL=/api # Vite 只把 VITE_ 前缀的变量内联进产物
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
COPY package.json package-lock.json ./
RUN npm ci && npm cache clean --force
COPY . .
RUN npm run build
# ---------- 阶段二:Nginx 托管 ----------
FROM nginx:1.27-alpine AS runner
RUN rm -f /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/app.conf
COPY --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.pid
USER nginx
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD wget -qO- http://127.0.0.1:8080/healthz || exit 1
CMD ["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;
}
}

构建、运行与验证:

Terminal window
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.0
curl -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。

十二、生产最佳实践#

镜像瘦身。 优先用 alpineslimdistroless 基础镜像,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...,代价是安全更新也不会自动进来,需要配合自动化的依赖升级流程。

分层缓存优化。 依赖清单先行、源码最后复制;不常变的 ENVARG 放前面;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-optsmax-size: 50mmax-file: 3 即可限制单容器日志体积。更完整的方案是接 fluentdgelfsyslog 驱动,把日志送到 ELK、Loki 之类的集中式系统。

其他容易忽略的点。 docker stop 默认超时 10 秒,应用优雅关闭更久时应显式 docker stop -t 30。容器内只跑一个前台进程,用 supervisord 塞多进程会带来信号转发和僵尸进程问题,正确做法是拆成多个容器。需要写入的目录声明为 VOLUME 或挂载卷,保持容器无状态才能随时重建。用 --read-only--tmpfs /tmp 运行可显著降低被写入攻击的风险,前提是应用不写文件系统。

十三、常见问题排查#

容器启动即退出#

先看退出码和日志,不要猜:

Terminal window
docker ps -a # 看 EXIT CODE
docker 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/ShanghaiRUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 两行即可。Java 应用除 TZ 外还要注意 JVM 自身参数,加 -Duser.timezone=Asia/Shanghai 更稳妥。

权限问题#

症状是 Permission deniedOperation 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 占用来自四处:镜像、容器、卷、构建缓存。清理顺序是先看再删:

Terminal window
docker system df # 总览各部分占用,-v 看明细
docker system prune # 安全清理:停止的容器、未使用网络、悬空镜像、构建缓存
docker system prune -a --volumes # 更彻底,含未被使用的镜像与卷
docker container prune && docker image prune -a && docker volume prune
docker builder prune --keep-storage 10GB

docker 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服务日志 / 服务状态

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Docker 入门与实战
https://sakura-hu.top/posts/devops/docker入门与实战/
作者
Sakura
发布于
2026-09-11
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Sakura
Hello, I'm Sakura.
公告
欢迎来到我的博客! 我是Sakura,祝你拥有美好的一天~
分类
标签
最新动态
站点统计
文章
21
分类
7
标签
35
总字数
85,101
运行时长
0
最后活动
0 天前
站点信息
构建平台
GitHub Actions
博客版本
Firefly v6.16.7
文章许可
CC BY-NC-SA 4.0
文章目录