Docker 从入门到生产实践:容器边界、Compose 部署与运维清单

一篇面向开发者的 Docker 生产实践参考:镜像、容器、卷、网络、Dockerfile、Compose、健康检查、日志、备份、回滚与安全边界。

Docker 进入生产环境时,最重要的不是多记几个命令,而是把运行边界讲清楚:镜像怎么固定,容器怎么重建,数据放哪,网络怎么连,更新怎么回退。 只要这几件事没有想明白,docker run 再熟练,也只是在更快地制造不稳定。

下面这份内容按“从零到生产”的顺序整理,但不是入门摘要。每一节都尽量给出判断规则、实际命令、配置样例和常见误区。官方事实以 Docker 官方文档构建最佳实践数据卷文档Compose 文件参考 为准。

先把四个对象说清楚

对象 一句话定义 生产判断
镜像 只读模板,里面放的是应用、依赖和默认配置 同一个镜像可以创建多个容器,生产必须固定版本或 digest
容器 镜像运行后的实例,主进程活着它才活着 容器是可删除、可重建的,不应该成为唯一数据仓库
仓库 镜像的分发和拉取地点 只从可信仓库拉取,生产环境要控制权限和来源
Volume 独立于容器生命周期的数据层 数据库、上传文件、缓存目录都要先判断是否需要持久化
虚拟机 有独立内核的完整系统 Docker 常常跑在虚拟机里,二者不是对立关系

容器与虚拟机最常见的混淆点在于“隔离对象”不同。虚拟机隔离的是整台机器,容器隔离的是进程、文件系统、网络和资源配额。 如果你的需求是多操作系统、强隔离或承载传统基础设施,虚拟机更合适;如果目标是把应用交付标准化、让环境可复现、让更新可回退,Docker 更合适。

安装:先验收,再使用

Linux 服务器优先按照 Docker Engine 安装文档 使用官方源安装,不建议长期依赖来源不明的一键脚本。 macOS 和 Windows 一般使用 Docker Desktop;Windows 配合 WSL 2 时,代码和构建上下文放在 Linux 文件系统里,通常更稳。

平台 推荐方式 注意点
Linux Docker Engine + Compose 插件 装完先验收 daemon、CLI、Compose 和 hello-world
macOS Docker Desktop 容器实际运行在轻量 Linux 环境中,不是 macOS 内核
Windows Docker Desktop + WSL 2 项目目录尽量放在 WSL 文件系统中,避免跨文件系统 I/O 惩罚
docker version
docker compose version
sudo docker run --rm hello-world

如果普通用户要直接操作 Docker,常见做法是加入 docker 组,但这件事不能只看方便。 只要一个用户能访问 Docker daemon,通常就已经接近宿主机 root 权限。多人共用服务器、对隔离要求较高的环境,应该更谨慎,必要时研究 Rootless 模式

docker run 的边界:学习用得上,生产不够用

docker run -d \
  --name web-demo \
  -p 8080:80 \
  nginx:alpine

docker ps
docker logs -f --tail 100 web-demo
docker exec -it web-demo sh

这条命令的价值,是让你看懂容器如何被创建、端口如何映射、日志如何查看、进入容器如何排查。它的问题也很明显:服务一多,长命令难以维护;配置一复杂,单条命令根本装不下;要加数据库、卷、健康检查、重启策略和资源限制时,docker run 就开始显得笨重。

动作 要点 常见误区
运行 容器主进程启动后进入运行状态 容器里的守护进程不前台运行,退出终端后就停
停止 先发 SIGTERM,再等待超时后强杀 应用不接收停止信号,导致优雅退出失败
删除 删除的是容器,不是镜像 删容器后误以为镜像和数据一起没了
重建 基于镜像和配置重新创建容器 把手工改动留在容器里,结果下次重建全丢

容器为什么会退出

容器的生命周期由主进程决定。主进程结束,容器就退出。 所以像 echo、脚本执行完毕、初始化命令跑完,这些情况都可能让容器显示为 Exited,并不一定是故障。

docker run --name once ubuntu:24.04 echo "hello"

真正值得警惕的是,应用本来应该持续运行,却在启动脚本结束后跟着退出。 这通常说明:入口命令写错、应用没有以前台方式运行、依赖缺失、配置文件找不到,或者主进程被 OOM 杀死。

镜像、标签和 digest

