外观
镜像与容器:run/exec/logs 的高频组合
docker run 有五十多个参数,日常只用十二个。这节把常用组合和退出码讲清楚——后者是排障的第一手证据。
场景
拿到一个镜像要跑起来:定端口、挂目录、传环境变量、限资源、配重启策略。跑起来之后要能看日志、进容器、查状态。
一、最小可用
bash
docker run -d --name app -p 8080:8080 myapp:1.0.0
docker pstext
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a3f8c2e91b04 myapp:1.0.0 "java -jar /app/..." 5 seconds ago Up 4 seconds 0.0.0.0:8080->8080/tcp app-d 后台运行。不加 -d 会占住当前终端,Ctrl+C 容器就停——这是"容器起来就退出"之外第二常见的误操作。
二、生产常用组合
bash
docker run -d \
--name order-api \
--restart unless-stopped \
-p 127.0.0.1:8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
-e JAVA_OPTS='-Xms1g -Xmx1g -XX:+UseG1GC' \
-v /data/order-api/logs:/app/logs \
-v /data/order-api/config:/app/config:ro \
--memory 2g --memory-swap 2g --cpus 2 \
--log-opt max-size=50m --log-opt max-file=3 \
myapp:1.0.0| 参数 | 作用 | 说明 |
|---|---|---|
--restart unless-stopped | 崩溃/重启后自动拉起 | 手动 stop 的不拉起 |
-p 127.0.0.1:8080:8080 | 只监听本机 | 前面有 Nginx 时必写,避免绕过代理直连 |
-e KEY=VALUE | 环境变量 | 密码别写在这里,见下方 warning |
-v 宿主:容器:ro | 挂载,只读 | 配置文件用 ro,容器改不坏宿主机 |
--memory | 内存上限 | 超过会被 OOMKilled |
--memory-swap | 内存+swap 总上限 | 等于 --memory 时禁用 swap |
--cpus 2 | CPU 配额 | 等价 2 核,可写小数 |
--log-opt | 单容器日志轮转 | 覆盖 daemon 全局设置 |
密码不要用 -e 传
docker inspect order-api 会把环境变量原样打出来,ps -ef 也能看到部分。密码用:
- 文件挂载:
-v /etc/app/secret:/app/secret:ro - Swarm/K8s 的 secret 机制
- 应用启动时从配置中心拉
-e 只适合非敏感配置(profile、日志级别、时区)。
重启策略对比:
| 策略 | 容器退出后 | daemon 重启后 | 手动 stop 后 |
|---|---|---|---|
no(默认) | 不重启 | 不重启 | 不重启 |
on-failure[:N] | 非 0 退出才重启,最多 N 次 | 不重启 | 不重启 |
always | 总是重启 | 重启 | 重启(坑) |
unless-stopped | 总是重启 | 重启 | 不重启 |
生产用 unless-stopped。always 的问题是:你手动 docker stop 后重启 daemon,它自己又起来了,排查问题时很迷惑。
三、退出码是最好的线索
bash
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'text
NAMES STATUS IMAGE
order-api Up 3 hours myapp:1.0.0
old-worker Exited (137) 2 hours ago worker:0.9.1
batch-job Exited (0) 2 hours ago batch:1.2.0
broken Exited (127) 10 minutes ago test:latest| 退出码 | 含义 | 常见原因 |
|---|---|---|
0 | 正常结束 | 一次性任务跑完(正常) |
1 | 应用自身报错 | 配置错误、启动异常 |
125 | Docker 本身出错 | docker run 参数写错 |
126 | 命令不可执行 | 脚本没执行权限,或解释器不对 |
127 | 命令找不到 | ENTRYPOINT/CMD 写错,镜像里没这个二进制 |
137 | 收到 SIGKILL(128+9) | 被 OOM killer 杀,或 docker kill |
139 | 段错误(128+11) | 应用崩溃(JNI/本地库问题) |
143 | 收到 SIGTERM(128+15) | 正常的 docker stop 或 K8s 驱逐 |
137 是最需要警惕的。确认是否 OOM:
bash
docker inspect -f '{{.State.OOMKilled}} {{.State.ExitCode}} {{.HostConfig.Memory}}' old-workertext
true 137 1073741824OOMKilled=true + 上限 1GB → 内存不够。
bash
dmesg -T | grep -i 'oom.*docker\|killed process'text
[Tue Oct 27 18:22:41 2026] Memory cgroup out of memory: Killed process 28441 (java) total-vm:2143212kB, anon-rss:1048420kB对策:调大 --memory,或用 -XX:MaxRAMPercentage=70 让 JVM 自己收敛到容器上限的 70%。
四、看日志
bash
docker logs --tail 200 -f --timestamps order-api
docker logs --since 30m --until 10m order-api
docker logs 2>&1 | grep -i -E 'error|exception' | tail -20text
2026-10-27T20:41:02.114Z INFO [main] Started OrderApiApplication in 8.421 seconds
2026-10-27T20:41:05.882Z INFO [http-nio-8080-exec-1] GET /health 200 3ms--timestamps加时间戳,容器时区不对时尤其有用。docker logs只输出 stdout/stderr。应用如果把日志写到文件,docker logs是空的——要么改成输出到 stdout,要么docker exec进去看文件。
日志撑爆磁盘的排查:
bash
du -sh /data/docker/containers/*/*-json.log | sort -h | tail -5text
1.2G /data/docker/containers/a3f8c2e91b04.../a3f8c2e91b04-json.log当场清空(不用重启容器):
bash
truncate -s 0 /data/docker/containers/a3f8c2e91b04.../a3f8c2e91b04-json.logtruncate 是唯一安全的清理方式
rm 掉 json.log 后,Docker 仍持有该文件的句柄,磁盘空间不会释放(du 显示已删但 df 不减)。必须 truncate。
五、进容器
bash
# 交互式 shell
docker exec -it order-api sh
# 用 root 进(镜像默认非 root 时)
docker exec -it -u root order-api sh
# 执行单条命令
docker exec order-api sh -c 'ls -l /app && echo $JAVA_OPTS'
# 看进程
docker exec order-api ps -ef
docker top order-apitext
PID USER TIME COMMAND
28441 app 0:42 java -jar /app/app.jar解释:
docker exec起的是新进程,它和主进程共享命名空间,但生命周期独立(退出不影响容器)。docker top显示的是容器内视角的进程(PID 1 是主进程)。- 镜像基于 distroless/scratch 时没有 shell,
docker exec sh会报executable file not found。此时用:
bash
docker inspect -f '{{.Config.Entrypoint}} {{.Config.Cmd}}' order-api
docker cp order-api:/app/config/app.yml ./app.yml # 拷出来看
docker run --rm --pid=container:order-api --network=container:order-api \
-it nicolaka/netshoot sh # 借一个带工具的容器进去--pid=container:xxx + --network=container:xxx 让临时容器共享目标容器的进程和网络命名空间,等于"带工具箱进去看"。
六、查状态
bash
docker inspect -f '{{.State.Status}} {{.State.ExitCode}} {{.State.OOMKilled}} {{.RestartCount}} {{.State.StartedAt}}' order-apitext
running 0 false 0 2026-10-27T20:41:02.114Z常用格式化查询:
bash
# IP
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' order-api
# 环境变量
docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' order-api
# 挂载
docker inspect -f '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}} ({{.RW}}){{println}}{{end}}' order-api
# 资源限制
docker inspect -f '{{.HostConfig.Memory}} {{.HostConfig.NanoCpus}} {{.HostConfig.RestartPolicy.Name}}' order-apitext
172.18.0.4
SPRING_PROFILES_ACTIVE=prod
JAVA_OPTS=-Xms1g -Xmx1g -XX:+UseG1GC
PATH=/usr/local/sbin:...
bind /data/order-api/logs -> /app/logs (true)
bind /data/order-api/config -> /app/config (false)
2147483648 2000000000 unless-stopped实时资源:
bash
docker stats --no-streamtext
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
a3f8c2e91b04 order-api 12.41% 1.312GiB / 2GiB 65.60% 1.24GB / 892MB 0B / 12.4MB 48MEM USAGE / LIMIT 接近 100% 就是在 OOM 边缘。JVM 应用要留 30% 给堆外。
七、镜像操作
bash
docker images
docker history myapp:1.0.0 --no-trunc | head -10
docker image inspect myapp:1.0.0 -f '{{.Size}} {{.Config.User}} {{json .Config.ExposedPorts}}'text
REPOSITORY TAG IMAGE ID CREATED SIZE
myapp 1.0.0 7d3c1e9a2b44 2 hours ago 312MB
myapp latest 7d3c1e9a2b44 2 hours ago 312MB
nginx alpine a8b1c2d3e4f5 2 weeks ago 42.6MB
IMAGE CREATED CREATED BY SIZE
7d3c1e9a2b44 2 hours ago CMD ["java" "-jar" "/app/app.jar"] 0B
<missing> 2 hours ago ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] 4.1kB
<missing> 2 hours ago COPY /app/app.jar /app/app.jar 98MB
<missing> 2 hours ago RUN /bin/sh -c apt-get update && apt-get ... 42MBdocker history 每行是一层。<missing> 是中间层(被后续层覆盖或来自其他镜像)。
清理:
bash
docker image prune # 悬空镜像
docker image prune -a # 所有没被容器使用的镜像
docker rmi myapp:0.9.1
docker rmi -f $(docker images -f 'dangling=true' -q)八、拷贝与快照
bash
# 容器 <-> 宿主机
docker cp order-api:/app/logs/app.log ./app.log
docker cp ./app.yml order-api:/app/config/app.yml
# 注意:修改容器内文件后 reinspect 才能看到差异
docker diff order-apitext
C /app/config
A /app/config/app.yml
D /app/logs/old.logA = 新增,C = 修改,D = 删除。
不要用 docker commit 做镜像
docker commit 会把容器当前状态打成一个新镜像,看起来很方便,但:
- 无法复现(不知道之前执行了什么)。
- 镜像只增不减(历史层全带着)。
- 没有构建记录、没有版本管理。
临时验证可以,绝不能用于发布。要改镜像就改 Dockerfile。
九、容器生命周期
bash
docker stop order-api # SIGTERM,默认等 10 秒后 SIGKILL
docker stop -t 30 order-api
docker kill order-api # 直接 SIGKILL(= 退出码 137)
docker restart order-api
docker rm order-api # 只能删已停止的
docker rm -f order-api # 强制删(先 kill)
docker rm -f $(docker ps -aq) # 删所有容器,危险docker stop 的 10 秒默认值对 Java 应用往往不够(要等 Spring 优雅停机)。两个办法:
bash
docker stop -t 30 order-apidockerfile
STOPSIGNAL SIGTERM应用侧配合:
yaml
spring:
lifecycle:
timeout-per-shutdown-phase: 25s
server:
shutdown: graceful十、踩过的坑
| 现象 | 根因 | 处置 |
|---|---|---|
容器刚起就 Exited (0) | 主进程是后台任务/脚本执行完就退 | 前台运行,或 tail -f /dev/null 临时占位 |
Exited (127) | CMD 里的命令不存在 | docker inspect 看 Entrypoint/Cmd,确认镜像内容 |
Exited (137) | OOM 或 docker kill | 查 OOMKilled 与 dmesg |
docker logs 空白 | 应用写文件不写 stdout | 改日志配置,或 exec 进去看 |
| 端口冲突 | 宿主端口被占 | ss -lntp | grep 8080 |
stop 要等 10 秒才停 | 应用不响应 SIGTERM | 加 -t 30,并修优雅停机 |
| 重启 daemon 后容器全没了 | 没开 live-restore | 开 live-restore |
| 日志把磁盘写满 | 没配轮转 | truncate 清空 + 配 max-size |