镜像和容器的关系
刚接触 Docker 时,最容易混淆的是"镜像"和"容器"。可以这样理解:镜像是只读的模板,容器是镜像跑起来之后的实例。一个镜像可以启动多个容器,每个容器在镜像之上加一层可写层,删掉容器,这层数据就没了。
日常操作基本围绕这两类对象:拉镜像、看镜像、删镜像;起容器、看日志、进容器、停容器。命令不多,但参数组合很多,踩坑主要集中在这几组参数上。
镜像相关命令
# 拉取指定版本,生产环境不要用 latest
docker pull nginx:1.24
# 查看本地镜像
docker images
# 给镜像打标签,准备推送到私有仓库
docker tag nginx:1.24 registry.example.com/library/nginx:1.24
# 查看镜像的构建历史
docker history nginx:1.24
# 导出和导入,适合离线环境
docker save -o nginx-1.24.tar nginx:1.24
docker load -i nginx-1.24.tar
# 删除镜像,先确认没有容器在使用
docker rmi nginx:1.24
# 清理悬空镜像
docker image prune -f
镜像由一层层只读层叠加而成,docker history 能看到每一层对应的构建指令。层数越多、体积越大,拉取和启动越慢,这也是后面写 Dockerfile 时要合并 RUN 指令、清理缓存的原因。
容器相关命令
最常用的是 docker run,参数决定容器怎么跑:
| 参数 | 作用 | 示例 |
|---|---|---|
-d |
后台运行 | docker run -d ... |
--name |
指定容器名 | --name app |
-p |
端口映射 | -p 8080:8080 |
-v |
目录挂载 | -v /data/logs:/app/logs |
-e |
环境变量 | -e TZ=Asia/Shanghai |
--restart |
重启策略 | --restart unless-stopped |
--network |
指定网络 | --network app-net |
--rm |
退出后自动删除 | 适合一次性任务 |
一个完整的启动示例:
docker run -d --name app \
-p 8080:8080 \
-v /data/app/app.jar:/app/app.jar \
-v /data/app/logs:/app/logs \
-e SPRING_PROFILES_ACTIVE=prod \
-e TZ=Asia/Shanghai \
--restart unless-stopped \
eclipse-temurin:17-jre-jammy \
java -jar /app/app.jar
容器起来之后,常用的排查命令:
docker ps -a # 包括已退出的容器
docker logs -f --tail 200 app # 跟踪日志
docker exec -it app sh # 进入容器
docker inspect -f '{{.State.Status}}' app
docker stats --no-stream # 资源占用
docker stop app && docker rm app # 停止并删除
容器的生命周期是 created → running → paused → exited → removed,退出不等于删除,日志和数据层还在,docker ps -a 能看到,docker rm 才真正清掉。
常见坑
1. 容器启动后立刻退出
docker run 返回了容器 ID,docker ps 却看不到。先看退出码:
docker ps -a
docker logs app
退出码 0 通常是容器里的主进程执行完就结束了,比如跑了一个一次性脚本;退出码 1 多半是命令或配置错误;退出码 137 一般是被 OOM 杀掉或收到 SIGKILL;143 是正常的 SIGTERM 停止。Docker 要求容器有一个前台进程,后台运行的进程撑不住容器。
2. 端口被占用
bind: address already in use 说明宿主机端口被别的进程占了。ss -lntp | grep 8080 找到占用者,换端口或者停掉旧进程。注意 -p 8080:8080 左边是宿主机端口,右边是容器端口,别写反。
3. 挂载目录权限不对
容器里的进程以非 root 用户运行时,往宿主机挂载目录写文件会报 Permission denied。要么调整宿主机目录权限,要么在镜像里创建对应用户并设置 UID。反过来,容器用 root 跑,写出来的文件在宿主机上也是 root 所有,运维清理时很麻烦。
4. 时区是 UTC
大多数基础镜像默认 UTC,日志时间比北京时间少 8 小时。启动时加 -e TZ=Asia/Shanghai,或者在 Dockerfile 里设置。Java 应用还可以加 -Duser.timezone=Asia/Shanghai。
5. 数据没有持久化
把 MySQL、Redis 的数据目录放在容器可写层里,删容器数据就没了。数据库类容器必须挂载数据卷,并且确认备份策略覆盖到卷。
6. 一直用 latest 标签
latest 会随仓库更新而变化,今天拉的和下周拉的可能是两个版本。生产环境固定到具体版本号或 digest。
7. 磁盘被日志和悬空镜像占满
Docker 的日志默认是 json-file 驱动,不限制大小会一直涨。可以在 /etc/docker/daemon.json 里配置 max-size 和 max-file。另外,频繁构建会留下大量悬空镜像,定期执行 docker system df 看看占用,再用 docker system prune 清理。
8. 进了容器改配置,重启就没了
容器可写层的修改不会保留到镜像里,重建容器就丢失。配置应该通过环境变量、挂载文件或者重新构建镜像来管理。
排查步骤
容器异常时,按这个顺序看:
docker ps -a确认状态和退出码;docker logs --tail 200 <容器>看应用日志;docker inspect <容器>看挂载、环境变量、网络配置;docker exec -it <容器> sh进去确认进程、端口、文件是否存在;docker stats看 CPU、内存是否触顶;- 宿主机上
df -h、dmesg | tail看磁盘和 OOM 记录。
大部分"容器起不来"的问题,在前两步就能定位。真正需要进容器排查的,通常是网络和文件权限问题。
评论