Docker

本文范围
这篇只讲 Docker 自身这条线:Docker Engine 安装、daemon 配置、镜像、容器、Dockerfile、Compose、网络、数据卷、日志、资源限制、私有仓库接入、离线镜像迁移、底层机制和生产排障。
不展开 K8S、Harbor、Nginx、Jenkins 的完整治理。它们会在后续文章里单独讲,这里只保留连接点:镜像从哪里来、怎么打 tag、怎么推送、怎么在另一台机器复现。
读完以后,你应该能完成三件事:
- 在 Linux 服务器或 Windows 开发机上把 Docker 跑起来。
- 用 Dockerfile 和 Compose 交付一个真实服务。
- 遇到拉镜像失败、容器秒退、端口不通、日志爆盘、OOM、磁盘增长时,能按命令往下查。
前置条件
建议先准备一台 Linux 服务器和一台本地开发机。服务器建议 2 核 4GB 起步,生产环境按业务容器、日志、镜像缓存和数据卷容量单独估算。
需要提前确认这些信息:
| 项目 | 要求 |
|---|---|
| 操作系统 | Ubuntu 22.04/24.04/26.04 LTS、CentOS Stream、Rocky Linux、AlmaLinux 或同类发行版 |
| 权限 | 能使用 sudo,能修改 systemd、/etc/docker/daemon.json、防火墙和数据目录权限 |
| 网络 | 能访问官方源、企业代理、云厂商仓库或内网 Harbor;公网不稳定时要准备离线包 |
| 存储 | 明确 Docker data-root 放在哪里,别让镜像、容器可写层和日志挤满系统盘 |
| 版本 | 生产环境固定 Docker Engine、Buildx、Compose 插件版本;升级前先查官方安装页和 release notes |
官方 Docker Ubuntu 安装文档已经明确提醒:Docker 暴露端口可能绕过 ufw 或 firewalld 的直观规则,生产上要把防火墙策略放到 DOCKER-USER 链一起治理。这个细节很容易被忽略,后面排查端口和安全边界时会反复用到。
很多人第一次用 Docker,最直观的感受是:命令一敲,服务就起来了。
但真正到项目里,问题马上就来了:为什么我本地能跑,服务器跑不起来?为什么容器删了数据也没了?为什么镜像越打越大?为什么 docker logs 把磁盘打满?为什么同一个 Compose 文件,在公司内网拉不下来镜像?
所以学 Docker,不能只背几个命令。我们要把一条完整链路走通:先安装,再拉镜像,再跑容器,再自己构建镜像,再用 Compose 编排多个服务,再推到仓库,最后知道线上出问题该从哪里查。
下面就按这个顺序来。每一步都给命令,也讲命令解决什么问题、怎么看结果、失败时先查哪里。
先把 Docker 的几个对象认清楚
先别急着敲命令,先把 Docker 里几个名字对上。
- Docker Client:你在终端里敲的
docker命令。 - Docker daemon:宿主机上的 Docker 后台服务,真正负责创建镜像、启动容器、管理网络和数据卷。
- Image:镜像,可以理解成应用运行环境的只读模板。
- Container:容器,镜像真正跑起来之后的进程实例。
- Registry:镜像仓库,负责保存和分发镜像。
镜像和容器的关系,可以先粗略理解成“类”和“对象”:镜像是模板,容器是运行实例。一个镜像可以启动多个容器,每个容器都有自己的运行状态。
仓库也可以类比 Maven 仓库。仓库里有很多镜像,镜像有很多 tag。这里要注意,latest 不是“最新版稳定版”,它只是一个可以移动的标签。生产环境不要只写 latest,至少要固定到明确版本,关键服务还要记录 digest。
这几个对象在设计评审时最好画出来看一遍:谁负责构建,谁负责分发,容器运行后网络、数据、日志分别落在哪里。下面这张图不是为了背概念,而是为了把上线检查入口也放进去。
用这张图做上线检查时,至少要问四个问题:镜像是否固定到 tag 和 digest,容器是否能用 docker inspect 看清 Mounts、NetworkSettings 和 LogPath,日志驱动有没有轮转,docker system df 里镜像、容器、volume、build cache 是否有容量预算。Compose 只是把多容器运行关系写成 YAML,最终还是通过 Docker daemon 管理网络、数据卷和容器生命周期;不要把 Compose 当成 K8S 那种多节点编排系统。
先用一组最小命令感受一下:
docker pull nginx:1.27
docker run -d --name nginx-demo -p 8080:80 nginx:1.27
docker ps
docker logs nginx-demo这几条命令做了四件事:
docker pull:从镜像仓库拉取nginx:1.27。docker run:用这个镜像创建并启动一个容器。-d:后台运行。-p 8080:80:把宿主机 8080 端口映射到容器 80 端口。
运行后访问:
http://localhost:8080如果能看到 Nginx 默认欢迎页,说明镜像、容器、端口映射这条链路是通的。
看完以后清理掉:
docker stop nginx-demo
docker rm nginx-demo这里要注意,stop 只是停止容器,rm 才是删除容器。容器删掉以后,镜像还在。可以用下面命令查看:
docker images nginx如果这里能看到 nginx,说明镜像仍然留在本机,下次再启动同版本容器就不需要重新下载。
Windows 上先把 Docker Desktop 跑通
开发机是 Windows 的同学,一般用 Docker Desktop + WSL2。它适合本地开发、联调、构建镜像和验证容器启动,不建议把 Windows Docker Desktop 当生产部署方案。
先检查 WSL:
wsl --status
wsl -l -v这里重点看两件事:
- WSL 是否安装成功。
- 发行版后面的 VERSION 是否为
2。
如果没有安装 WSL,可以在管理员 PowerShell 里执行:
wsl --install安装完成一般需要重启。重启后再看:
wsl -l -v如果某个发行版不是 WSL2,可以这样切换:
wsl --set-version Ubuntu 2
wsl --set-default-version 2当前 Docker Desktop for Windows 主线要求里,一般需要 Windows 10 22H2 build 19045 或 Windows 11 23H2 build 22631 以上版本,并开启硬件虚拟化。实际安装前最好再看一次 Docker Desktop 官方安装页,因为 Windows 版本和 WSL 版本要求会更新。
Docker Desktop 安装好以后,打开 PowerShell 验证:
docker version
docker info
docker compose version
docker run --rm hello-world怎么看结果?
docker version里要同时有 Client 和 Server。docker info能看到 Storage Driver、Logging Driver、Operating System 等信息。docker compose version能看到 Compose v2 版本。hello-world能正常输出说明容器能启动。
如果 docker version 只有 Client,没有 Server,通常是 Docker Desktop 没启动成功,或者 WSL 后端异常。先执行:
wsl --shutdown然后重新启动 Docker Desktop。
Windows 路径挂载怎么写
本地开发经常要把当前目录挂进容器。PowerShell 里可以这样写:
docker run --rm -it -v ${PWD}:/app -w /app eclipse-temurin:17-jre sh这条命令的意思是:
-v ${PWD}:/app:把当前目录挂载到容器的/app。-w /app:进入容器后默认工作目录就是/app。--rm:容器退出后自动删除,适合临时测试。
如果要写显式 Windows 路径,可以这样:
docker run --rm -it -v C:\work\demo:/app -w /app eclipse-temurin:17-jre sh进去以后执行:
ls -la应该能看到宿主机目录里的文件。
这里要注意四个坑:
- 容器内部路径使用 Linux 风格,比如
/app/app.jar,不要写成 Windows 路径。 - 脚本在 Windows 里编辑后放进 Linux 容器,可能因为 CRLF 换行报错。
- 大量小文件放在 Windows 文件系统里再挂进 WSL2 容器,性能可能不如放在 WSL2 Linux 文件系统里。
- 挂载目录不存在时,有些场景会被自动创建成空目录,结果你以为挂进去了,其实文件没在里面。
遇到脚本报错:
bad interpreter
$'\r': command not found优先检查换行符,把脚本转成 LF。
Linux 服务器安装 Docker
生产环境一般用 Linux Docker Engine。这里讲 Ubuntu 和 CentOS Stream 两条主线。以前很多环境会沿用已经停止维护的系统版本和固定旧 Docker 版本,新环境不要再按这条线走,先以当前官方支持版本为准。
安装前先确认系统版本:
cat /etc/os-release
uname -m这里看两件事:
- 系统版本是否还在官方支持范围内。
- 架构是
x86_64、aarch64还是别的。
很多安装失败不是 Docker 命令错了,而是系统太旧、架构不匹配、内网访问不到官方仓库。
Ubuntu 安装
先卸载可能冲突的旧包:
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do
sudo apt-get remove -y "$pkg"
done这一步解决的是“系统源里旧 Docker 包”和 Docker 官方包冲突的问题。没装过旧包也没关系,命令会提示未安装或跳过。
安装基础依赖:
sudo apt-get update
sudo apt-get install -y ca-certificates curl创建 keyring 目录并导入 Docker 官方 GPG key:
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc配置 Docker 官方 apt 仓库:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null更新索引并安装:
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin这里安装的不只是 Docker Engine,还包括 CLI、containerd、Buildx 和 Compose v2 插件。现在主线命令是 docker compose,不是把旧的 docker-compose 当成主入口。
启动并设置开机自启:
sudo systemctl enable --now docker验证:
sudo docker version
sudo docker info
sudo docker run --rm hello-world
docker compose version如果 docker version 能看到 Client 和 Server,说明 Docker 服务起来了。如果 hello-world 能输出说明容器能创建、能启动、能连到镜像来源。
失败时先看日志:
sudo systemctl status docker
sudo journalctl -u docker -n 100 --no-pager一般先看三个点:
- 配置文件 JSON 有没有写错。
- Docker 数据目录权限是否异常。
- 机器是否访问不到 Docker 官方仓库或镜像仓库。
CentOS Stream 安装
当前 Docker 官方 CentOS 安装主线是 CentOS Stream 9 和 CentOS Stream 10。先卸载旧包:
sudo dnf remove -y docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine安装仓库管理工具:
sudo dnf install -y dnf-plugins-core添加 Docker 官方仓库:
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo安装 Docker:
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin启动并验证:
sudo systemctl enable --now docker
sudo docker run --rm hello-world
docker compose version如果 dnf install 卡在下载元数据,先不要怀疑 Docker 本身,先确认服务器能否访问对应源:
curl -I https://download.docker.com/linux/centos/docker-ce.repo内网机器访问不到很常见,后面会专门讲国内、公网不稳定和离线环境。
非 root 用户执行 docker
刚安装完 Docker,普通用户执行:
docker ps可能会看到权限错误。一般做法是把当前用户加入 docker 组:
sudo usermod -aG docker $USER然后重新登录 SSH,或者临时执行:
newgrp docker再验证:
docker ps这里要注意,加入 docker 组基本等同于给了这个用户很高的宿主机权限。生产服务器上不要随便把普通账号都加进去,至少要控制登录用户、审计操作记录。
国内、公网不稳定、企业内网怎么处理
很多 Docker 教程默认可以直连 Docker Hub、GitHub、Docker 官方安装源。真实项目里,尤其是国内公网、企业内网、离线机房,这个默认经常不成立。
所以部署前先做一个简单判断:
docker pull nginx:1.27如果能拉下来,再看镜像:
docker images nginx如果失败,不要只盯着 Docker。先分清楚是哪一类问题:
- DNS 解析失败。
- TCP 连接超时。
- TLS 证书错误。
- 认证失败。
- 镜像名或 tag 不存在。
- 公司代理没有配置。
可以先用:
docker pull hello-world
docker pull nginx:1.27如果 hello-world 能拉、nginx:1.27 拉不了,再检查镜像名和 tag。如果都拉不了,优先看网络、代理、镜像源。
配置 registry mirrors
如果你所在环境提供了可用的镜像加速地址,可以写到 Docker daemon 配置里。
先确认配置目录:
sudo mkdir -p /etc/docker编辑配置:
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"registry-mirrors": [
"https://your-mirror.example.com"
]
}
EOF这里要注意,地址必须换成你自己账号、企业或云厂商提供的可用地址,不要复制别人的旧加速器地址。很多旧地址已经失效,或者只对特定账号可用。
重启 Docker:
sudo systemctl daemon-reload
sudo systemctl restart docker验证配置是否生效:
docker info在输出里找 Registry Mirrors。如果能看到你配置的地址,说明 Docker daemon 已经读到配置。
如果 Docker 重启失败,第一时间检查 JSON:
sudo dockerd --validate --config-file /etc/docker/daemon.json或者看服务日志:
sudo journalctl -u docker -n 100 --no-pager最常见错误是 JSON 最后一项多了逗号、引号不成对、复制了不可见字符。
Docker Engine 安装源访问不到
镜像拉取慢是一类问题,Docker Engine 安装源访问不到是另一类问题。
先验证官方源是否可达:
curl -I https://download.docker.com/linux/ubuntu/
curl -I https://download.docker.com/linux/centos/如果公司网络访问不到,一般有三种办法:
- 使用公司批准的软件源代理。
- 在可联网机器上下载离线安装包,再带到目标机器。
- 使用云厂商或企业内部维护的 Docker 安装源。
生产建议很简单:不要到现场才临时找源。交付前要把安装源、镜像源、代理、离线包和校验方式都准备好。
企业代理
如果服务器访问外网必须走代理,需要给 Docker daemon 配置 systemd 代理。
创建目录:
sudo mkdir -p /etc/systemd/system/docker.service.d写入代理配置:
sudo tee /etc/systemd/system/docker.service.d/http-proxy.conf > /dev/null <<'EOF'
[Service]
Environment="HTTP_PROXY=http://proxy.example.com:8080"
Environment="HTTPS_PROXY=http://proxy.example.com:8080"
Environment="NO_PROXY=localhost,127.0.0.1,.example.local,registry.example.local"
EOF重载并重启:
sudo systemctl daemon-reload
sudo systemctl restart docker验证 systemd 是否读到环境变量:
systemctl show --property=Environment docker如果代理配置正确,再测试拉取镜像:
docker pull nginx:1.27这里要注意,NO_PROXY 很重要。内网私有仓库、Harbor、公司域名如果不放进 NO_PROXY,请求可能绕到代理上,反而访问失败。
云厂商镜像仓库
实际项目里,不建议生产环境直接依赖 Docker Hub。更常见的做法是把基础镜像和业务镜像同步到云厂商镜像仓库或企业私有仓库。
登录仓库一般是这个结构:
docker login registry.example.com登录成功后会看到类似:
Login Succeeded然后给镜像重新打 tag:
docker tag app:1.0.0 registry.example.com/team/app:1.0.0推送:
docker push registry.example.com/team/app:1.0.0怎么看结果?
- push 输出里每一层都会显示 pushed 或 already exists。
- 仓库控制台里能看到镜像和 tag。
- 另一台机器能
docker pull registry.example.com/team/app:1.0.0。
生产建议:临时登录口令、长期访问凭证、机器人账号要分开管理。不要把 docker login 的密码写进脚本仓库。
Harbor 或企业私有仓库
如果公司自建 Harbor,Docker 客户端默认会先按 HTTPS 访问仓库。生产建议优先使用正式证书或企业 CA。
如果 Harbor 使用自签名证书,把 CA 放到客户端:
sudo mkdir -p /etc/docker/certs.d/registry.example.local
sudo cp ca.crt /etc/docker/certs.d/registry.example.local/ca.crt
sudo systemctl restart docker然后登录:
docker login registry.example.local如果只是测试环境临时使用 HTTP,需要配置 insecure registry:
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"insecure-registries": [
"registry.example.local"
]
}
EOF重启:
sudo systemctl daemon-reload
sudo systemctl restart docker这里要注意,insecure-registries 只适合受控内网测试环境。生产环境不要长期依赖 HTTP。
离线环境怎么迁移镜像
离线环境不能现场 docker pull,那就提前在可联网机器上把镜像准备好。
先拉取镜像:
docker pull nginx:1.27
docker pull redis:7.4保存成 tar 包:
docker save nginx:1.27 redis:7.4 -o base-images.tar生成校验文件:
sha256sum base-images.tar > base-images.tar.sha256把 base-images.tar 和 base-images.tar.sha256 拷贝到离线机器后,先校验:
sha256sum -c base-images.tar.sha256看到 OK 再导入:
docker load -i base-images.tar验证:
docker images如果要推到离线 Harbor:
docker tag nginx:1.27 registry.example.local/base/nginx:1.27
docker push registry.example.local/base/nginx:1.27生产建议:离线交付不要只给 tar 包,还要给镜像清单,至少包含镜像名、tag、digest、tar 包 sha256、制作日期、适用架构。
Docker daemon 基础配置
Docker 的全局配置主要在:
/etc/docker/daemon.json一般生产服务器至少会关心四类配置:
- 镜像仓库和加速地址。
- Docker 数据目录。
- 日志驱动和日志轮转。
- 私有仓库证书或 insecure registry。
一个示例配置:
{
"data-root": "/data/docker",
"log-driver": "local",
"log-opts": {
"max-size": "100m",
"max-file": "5"
},
"registry-mirrors": [
"https://your-mirror.example.com"
]
}这里逐个解释:
data-root:Docker 镜像、容器、卷等数据放在哪里。生产服务器通常不要把大量容器数据都放在系统盘。log-driver:日志驱动。local默认带日志轮转,更适合避免磁盘被打满。log-opts:日志大小和文件个数限制。registry-mirrors:镜像加速地址。
改完以后先验证 JSON:
sudo dockerd --validate --config-file /etc/docker/daemon.json再重启:
sudo systemctl daemon-reload
sudo systemctl restart docker查看是否生效:
docker info这里要注意,修改 data-root 前,如果机器上已经有容器和镜像,要先规划迁移,不能随手一改就重启。否则 Docker 会去新目录找数据,看起来就像镜像和容器“都没了”。
镜像常用命令
先看本地有哪些镜像:
docker images输出里常见列:
REPOSITORY:镜像仓库名。TAG:标签。IMAGE ID:镜像 ID。CREATED:创建时间。SIZE:镜像大小。
拉镜像:
docker pull nginx:1.27拉完看详情:
docker image inspect nginx:1.27如果只想看 digest:
docker image inspect nginx:1.27 --format '{{json .RepoDigests}}'为什么要看 digest?因为 tag 可能移动,digest 才能精确指向某一次镜像内容。生产发布记录里建议保存 digest。
给镜像打 tag:
docker tag nginx:1.27 registry.example.local/base/nginx:1.27删除镜像:
docker rmi nginx:1.27如果提示镜像正在被容器使用,先查容器:
docker ps -a --filter ancestor=nginx:1.27删掉对应容器后再删镜像:
docker rm <container_id>
docker rmi nginx:1.27清理悬空镜像:
docker image prune生产环境不要随手执行:
docker system prune -a这个命令会清理所有未被容器使用的镜像。线上机器如果靠本地保留旧镜像回滚,可能一条命令把回滚镜像清掉。
容器常用命令
启动一个容器:
docker run -d --name nginx-demo -p 8080:80 nginx:1.27查看正在运行的容器:
docker ps查看所有容器,包括已经退出的:
docker ps -a看日志:
docker logs nginx-demo持续跟日志:
docker logs -f --tail=100 nginx-demo进入容器:
docker exec -it nginx-demo sh这里要注意,很多精简镜像没有 bash,所以先用 sh。如果你执行 docker exec -it nginx-demo bash 报找不到命令,不代表容器坏了,只是镜像里没有 bash。
看容器详细信息:
docker inspect nginx-demo看资源占用:
docker stats nginx-demo停止、启动、重启:
docker stop nginx-demo
docker start nginx-demo
docker restart nginx-demo删除容器:
docker rm nginx-demo如果容器正在运行,先停再删;测试环境也可以强制删除:
docker rm -f nginx-demo生产建议:不要把业务排障习惯建立在进容器手工改文件上。容器内手工修改很难追踪,重启或重新部署后也容易丢。配置应该外置,镜像应该重打,变更应该可复现。
Nginx 容器实战
我们用 Nginx 走一个完整的小例子:准备页面、挂载配置、启动容器、验证访问。
先准备目录:
mkdir -p nginx-demo/html nginx-demo/conf
cd nginx-demo写一个首页:
cat > html/index.html <<'EOF'
<h1>Hello Docker Nginx</h1>
EOF写 Nginx 配置:
cat > conf/default.conf <<'EOF'
server {
listen 80;
server_name _;
location / {
root /usr/share/nginx/html;
index index.html;
}
}
EOF启动容器:
docker run -d \
--name nginx-demo \
-p 8080:80 \
-v "$PWD/html":/usr/share/nginx/html:ro \
-v "$PWD/conf/default.conf":/etc/nginx/conf.d/default.conf:ro \
nginx:1.27这里解释一下:
-v "$PWD/html":/usr/share/nginx/html:ro:把宿主机页面目录只读挂进容器。-v "$PWD/conf/default.conf":/etc/nginx/conf.d/default.conf:ro:把配置文件挂进去。:ro表示容器内只读,避免容器进程改宿主机文件。
验证:
curl http://127.0.0.1:8080应该看到:
<h1>Hello Docker Nginx</h1>如果访问不到,按顺序查:
docker ps
docker logs nginx-demo
ss -lntp | grep 8080排查思路:
docker ps看容器是否还在运行。docker logs看 Nginx 配置是否报错。ss看宿主机端口是否监听。
修改首页后:
echo '<h1>Hello Updated</h1>' > html/index.html
curl http://127.0.0.1:8080这时应该能看到新内容。因为页面目录是 bind mount,宿主机文件变化会直接反映到容器里。
清理:
docker rm -f nginx-demoDockerfile 实战
镜像不能总靠手工进容器改。真正的项目应该用 Dockerfile 把运行环境写下来,这样别人才能重复构建。
先准备一个最小的静态站镜像。
创建目录:
mkdir -p image-demo/html
cd image-demo写页面:
cat > html/index.html <<'EOF'
<h1>Hello From Image</h1>
EOF写 Dockerfile:
FROM nginx:1.27
COPY html/ /usr/share/nginx/html/构建镜像:
docker build -t nginx-image-demo:1.0.0 .这里 . 是构建上下文,意思是把当前目录作为 Docker build 可以读取的范围。COPY html/ ... 只能复制构建上下文里的文件,不能复制上下文外面的文件。
启动验证:
docker run -d --name nginx-image-demo -p 8081:80 nginx-image-demo:1.0.0
curl http://127.0.0.1:8081应该看到:
<h1>Hello From Image</h1>如果构建时报:
failed to compute cache key通常是 COPY 的路径写错,或者文件不在构建上下文里。先执行:
find . -maxdepth 3 -type f确认 Dockerfile 旁边确实有你要复制的文件。
.dockerignore 很重要
构建上下文不是越大越好。如果你把 target/、.git/、日志、临时文件都发给 Docker daemon,构建会慢,镜像也可能误带垃圾。
新建 .dockerignore:
.git
.idea
.vscode
target
build
logs
*.log
node_modules这里要注意,.dockerignore 影响的是构建上下文,不是容器运行时文件。它解决的是“哪些文件不要参与构建”。
RUN、CMD、ENTRYPOINT 怎么区分
先记住一句话:
RUN是构建镜像时执行。CMD是容器启动时默认执行。ENTRYPOINT是容器启动入口,通常用来固定主进程。
例子:
FROM alpine:3.20
RUN echo "build time"
CMD ["echo", "container start"]构建时会执行 RUN,启动容器时才执行 CMD。
一般业务镜像更推荐 exec 形式:
CMD ["java", "-jar", "/app/app.jar"]不要写成:
CMD java -jar /app/app.jarexec 形式更利于信号传递,容器停止时 Java 进程更容易正确收到退出信号。
Java / Spring Boot 镜像
现在不要把已经弃用的老 Java 基础镜像作为主线示例。Docker Hub 上的 OpenJDK 官方镜像已经不适合作为新项目默认选择,常见替代是 Eclipse Temurin、Amazon Corretto 等。
假设我们已经有一个构建好的 jar:
target/app.jar写 Dockerfile:
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY target/app.jar /app/app.jar
ENV TZ=Asia/Shanghai
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]构建:
docker build -t registry.example.local/team/app:1.0.0 .启动:
docker run -d \
--name app-demo \
-p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
--memory=512m \
--cpus=1 \
registry.example.local/team/app:1.0.0这里要注意:
SPRING_PROFILES_ACTIVE通过环境变量传,不要写死进镜像。--memory和--cpus给容器资源边界。MaxRAMPercentage让 JVM 按容器内存比例估算堆,而不是盲目吃宿主机内存。ExitOnOutOfMemoryError让 OOM 时进程退出,交给编排或监控拉起。
验证应用:
docker logs -f --tail=100 app-demo
curl http://127.0.0.1:8080/actuator/health如果健康检查返回:
{"status":"UP"}说明服务至少在容器里启动并能被宿主机访问。
如果容器秒退:
docker ps -a
docker logs app-demo
docker inspect app-demo --format '{{.State.ExitCode}}'排查重点:
- jar 路径是否存在。
- Java 版本是否和应用编译版本匹配。
- 启动参数是否缺少配置。
- 容器内端口和宿主机映射端口是否写反。
多阶段构建
如果你希望在 Docker 构建里完成 Maven 编译,可以用多阶段构建:
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /src
COPY pom.xml .
COPY src ./src
RUN mvn -B -DskipTests package
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /src/target/*.jar /app/app.jar
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]构建:
docker build -t registry.example.local/team/app:1.0.0 .多阶段构建的好处是:编译环境留在 build 阶段,最终镜像只带运行环境和 jar,镜像更小,攻击面也更少。
生产建议:企业内网里 Maven 依赖也要走私服,不要让 Docker build 在生产网络里临时访问公网 Maven 仓库。
网络:先用自定义 bridge
Docker 默认有一个 bridge 网络,但实际项目里更推荐自己创建网络。原因很简单:自定义 bridge 网络里,容器可以用服务名互相访问,隔离性也更好。
创建网络:
docker network create app-net启动 Redis:
docker run -d --name redis --network app-net redis:7.4启动一个临时容器测试解析:
docker run --rm --network app-net redis:7.4 redis-cli -h redis ping如果返回:
PONG说明同一个自定义网络里,服务名 redis 能解析到容器。
查看网络:
docker network inspect app-net这里能看到哪些容器加入了这个网络、容器 IP 是什么。
端口映射要分清楚左右:
docker run -d --name nginx-demo -p 8080:80 nginx:1.278080:80 的意思是宿主机 8080 -> 容器 80。写反了就访问不到你想要的端口。
端口不通时按这个顺序查:
docker ps
docker port nginx-demo
curl http://127.0.0.1:8080
ss -lntp | grep 8080生产建议:容器之间通信优先走容器网络和服务名,只有需要外部访问的入口才发布端口。不要把数据库、Redis 这类内部服务随手 -p 到公网网卡。
数据卷:别把数据放在容器可写层
容器删了,容器可写层也会跟着删。数据库、上传文件、业务持久化目录不要放在容器可写层里。
创建 volume:
docker volume create mysql-data启动 MySQL:
docker run -d \
--name mysql-demo \
-e MYSQL_ROOT_PASSWORD=change-me \
-v mysql-data:/var/lib/mysql \
mysql:8.4查看 volume:
docker volume ls
docker volume inspect mysql-data这里 Mountpoint 就是 Docker 在宿主机上管理这个 volume 的位置。一般不建议直接进去手工改文件。
为什么不建议用 docker commit 保存数据库数据?因为 volume 不是容器可写层的一部分。你把 MySQL 容器 commit 成新镜像,volume 里的数据不会被打进镜像。
可以做个判断:
docker inspect mysql-demo --format '{{json .Mounts}}'如果看到 /var/lib/mysql 是 volume 挂载,就说明数据在 volume 里,不在镜像层里。
备份 volume:
docker run --rm \
-v mysql-data:/data \
-v "$PWD":/backup \
busybox \
tar czf /backup/mysql-data.tar.gz -C /data .恢复 volume:
docker volume create mysql-data-restore
docker run --rm \
-v mysql-data-restore:/data \
-v "$PWD":/backup \
busybox \
sh -c 'cd /data && tar xzf /backup/mysql-data.tar.gz'生产建议:数据库备份优先使用数据库自己的备份工具,volume 文件备份更适合作为辅助兜底。直接拷贝数据库文件前,要确认数据库一致性。
bind mount 和 volume 怎么选
一般这样判断:
- 配置文件、静态资源、开发联调目录:可以用 bind mount。
- 数据库数据、生产持久化数据:优先用 volume 或明确规划的宿主机数据目录。
- 临时测试:怎么方便怎么来,但要知道数据在哪里。
bind mount 示例:
docker run -d \
--name nginx-demo \
-p 8080:80 \
-v "$PWD/html":/usr/share/nginx/html:ro \
nginx:1.27volume 示例:
docker run -d \
--name redis-demo \
-v redis-data:/data \
redis:7.4 redis-server --appendonly yes验证 Redis 数据目录挂载:
docker inspect redis-demo --format '{{json .Mounts}}'日志:默认不管,早晚爆盘
Docker 默认日志驱动通常是 json-file。这里要注意,默认情况下它不会主动轮转,容器日志多了就可能把磁盘打满。
先看当前日志驱动:
docker info --format '{{.LoggingDriver}}'如果输出是:
json-file就要确认有没有配置日志大小限制。
推荐在 daemon.json 里设置:
{
"log-driver": "local",
"log-opts": {
"max-size": "100m",
"max-file": "5"
}
}重启 Docker:
sudo systemctl daemon-reload
sudo systemctl restart docker注意:这个配置主要影响新创建的容器。已经存在的容器不一定自动应用新日志策略,通常要重建容器。
查某个容器日志文件:
docker inspect nginx-demo --format '{{.LogPath}}'如果磁盘满了,先定位:
df -h
docker system df
sudo du -sh /var/lib/docker/* 2>/dev/null生产建议:应用日志优先输出到 stdout/stderr,由平台采集。不要在容器内无限写本地日志文件。如果必须写文件,要挂载明确目录并配置应用侧轮转。
资源限制:别让一个容器吃完整台机器
默认情况下,容器没有严格的 CPU 和内存上限。开发环境问题不大,生产环境很危险。
启动容器时限制资源:
docker run -d \
--name app-demo \
--memory=512m \
--memory-swap=512m \
--cpus=1 \
registry.example.local/team/app:1.0.0这里的意思是:
--memory=512m:容器最多使用 512MB 内存。--memory-swap=512m:不额外使用 swap。--cpus=1:最多使用约 1 个 CPU。
查看运行时资源:
docker stats app-demo如果容器因为 OOM 被杀:
docker inspect app-demo --format '{{.State.OOMKilled}}'
docker inspect app-demo --format '{{.State.ExitCode}}'如果 OOMKilled 是 true,说明内存超过限制。Java 服务要同时看 JVM 参数和容器限制,不要只给容器限制,却让 JVM 仍然按大内存机器估算堆。
生产建议:资源限制要和压测结果、JVM 参数、监控告警一起设计。不是随便写个 512m 就完事。
Docker Compose:把多个容器写成一份文件
单个容器可以用 docker run。一旦有应用、Redis、MySQL、Nginx,就不要靠一串手工命令维护了,用 Compose 更清楚。
现在主线命令是:
docker compose version如果能看到版本,说明 Compose v2 插件可用。
准备一个示例目录:
mkdir -p compose-demo
cd compose-demo写 compose.yaml:
services:
redis:
image: redis:7.4
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
networks:
- app-net
app:
image: registry.example.local/team/app:1.0.0
depends_on:
- redis
environment:
SPRING_PROFILES_ACTIVE: prod
SPRING_DATA_REDIS_HOST: redis
JAVA_TOOL_OPTIONS: "-XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"
ports:
- "8080:8080"
deploy:
resources:
limits:
cpus: "1"
memory: 512M
networks:
- app-net
volumes:
redis-data:
networks:
app-net:启动:
docker compose up -d查看:
docker compose ps
docker compose logs -f --tail=100访问应用:
curl http://127.0.0.1:8080/actuator/health这里要注意,Compose 里的服务名就是默认 DNS 名。应用访问 Redis 时写 redis,不要写 127.0.0.1。在容器里,127.0.0.1 指的是容器自己,不是 Redis 容器。
停止:
docker compose down如果要连 volume 一起删:
docker compose down -v这个命令要谨慎,-v 会删除 Compose 创建的数据卷。生产环境不要手滑。
再强调一个坑:depends_on 只表示启动顺序,不等于服务已经准备好。Redis 容器启动了,不代表应用连接时 Redis 一定已经可用。生产里要靠应用重试、健康检查或启动探针处理。
镜像版本和发布回滚
业务镜像命名不要随意。推荐至少包含:
- 仓库地址。
- 团队或项目名。
- 应用名。
- 明确版本号。
例如:
registry.example.local/pay/order-service:2026.07.09.1
registry.example.local/pay/order-service:git-5ca0818构建:
docker build -t registry.example.local/pay/order-service:2026.07.09.1 .推送:
docker push registry.example.local/pay/order-service:2026.07.09.1上线时拉取:
docker pull registry.example.local/pay/order-service:2026.07.09.1记录 digest:
docker image inspect registry.example.local/pay/order-service:2026.07.09.1 --format '{{json .RepoDigests}}'回滚时不要重新构建旧代码,优先拉回上一个已经验证过的镜像 tag:
docker pull registry.example.local/pay/order-service:2026.07.08.3生产建议:tag、digest、构建时间、Git commit、构建参数、基础镜像版本都要能追踪。否则出了问题只能靠猜。
Overlay2 和 CoW:不是背概念,是用来解释问题
很多人学到 Overlay2、CoW 就开始背定义。实际工作里你只要先理解它能解释哪些问题。
看 Docker 使用的存储驱动:
docker info --format '{{.Driver}}'常见输出:
overlay2overlay2 大致做了这件事:镜像是多层只读层叠起来的,容器启动后在最上面加一层可写层。容器里改文件,改的是容器可写层,不会改原始镜像层。
这就能解释几个现象:
- 多个容器可以共享同一个镜像层,所以启动很快。
- 容器里写大量文件,会让容器可写层变大。
- 删除容器后,可写层就没了。
- volume 不属于镜像层,所以
docker commit不会把 volume 数据打进镜像。
看容器大小:
docker ps -s这里会看到两类大小:
SIZE:容器可写层大小。VIRTUAL SIZE:镜像层加容器层的总视图大小。
如果某个容器 SIZE 很大,说明它在容器可写层里写了大量数据。常见原因:
- 应用日志写进容器文件系统。
- 临时文件没有清理。
- 上传文件没有挂 volume。
- 数据库数据目录没有挂载。
进容器查:
docker exec -it app-demo sh
du -sh /* 2>/dev/null生产建议:把可增长的数据放到 volume、对象存储或外部服务里,不要让容器可写层承载业务持久化数据。
镜像层、可写层、volume 和日志的边界可以按下面这张图理解。上线评审时它用来判断“这个目录写到哪里去了”,排障时它用来解释为什么 docker ps -s 变大、为什么删容器会丢数据、为什么日志不在镜像层里。
这张图的结论很直接:docker ps -s 看到的是容器可写层增长,docker system df 看的是 Docker 整体磁盘账本,docker inspect 才能确认数据目录和日志路径。写密集场景,比如数据库、上传文件、索引缓存、大日志文件,默认都应该走 volume、bind mount 或外部存储;容器可写层只适合短生命周期变化。
Namespace 和 Cgroups:为什么容器像一台小机器
容器不是虚拟机。它本质上还是宿主机上的进程,只是通过 Linux 内核能力做了隔离和限制。
Namespace 负责“看起来隔离”:
- PID namespace:容器里看到自己的进程号。
- Network namespace:容器有自己的网络栈。
- Mount namespace:容器看到自己的文件系统视图。
- UTS namespace:容器可以有自己的 hostname。
Cgroups 负责“用多少资源”:
- 限制 CPU。
- 限制内存。
- 统计资源使用。
- OOM 时做处理。
为什么要知道这些?因为排障时你会遇到这种问题:
- 容器里看
localhost,不是宿主机。 - 容器里看到的 PID 1,就是容器主进程。
- 容器内存不是 JVM 自己想用多少就用多少。
- 容器能看到的文件系统,不等于宿主机真实目录结构。
看容器主进程:
docker inspect app-demo --format '{{.State.Pid}}'在宿主机上看这个进程:
ps -fp <pid>这能帮助你理解:容器确实是宿主机上的进程,只是被隔离起来了。
常见排障:按现象往下查
Docker daemon 起不来
先看状态:
sudo systemctl status docker再看日志:
sudo journalctl -u docker -n 200 --no-pager常见原因:
/etc/docker/daemon.json写坏了。data-root指向的磁盘不存在或权限不对。- 配置了不可用的 insecure registry 或证书路径。
- 上一次异常断电导致 Docker 数据目录状态异常。
先验证配置文件:
sudo dockerd --validate --config-file /etc/docker/daemon.json如果 JSON 没问题,再看磁盘:
df -h
sudo ls -ld /var/lib/docker /data/docker 2>/dev/null拉镜像失败
先看完整错误,不要只看最后一行。
测试 DNS:
getent hosts registry.example.com测试网络:
curl -I https://registry.example.com/v2/测试登录:
docker login registry.example.local常见判断:
no such host:DNS 问题。i/o timeout:网络或代理问题。x509 certificate:证书问题。unauthorized:认证问题。manifest unknown:镜像 tag 不存在。
容器秒退
查看退出状态:
docker ps -a
docker logs <container_name>
docker inspect <container_name> --format '{{.State.ExitCode}}'如果日志没有输出,可以临时把入口改成 shell 进去看:
docker run --rm -it --entrypoint sh registry.example.local/team/app:1.0.0进去以后检查:
ls -la /app
java -version常见原因:
- jar 没复制进去。
- 启动命令路径错。
- 环境变量缺失。
- 配置文件没挂载。
- Java 版本不匹配。
端口访问不通
先看容器是否运行:
docker ps再看端口映射:
docker port <container_name>宿主机本机访问:
curl -v http://127.0.0.1:8080如果本机通,外部不通,再查防火墙、安全组、云厂商访问策略。
容器内访问自己服务:
docker exec -it <container_name> sh
curl -v http://127.0.0.1:8080这里要注意,应用监听 127.0.0.1 和监听 0.0.0.0 是两回事。容器里服务只监听 localhost,外部端口映射也可能访问不到。Java Web 服务一般要确认监听地址和端口。
日志爆盘
先看磁盘:
df -h
docker system df定位大目录:
sudo du -h --max-depth=1 /var/lib/docker | sort -h看容器日志路径:
docker inspect <container_name> --format '{{.LogPath}}'临时止血可以先停掉异常容器、清理确认无用的日志和镜像。但生产里更重要的是补日志轮转配置,避免下次再爆。
容器 OOM
看状态:
docker inspect <container_name> --format '{{.State.OOMKilled}}'
docker inspect <container_name> --format '{{.State.ExitCode}}'看资源:
docker stats <container_name>看宿主机内核日志:
dmesg -T | grep -i -E 'killed process|out of memory'如果是 Java 服务,重点看:
- 容器 memory limit。
- JVM 最大堆。
- 直接内存、线程数、Metaspace。
- 是否有内存泄漏。
镜像删不掉
先看谁在用:
docker ps -a --filter ancestor=<image>删除容器:
docker rm <container_id>再删镜像:
docker rmi <image>如果同一个 image id 有多个 tag,要把相关 tag 都处理掉:
docker images --no-trunc | grep <image_id>落地工程深水区
Docker 真正拖垮生产方案的,往往不是 docker run 会不会写,而是长期运行以后没人治理的边界。
容量和磁盘:镜像层、容器可写层、build cache、json 日志、volume 都会长。上线前先执行:
docker system df -v
docker ps -s
du -sh /var/lib/docker 2>/dev/null || true生产建议是把 data-root 放到独立磁盘,配置日志轮转,定期清理构建缓存。docker system prune -a 不能当日常脚本无脑跑,它会清理未使用镜像,可能把回滚窗口里的镜像删掉。
权限和安全:加入 docker 组等同于给了近似 root 的能力。生产发布账号能不能执行 Docker,要有明确边界。流水线账号只需要 login、pull、run、compose 的最小权限,不应该拿宿主机 root 私钥到处跑。
网络和防火墙:端口映射成功不代表安全策略正确。Docker 会改 iptables,ufw status 看着没放行,容器端口仍可能能访问。排查顺序是:
docker port <container>
iptables -S DOCKER-USER
ss -lntp
curl -v http://127.0.0.1:8080生产上把外部入口交给 Nginx、网关或负载均衡,Docker 端口尽量只暴露必要范围。
升级和回滚:升级 Docker Engine 前先确认 containerd、runc、Compose 插件和现有镜像兼容性。回滚不是“重新拉 latest”,而是保留上一版镜像 tag/digest、Compose 文件、环境变量和挂载目录。发布记录至少要写清镜像 digest、配置文件版本和启动命令。
监控和告警:最少要监控宿主机磁盘、Docker data-root、容器重启次数、容器内存、日志增长和关键端口。只看 docker ps 是不够的,容器反复重启、内存接近限制、日志持续增长都要能告警。
团队协作:Dockerfile、Compose、.env.example、部署脚本和回滚脚本必须入库。不要让生产命令只存在某个人的 shell history 里;也不要让每个项目各写一套互不兼容的镜像 tag 规则。
生产落地检查清单
上线前按这张清单过一遍,比事后排障省很多时间。
- Docker Engine 安装方式明确,版本和系统版本匹配。
- 国内、内网、离线环境的安装源和镜像源已经验证。
- 业务镜像不用
latest,tag 和 digest 可追踪。 - Dockerfile 能从干净环境重新构建。
.dockerignore已配置,构建上下文没有垃圾文件。- 配置通过环境变量、配置中心或挂载传入,不写死进镜像。
- 数据库、上传文件、持久化目录不放容器可写层。
- 日志输出和轮转策略明确。
- 容器 CPU、内存限制明确。
- 端口映射只暴露必要入口。
- 私有仓库证书、登录凭据、机器人账号有管理策略。
- Compose 或启动脚本可以一键重建服务。
- 发布记录里有镜像 tag、digest、Git commit 和构建时间。
- 回滚镜像已保留,回滚命令提前验证。
- 常用排障命令团队成员都能执行。
常用命令速查
查看版本:
docker version
docker info
docker compose version镜像:
docker pull nginx:1.27
docker images
docker image inspect nginx:1.27
docker tag nginx:1.27 registry.example.local/base/nginx:1.27
docker push registry.example.local/base/nginx:1.27
docker rmi nginx:1.27容器:
docker run -d --name nginx-demo -p 8080:80 nginx:1.27
docker ps
docker ps -a
docker logs -f --tail=100 nginx-demo
docker exec -it nginx-demo sh
docker stop nginx-demo
docker rm nginx-demo网络:
docker network ls
docker network create app-net
docker network inspect app-net
docker port nginx-demo数据卷:
docker volume ls
docker volume create app-data
docker volume inspect app-data资源:
docker stats
docker system df
docker ps -sCompose:
docker compose up -d
docker compose ps
docker compose logs -f --tail=100
docker compose down离线迁移:
docker save nginx:1.27 -o nginx-1.27.tar
sha256sum nginx-1.27.tar > nginx-1.27.tar.sha256
sha256sum -c nginx-1.27.tar.sha256
docker load -i nginx-1.27.tarDocker 真正用顺手以后,你会发现它不是“把服务塞进容器”这么简单。它更像一套交付约定:环境怎么定义,镜像怎么构建,配置怎么传入,数据放在哪里,日志怎么收,资源怎么限,版本怎么回滚,故障怎么查。把这些约定落实到命令、配置和检查清单里,Docker 才能从本地实验变成生产交付能力。
官方文档入口
安装和基础:
- Docker Engine install: https://docs.docker.com/engine/install/
- Ubuntu install: https://docs.docker.com/engine/install/ubuntu/
- CentOS install: https://docs.docker.com/engine/install/centos/
- Linux post-install: https://docs.docker.com/engine/install/linux-postinstall/
- Docker Desktop for Windows: https://docs.docker.com/desktop/setup/install/windows-install/
- Docker Desktop WSL2 backend: https://docs.docker.com/desktop/features/wsl/
- Microsoft WSL install: https://learn.microsoft.com/windows/wsl/install
构建、运行和编排:
- Dockerfile reference: https://docs.docker.com/reference/dockerfile/
- Build best practices: https://docs.docker.com/build/building/best-practices/
- Running containers: https://docs.docker.com/engine/containers/run/
- Docker Compose: https://docs.docker.com/compose/
- Compose file reference: https://docs.docker.com/reference/compose-file/
存储、网络、日志和资源:
- Volumes: https://docs.docker.com/engine/storage/volumes/
- Networking: https://docs.docker.com/engine/network/
- Bridge network: https://docs.docker.com/engine/network/drivers/bridge/
- Port publishing: https://docs.docker.com/engine/network/port-publishing/
- Storage drivers: https://docs.docker.com/engine/storage/drivers/
- OverlayFS driver: https://docs.docker.com/engine/storage/drivers/overlayfs-driver/
- Resource constraints: https://docs.docker.com/engine/containers/resource_constraints/
- Logging drivers: https://docs.docker.com/engine/logging/configure/
- dockerd reference: https://docs.docker.com/reference/cli/dockerd/
国内和私有仓库:
- 阿里云 ACR: https://help.aliyun.com/zh/acr/
- 腾讯云 TCR: https://cloud.tencent.com/document/product/1141
- 华为云 SWR: https://support.huaweicloud.com/swr/
- Harbor: https://goharbor.io/docs/