标签不是版本锁,尤其不要把 latest 当成稳定语义。 生产环境至少固定到明确版本,例如 postgres:17.5。 如果追求完全可复现,可以进一步固定到 digest,例如 postgres:17.5@sha256:...,但升级和排查时要接受维护成本更高这个现实。

策略 优点 代价
标签版本 易读、便于升级 同一标签可能指向新镜像
digest 锁定 内容不可变,重建结果一致 升级时要同步更新 digest
latest 写起来省事 生产不应依赖它

如果你只想快速验证功能,可以用标签;如果你关心回滚、审计和供应链可复现,最好把标签和 digest 都记录下来。

Volume、Bind Mount 和只读挂载

数据库和上传文件不该只放在容器可写层里。容器可删可重建,但用户数据不能跟着重建。 这就是 Volume 出场的原因:卷由 Docker 管理,生命周期独立于容器。

docker volume create postgres-data
docker run -d \
  --name db \
  -e POSTGRES_DB=app \
  -e POSTGRES_USER=app \
  -e POSTGRES_PASSWORD='change-me' \
  --mount type=volume,source=postgres-data,target=/var/lib/postgresql/data \
  postgres:17
方式 适合什么 风险判断
Volume 数据库、业务数据、需要容器重建后仍保留的内容 不是备份,仍需单独导出或快照
Bind Mount 宿主机上直接维护的配置、证书、目录 宿主机路径和权限决定一切,容易被遮蔽或误删
只读挂载 容器只读配置、证书、静态文件 能明显减少容器内部篡改宿主机文件的风险
--mount type=bind,source=/opt/myapp/config,target=/app/config,readonly

Bind Mount 的一个经典坑,是把宿主机空目录挂到容器某个原本已有内容的目录上。 挂载后,容器里原先可见的文件会被遮住,看起来就像“文件不见了”。

网络:localhost 不是万能答案

容器里的 localhost 只指向容器自己,不指向宿主机,也不指向别的容器。 服务之间通信,应该优先使用自定义网络和服务名。

docker network create app-network
docker run -d --name db --network app-network \
  -e POSTGRES_DB=app -e POSTGRES_USER=app -e POSTGRES_PASSWORD='change-me' \
  --mount type=volume,source=postgres-data,target=/var/lib/postgresql/data \
  postgres:17

docker run --rm -it --network app-network postgres:17 psql -h db -U app -d app
场景 正确写法 错误写法
应用连数据库 jdbc:postgresql://db:5432/app jdbc:postgresql://localhost:5432/app
公开 Web 服务 宿主机:容器端口映射后由反向代理接入 把数据库、Redis 之类也直接暴露公网
仅本机访问 127.0.0.1:5432:5432 直接把管理端口绑到 0.0.0.0

容器内网络和宿主机网络不是一回事。外部必须访问的端口才发布;内部组件之间能在同一网络里直接连,就没有必要暴露到公网。

Dockerfile:把自己的项目做成镜像

Dockerfile 的目标不是把构建机里的所有东西打包进来,而是用最少的层,把“能运行的最终产物”做成镜像。 对 Java 项目来说,运行时通常只需要 JRE,不需要完整 JDK。

FROM eclipse-temurin:21-jre

WORKDIR /app

COPY target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

如果项目还要编译,建议用多阶段构建:前一阶段负责 Maven/Gradle 编译,后一阶段只保留运行产物。 这样最终镜像更小,攻击面也更少。

FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /workspace
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 mvn -B dependency:go-offline
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 mvn -B clean package -DskipTests

