Docker 实战:从会用到用好
很多人会
docker run,但 Dockerfile 写得一塌糊涂,镜像动辄 2GB,CI 跑几分钟。这篇聚焦日常真正有用的实践。
容器 vs 虚拟机
容器不是"轻量级虚拟机",本质上是带隔离的进程。
| 技术 | 隔离机制 | 启动时间 | 开销 |
|---|---|---|---|
| 容器 | Linux namespace + cgroup | < 1秒 | 极低 |
| 虚拟机 | Hypervisor + 独立内核 | 数十秒 | 较高 |
- namespace:隔离进程、网络、文件系统、用户
- cgroup:限制 CPU、内存、IO 资源使用
容器共享宿主机内核,这是它轻量的原因,也是安全边界比 VM 弱的原因。
Dockerfile 最佳实践
层缓存利用
# 糟糕:每次代码变化都重新安装依赖
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
# 好:先复制依赖文件,利用缓存
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
缓存失效规则: 某层失效,其后所有层都失效。把变化频率低的放前面(依赖 > 配置 > 源代码)。
.dockerignore
node_modules/
.git/
.env
*.log
dist/
coverage/
.DS_Store
减少构建上下文大小,防止 node_modules(数百MB)被发送到 Docker daemon。
非 root 用户运行
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --chown=appuser:appgroup . .
USER appuser
EXPOSE 3000
CMD ["node", "server.js"]
多阶段构建
多阶段构建是缩小镜像体积最有效的方法。
# Go 应用:编译 + 运行分离
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server ./cmd/server
FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/server"]
# 最终镜像:~10MB(而不是 ~800MB 的 golang 镜像)
# Node.js 前端应用
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
# 最终镜像:~25MB
镜像瘦身技巧
# 1. 使用 slim 基础镜像
FROM python:3.12-slim # ~130MB vs python:3.12 的 ~900MB
# 2. 合并 RUN 层(减少层数)
# 不好的写法(3层)
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# 好的写法(1层)
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
# 3. 不要缓存 pip 包
RUN pip install -r requirements.txt --no-cache-dir
# 4. 只安装需要的包
RUN apt-get install -y --no-install-recommends libpq-dev && \
rm -rf /var/lib/apt/lists/*
Docker Compose
# docker-compose.yml
version: '3.9'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://user:pass@db:5432/mydb
depends_on:
db:
condition: service_healthy
volumes:
- ./uploads:/app/uploads
restart: unless-stopped
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
docker compose up -d # 后台启动
docker compose down # 停止并删除容器
docker compose logs -f app # 实时查看日志
docker compose exec app bash # 进入容器
docker compose build --no-cache
网络模式
# bridge(默认):容器间用服务名互相通信
# 在 compose 中,服务名就是 DNS 名
# app 访问 db:postgresql://db:5432/xxx
# host:容器直接使用宿主机网络
docker run --network host nginx
# 自定义网络
docker network create mynet
docker run --network mynet --name service1 image1
docker run --network mynet --name service2 image2
# service2 可以 ping service1
数据持久化
# Volume(推荐用于持久数据)
docker volume create mydata
docker run -v mydata:/app/data myimage
# volume 由 Docker 管理,路径在 /var/lib/docker/volumes/
# Bind Mount(推荐用于开发时挂载代码)
docker run -v $(pwd)/src:/app/src myimage
# 备份 volume
docker run --rm -v mydata:/data -v $(pwd):/backup alpine \
tar czf /backup/mydata-backup.tar.gz -C /data .
# 恢复
docker run --rm -v mydata:/data -v $(pwd):/backup alpine \
tar xzf /backup/mydata-backup.tar.gz -C /data
常见坑
PID 1 信号处理
# shell 形式(sh 是 PID 1,不转发信号给 node)
CMD node server.js
# exec 形式(node 直接是 PID 1,接收 SIGTERM)
CMD ["node", "server.js"]
# 使用 tini(需要僵尸进程回收时)
FROM node:20-alpine
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]
时区问题
# Alpine
RUN apk add --no-cache tzdata && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone && \
apk del tzdata
# 或通过环境变量
ENV TZ=Asia/Shanghai
编码问题
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
生产实践
# 资源限制
docker run \
--memory 512m \
--memory-swap 512m \
--cpus 0.5 \
--pids-limit 100 \
myimage
# 重启策略
docker run --restart unless-stopped myimage
# 日志驱动
docker run \
--log-driver json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
myimage
调试技巧
# 进入运行中的容器
docker exec -it <container_id> bash
# 查看容器详细信息
docker inspect <container_id>
docker inspect <container_id> | jq '.[0].NetworkSettings'
# 查看日志
docker logs -f --tail 100 <container_id>
# 资源使用
docker stats --no-stream
# 事件监听
docker events --filter type=container
与 GitHub Actions 集成
# .github/workflows/docker.yml
name: Build and Push
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myuser/myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max