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-size 和 max-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
更新、回滚和发布纪律
- 先改源码,先跑测试。
- 构建新镜像,使用明确版本号或 digest。
- 把镜像推送到可信仓库。
- 在 Compose 里替换镜像版本。
- 执行 `docker compose up -d`,而不是先 `down`。
- 观察健康检查、日志和数据库状态。
- 一旦异常,立即回退到上一版本镜像。
不要进入正在运行的容器里手工改 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 上做开发,最重要的是理解:构建和运行并不等于直接使用宿主机内核。 这会影响网络、磁盘性能、文件权限和调试方式。
安装完成后的三项核对
- docker version 和 docker compose version 都能返回版本号。
- hello-world 能正常拉取并运行。
- 普通用户是否真的有权访问 Docker daemon,要按安全要求判断,而不是默认放开。
很多服务器问题不是 Docker 版本太低,而是安装后没做最小验收。 只要 daemon 没真正起来,后面所有镜像、网络和卷的判断都没有意义。
把 Docker 作为交付边界,而不是开发环境玩具
生产上 Docker 最值钱的地方,是把交付边界固定下来。 你的镜像里装了什么,容器怎样启动,数据写到哪里,端口怎样暴露,更新怎样回退,这些都要写成可复查的文件,而不是口头约定。
| 边界项 | 为什么要明确 | 推荐做法 |
|---|---|---|
| 启动命令 | 决定主进程与退出行为 | 使用 ENTRYPOINT JSON 形式 |
| 配置 | 决定环境差异是否可控 | 环境变量 + env_file + 必要 secrets |
| 数据 | 决定重建后是否丢失 | 数据库和持久文件统一放到卷 |
| 入口 | 决定公网暴露范围 | 应用只绑本机回环,入口由反向代理承接 |
| 发布 | 决定是否能回退 | 保留上一镜像版本,不在容器内手工改动 |
更完整的更新和回退方法
更新不是把新镜像拉下来这么简单。 你应该把每次上线看成一次可回滚发布:先有新版本,再确认旧版本仍在,再做切换,最后观察健康状态。
- 构建并标记新镜像,最好带语义化版本。
- 推送到可信仓库,记录 digest。
- 在 Compose 中只替换镜像版本,不改动无关配置。
- 执行 docker compose up -d,而不是先 down。
- 观察健康检查、日志、数据库连接和反向代理状态。
- 若异常,立即切回上一版镜像与配置。
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,最后才是修改配置。