FROM eclipse-temurin:21-jre
WORKDIR /app
RUN useradd --system --uid 10001 appuser
COPY --from=builder /workspace/target/*.jar app.jar
USER appuser
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

多阶段构建里,先复制 pom.xml 再下载依赖,是为了让依赖缓存更稳定。 只有依赖声明变化时才重新安装依赖,单纯改源码时,缓存层仍然可复用。

.dockerignore 不是装饰品

构建上下文会整体发给构建器。 如果不写 .dockerignore.git、日志、IDE 配置、本地缓存、敏感文件都可能一起进上下文,既慢又危险。

.git
.idea
.vscode
target
*.log
.env
compose.override.yaml

别机械照抄。比如你的 Dockerfile 需要复制本地 target/app.jar,就不能把整个 target 都忽略掉。

分层缓存怎么判断值不值得保留

做法 意义 判断
先放稳定层 基础镜像、依赖安装、公共配置放前面 能让后续源码改动复用缓存
后放易变层 业务代码和资源放后面 减少每次改一行就全量重建
少而清晰 把相近命令合并,但不要过度压缩 可维护性比少几个层更重要

Compose:当应用不再只有一个容器

当项目里开始同时出现应用、数据库、反向代理、监控和缓存时,docker run 就会迅速膨胀成没人愿意维护的长命令。 Compose 的价值,是把这一整套部署写进一个可审查的 YAML。

name: demo-app
services:
  app:
    build:
      context: .
    image: demo-app:1.0.0
    restart: unless-stopped
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/app
      SPRING_DATASOURCE_USERNAME: app
      SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
    ports:
      - "127.0.0.1:8080:8080"
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 40s
    networks:
      - backend

  db:
    image: postgres:17
    restart: unless-stopped
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

volumes:
  postgres-data:

networks:
  backend:

这里最关键的不是 YAML 长什么样,而是部署契约是否成立:服务名就是地址、数据库有卷、容器有健康检查、端口不直接裸奔、重启策略是默认配置的一部分。

depends_on 不等于可用

短格式 depends_on: [db] 只说明启动顺序,不保证数据库已经完成初始化,更不保证应用已能连接。 更稳妥的做法,是把数据库健康检查作为前置条件,并让应用自己具备连接重试。

depends_on:
  db:
    condition: service_healthy

启动顺序解决不了所有时序问题。数据库重启、迁移失败、连接暂时不可用,最后还是要靠应用自身的重试、超时和降级处理。

environment、env_file 和 secrets

非敏感配置放环境变量或 env_file,敏感内容尽量不要写死在镜像和公开的 Compose 文件里。 单机 Compose 的 secrets 更像文件挂载方案,不要把它误解成云平台级的托管密钥系统。

services:
  app:
    env_file:
      - .env
    secrets:
      - db-password

secrets:
  db-password:
    file: ./secrets/db-password.txt

优雅停止、重启策略和资源限制

容器能否优雅停止,直接决定更新时会不会出现半截请求、脏状态和数据库连接泄露。 最基本的要求,是让应用以前台进程方式运行,并正确处理停止信号。

ENTRYPOINT ["java", "-jar", "/app/app.jar"]
ENTRYPOINT java -jar /app/app.jar

JSON 数组形式更利于信号直接传给 Java 进程。 Shell 形式容易把信号留给包装 shell,容器停不下来时,通常就是从这里开始查。

策略 含义 适用
no 默认不自动重启 临时任务、一次性批处理
on-failure 异常退出时重启 容易受瞬时错误影响的服务
always 总是尝试拉起 不太建议无脑默认
unless-stopped 除非人为停止,否则自动重启 单机生产服务最常见
docker run -d --name worker --memory 1g --cpus 1.5 my-worker:1.0.0
docker stats

资源限制不是摆设。一个失控容器可以把整台机器的内存和 CPU 吃光。 Java 应用在容器里还要额外给线程栈、Metaspace、直接内存和系统组件留空间,不能简单把 -Xmx 设成容器上限。

反向代理、日志轮转和数据库备份

生产环境通常不会把应用端口直接暴露到公网,而是让 Nginx、Caddy 或其他反向代理处理域名、HTTPS 和转发。 应用服务只绑定在宿主机回环地址,或者干脆只留给内部网络访问。

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://127.0.0.1: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;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

日志不要无限写。容器默认日志如果没有轮转,磁盘会很快被打满。 需要控制大小时,可以为 JSON 日志设置 max-sizemax-file

services:
  app:
    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "5"

数据库备份要区分“卷存在”与“可恢复”。Volume 让容器重建后数据还在,但并不等于备份。 PostgreSQL 的逻辑备份优先用 pg_dump / pg_restore,并且要定期做恢复演练。

docker compose exec -T db pg_dump -U app -d app -Fc > app-$(date +%F).dump
docker compose exec -T db pg_restore -U app -d app --clean --if-exists < app-2026-07-21.dump

更新、回滚和发布纪律

  1. 先改源码,先跑测试。
  2. 构建新镜像,使用明确版本号或 digest。
  3. 把镜像推送到可信仓库。
  4. 在 Compose 里替换镜像版本。
  5. 执行 `docker compose up -d`,而不是先 `down`。
  6. 观察健康检查、日志和数据库状态。
  7. 一旦异常,立即回退到上一版本镜像。

不要进入正在运行的容器里手工改 JAR、git pull 或临时装依赖。 那些修改不可复现,容器一重建就全部消失。

安全底线

风险动作 为什么危险 默认建议
privileged: true 权限过高,几乎等于把宿主机交出去 默认不要开
挂载 /var/run/docker.sock 拿到 Docker 控制权通常意味着拿到宿主机控制权 只在明确必要时使用
network_mode: host 绕过容器网络隔离,暴露面扩大 生产默认不用
容器 root 运行 一旦被打穿,权限更高 应用尽量用非 root 用户
任意开放端口 数据库、管理面板和调试端口容易被扫到 只发布必要端口,并配合防火墙

安全不是某个开关,而是一组默认动作:尽量使用官方镜像,固定版本,缩小权限,最小端口暴露,宿主机防火墙实际验证,必要时研究 Docker 安全文档Rootless 模式

常见故障排查路线

现象 先看什么 最小修复
容器秒退 docker ps -a / docker logs / docker inspect 修正入口命令、环境变量、文件路径、权限或主进程
端口访问不了 ports、应用监听地址、防火墙、安全组、NAT 确认监听 0.0.0.0,再验证外部网络路径
连不上数据库 localhost、网络、健康检查、用户名密码、卷内已有数据 改成服务名,检查 db 健康和凭据一致性
磁盘突然满了 docker system df / 容器日志 / 镜像缓存 / 未清理卷 先找增长源,再做受控清理
更新后报错 镜像版本、compose config、变更日志 回退到上一版本并保留旧镜像
docker ps -a
docker logs 容器名
docker inspect 容器名
docker inspect -f '{{.State.OOMKilled}}' 容器名
docker system df -v
docker compose config

有些问题非常常见:应用只监听容器内 127.0.0.1,外部当然连不上;Bind Mount 把镜像内文件遮住了,看起来像“文件没了”;数据库卷里已经有旧数据,修改环境变量不会自动重置密码。

Rainyun 的实用推荐卡

如果你需要一台便宜好上手的环境做个人项目、开发测试、轻量站点或者 Docker Compose 入门,可以先看看 雨云。 部分链接可能包含返利,不影响推荐判断。

适合 不适合 判断重点
个人项目、开发测试、轻量站点、Compose 入门 高重要性主业务的唯一兜底 先确认机房、端口、线路、磁盘和备份
NAT 场景下的轻量接入 强依赖固定公网独立 IP 的场景 看你的服务是否需要直接暴露固定地址
短周期验证和快速试错 一上来就把单一云厂商当唯一容灾方案 生产服务先小规格验证,再看真实负载

NAT 与独立 IP 的差异要先想明白。 只需要少量端口、临时服务、演示环境时,NAT 方案也许足够;如果业务需要稳定对外开放、被第三方回调、做白名单联通,独立 IP 更直接。 不管选哪种,生产服务都应该先用小规格、短周期验证机房、端口、线路、磁盘和备份策略,别把单个云厂商当成唯一兜底。

什么时候不该上 Kubernetes

单机服务、少量服务、没有复杂调度需求、团队也不想承担额外运维复杂度时,Compose 往往更合适。 Kubernetes 不是升级版 Docker,也不是所有项目的默认答案。

如果只有这些需求 更合适的选择
一台服务器就够,应用和数据库同机 Docker Compose
服务数量不多,但要稳定、可复现、可回滚 Docker Compose
多节点调度、弹性扩缩容、复杂服务治理 再考虑 Kubernetes

判断标准不是“我想不想学”,而是“当前复杂度是否值得再引入一个编排平台”。 只要 Compose 已经能把边界、数据、网络、日志和回滚管好,就没有理由把复杂度提前抬高。

命令速查

docker ps
docker ps -a
docker logs -f --tail 100 app
docker exec -it app sh
docker image ls
docker image inspect nginx:alpine
docker volume ls
docker network ls
docker compose up -d --build
docker compose ps
docker compose logs -f app
docker compose config
docker compose down

Linux、macOS、Windows 的安装差异

不同平台安装 Docker 的差异,真正影响的是文件系统、虚拟化层和权限模型,而不是命令是否相同。 Linux 上最直接,macOS 和 Windows 往往要借助 Docker Desktop 或 WSL 2。

平台 你真正要关心的点 常见误判
Linux daemon、systemd、用户组、cgroup、iptables 以为装上 docker 就万事大吉
macOS 容器在 Linux 虚拟环境中运行、文件共享性能、端口转发 把容器当成本机原生进程
Windows WSL 2 后端、目录位置、路径分隔、文件 I/O 把大项目放在 Windows 文件系统里直接挂载

如果你在 macOS 或 Windows 上做开发,最重要的是理解:构建和运行并不等于直接使用宿主机内核。 这会影响网络、磁盘性能、文件权限和调试方式。

安装完成后的三项核对

  1. docker version 和 docker compose version 都能返回版本号。
  2. hello-world 能正常拉取并运行。
  3. 普通用户是否真的有权访问 Docker daemon,要按安全要求判断,而不是默认放开。

很多服务器问题不是 Docker 版本太低,而是安装后没做最小验收。 只要 daemon 没真正起来,后面所有镜像、网络和卷的判断都没有意义。

把 Docker 作为交付边界,而不是开发环境玩具

生产上 Docker 最值钱的地方,是把交付边界固定下来。 你的镜像里装了什么,容器怎样启动,数据写到哪里,端口怎样暴露,更新怎样回退,这些都要写成可复查的文件,而不是口头约定。

边界项 为什么要明确 推荐做法
启动命令 决定主进程与退出行为 使用 ENTRYPOINT JSON 形式
配置 决定环境差异是否可控 环境变量 + env_file + 必要 secrets
数据 决定重建后是否丢失 数据库和持久文件统一放到卷
入口 决定公网暴露范围 应用只绑本机回环,入口由反向代理承接
发布 决定是否能回退 保留上一镜像版本,不在容器内手工改动

更完整的更新和回退方法

更新不是把新镜像拉下来这么简单。 你应该把每次上线看成一次可回滚发布:先有新版本,再确认旧版本仍在,再做切换,最后观察健康状态。

  1. 构建并标记新镜像,最好带语义化版本。
  2. 推送到可信仓库,记录 digest。
  3. 在 Compose 中只替换镜像版本,不改动无关配置。
  4. 执行 docker compose up -d,而不是先 down。
  5. 观察健康检查、日志、数据库连接和反向代理状态。
  6. 若异常,立即切回上一版镜像与配置。
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail 200 app
docker image ls

回退时要避免两个错误:一是把旧容器删干净后才发现新版本有问题,二是只回退应用镜像却忘了数据库迁移。 生产里真正稳妥的回退,应该连数据变更一起考虑。

网络、反向代理和防火墙要一起看

很多“Docker 端口访问不了”的问题,其实和 Docker 本身关系不大。 应用监听地址、宿主机防火墙、云安全组、NAT 端口映射、反向代理配置,任何一层错了都可能导致外部不可达。

检查层 典型命令 判断
容器 docker ps 端口映射是否存在
宿主机 ss -lntp / ufw status 端口是否真的监听和放行
云侧 安全组控制台 有没有放行对应端口
应用 curl localhost 应用是否只监听 127.0.0.1
代理 nginx/caddy 配置 是否把请求转到了正确服务

为什么很多项目最后还是适合 Compose

不是所有服务都需要 Kubernetes。 对很多单机项目、内容站、内部工具和中小型业务来说,Compose 已经能把镜像、卷、网络、日志和健康检查管得很清楚。

复杂度 更合适的工具
单机部署、少量服务、人工可接受的发布频率 Docker Compose
多节点调度、弹性扩缩容、跨主机服务治理 Kubernetes
想“先试试容器”但还没真正遇到多节点问题 先别上 K8s

判断标准不是“技术栈更高级”,而是“现在的复杂度是否真的逼你付出编排平台的额外成本”。

上手时值得反复检查的命令

docker ps
docker ps -a
docker logs -f --tail 100 app
docker inspect app
docker volume ls
docker volume inspect postgres-data
docker network ls
docker compose config
docker compose ps
docker system df -v

这些命令不是背诵题,而是生产排查时最先想到的几个观察窗口。 先看状态,再看日志,再看 inspect,最后才是修改配置。

官方链接清单