docker-compose-hardening
package_installshell_execution
name: docker-compose-hardening
description: Docker 与 Compose 部署的收口和加固。当要把服务容器化上线、要审一份 Dockerfile 或 compose 文件、镜像太大、容器反复重启、要限制资源、要处理日志和数据卷时使用。触发词:Dockerfile、docker compose、镜像太大、容器重启、OOMKilled、数据卷、挂载、多阶段构建、镜像瘦身、容器日志、latest 标签、healthcheck。不负责 Kubernetes 编排和服务发布流程(走 go-service-release)。
Docker 部署收口
容器化的常见故障就那么几类:镜像里带了密钥、用了 latest 标签导致版本漂移、没设资源限制拖垮宿主机、数据卷没规划导致数据丢失、日志把磁盘写满。这份检查单围绕这五类。
一、Dockerfile
多阶段构建
编译产物和编译工具链不要出现在同一个镜像里。
FROM golang:1.23-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /out/app ./cmd/app
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
要点:
- 先拷依赖清单再拷源码。 依赖层能被缓存,改一行代码不用重装依赖。
- 运行阶段用最小基础镜像:Go 用 distroless 或 scratch,Node/Python 用 slim。
- 基础镜像必须 pin 死版本,禁止
latest。写golang:1.23-alpine,更严格的写摘要@sha256:...。 USER必须设成非 root。 默认 root 是最常见的容器安全问题。
不要把密钥打进镜像
- 密钥、证书、
.env一律走运行时环境变量或密钥管理,不进 Dockerfile、不进构建参数、不进镜像层。 - 注意:
ARG传进来的值会留在镜像历史里,docker history能看到。需要构建期密钥就用 BuildKit 的--mount=type=secret。 - 写
.dockerignore,至少包含.git、.env*、node_modules、*.pem、*.key。没有.dockerignore时COPY . .会把整个.git目录连同历史里的密钥一起打进去。
镜像瘦身
按收益排序:换更小的基础镜像 → 多阶段构建 → 合并 RUN 层并在同层清理缓存 → 删掉构建缓存和文档。
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
清理必须和安装在同一个 RUN 里,分开写的话上一层已经把文件固化了,删了也不减体积。
二、Compose 文件
services:
app:
image: registry.example.com/app:0.229.0 # 不用 latest
restart: unless-stopped
env_file: [.env] # .env 不进仓库
ports: ["127.0.0.1:8080:8080"] # 只绑本机,外部走反向代理
depends_on:
db: { condition: service_healthy }
healthcheck:
test: ["CMD", "/app", "healthcheck"]
interval: 10s
timeout: 3s
retries: 3
start_period: 20s
deploy:
resources:
limits: { cpus: "2.0", memory: 1g }
logging:
driver: json-file
options: { max-size: "50m", max-file: "3" }
stop_grace_period: 30s
逐条说明为什么:
image写死版本。latest会让「重启一下」变成「悄悄升级到未测试的版本」。- 端口绑
127.0.0.1。 直接写"8080:8080"会绕过宿主机防火墙暴露到公网,这是个很常见且很危险的坑。 depends_on要配condition: service_healthy。 只写服务名只保证启动顺序,不保证依赖真的可用。- 必须有
healthcheck。 没有健康检查的容器,编排层不知道它是不是活的。健康检查要真检查依赖。 - 必须有资源限制。 没有
memory限制时,一个泄漏的容器能把整台机器拖死。 - 必须配日志轮转。 Docker 默认的 json-file 驱动不限大小,几个月后磁盘就满了。这是容器化最常见的磁盘满原因。
stop_grace_period要大于应用的优雅退出时间。
三、数据与卷
- 有状态数据必须用命名卷或绑定挂载,绝不能只存在容器内。容器是一次性的,重建就没了。
- 绑定挂载写清楚只读还是读写,配置文件挂
:ro。 - 数据库容器化要谨慎。 开发环境随意,生产环境的数据库建议用托管服务或独立部署。容器里的数据库在磁盘性能和故障恢复上都更麻烦。
- 备份针对的是卷,不是容器。备份要定期做恢复演练,没验证过的备份等于没有。
四、运行时安全
read_only: true
tmpfs: ["/tmp"]
cap_drop: ["ALL"]
security_opt: ["no-new-privileges:true"]
- 根文件系统只读,需要写的目录单独挂 tmpfs 或卷。
- 丢掉所有 capability,需要哪个再单独加。
- 禁止提权。
- 不要用
privileged: true,也不要挂载/var/run/docker.sock到业务容器。挂了等于把宿主机 root 权限给了容器。
五、容器反复重启的排查
docker compose ps # 看状态和退出码
docker compose logs --tail 200 <svc>
docker inspect <容器> --format '{{.State.ExitCode}} {{.State.OOMKilled}}'
docker stats --no-stream
按退出码分诊:
| 现象 | 常见原因 |
|---|---|
OOMKilled: true |
内存限制太小,或应用泄漏。JVM/Node 的堆上限要跟着容器限制设 |
| 退出码 1 + 日志有报错 | 配置缺失、依赖连不上、迁移失败 |
| 退出码 137 | 被 SIGKILL,通常是 OOM 或停止超时 |
| 健康检查一直不过 | 检查命令写错,或 start_period 太短 |
| 启动瞬间退出、无日志 | ENTRYPOINT 路径错误,或架构不匹配(arm64 镜像跑在 amd64 上) |
最后一条在 Mac 上构建、Linux 上运行时特别常见。构建时明确指定 --platform linux/amd64。
六、上线检查
- 基础镜像和应用镜像都 pin 了具体版本,没有
latest -
docker history里看不到任何密钥 - 容器以非 root 运行
- 所有服务有 healthcheck 且真的检查依赖
- 所有服务有 memory 和 cpu 限制
- 所有服务配了日志轮转
- 端口没有意外暴露到 0.0.0.0
- 有状态数据在卷里,且备份做过恢复演练
-
.env和证书在.gitignore和.dockerignore里 - 回滚方式明确:上一个镜像标签是什么,怎么切回去