欢迎你来读这篇博客。
这篇文章是一篇系统性的 Docker 深度长文,目标不是让你只会敲几条 docker run,而是能真正理解 Docker 在工程中的价值:它为什么能解决环境一致性问题,镜像和容器到底是什么,网络为什么经常出问题,数据为什么必须挂卷,端口映射为什么不是 EXPOSE,Dockerfile 为什么会影响镜像大小和构建速度,以及多阶段构建为什么是生产镜像的基本功。
本文核心内容包括:
Docker 环境安装与基础配置
Docker 架构、镜像、容器、仓库、分层文件系统
容器与虚拟机的区别
Namespace、Cgroups、UnionFS、overlay2 的基本理解
容器生命周期与常用命令
Docker 网络、bridge、host、none、自定义网络、容器 DNS
端口映射、端口发布、容器内外访问问题
数据卷、bind mount、tmpfs、数据备份与恢复
常见应用部署:Nginx、Redis、MySQL、PostgreSQL、RabbitMQ、MinIO、Spring Boot
容器转镜像:docker commit、save/load、export/import 的区别
Dockerfile 指令详解、缓存机制、构建优化
Dockerfile 多阶段构建:Java、Node.js、Go 案例
健康检查、日志、资源限制、安全实践、排错脚本
Docker 是一个很典型的“入门很快,写好很难”的技术。你可以三分钟跑起来一个 Nginx,也可以三天排查一个网络和数据卷权限问题。别怕,Docker 这东西表面像集装箱,深入之后像俄罗斯套娃,不过我们今天一层一层拆。
一、为什么需要 Docker? 在没有 Docker 之前,应用部署常见流程大概是这样:
申请服务器。
安装 JDK、Node.js、Python、Nginx、MySQL、Redis。
修改配置文件。
上传应用包。
启动服务。
出问题后开始怀疑人生。
常见问题包括:
开发环境能跑,测试环境不能跑。
测试环境能跑,生产环境不能跑。
A 同事 JDK 是 8,B 同事 JDK 是 17。
本地 Redis 没密码,线上 Redis 有密码。
Linux 发行版不同,依赖包版本不同。
服务器换一台之后,部署文档像考古现场。
Docker 的思路是:
不要只交付代码,而是把代码、运行时、系统依赖、启动命令一起打包成镜像。
这样,应用运行环境就可以从“靠人肉安装”变成“靠镜像复制”。
1. 传统部署和 Docker 部署对比 flowchart TB
subgraph A[传统部署]
A1[服务器] --> A2[手动安装 JDK]
A2 --> A3[手动安装 Nginx / Redis]
A3 --> A4[上传 jar 包]
A4 --> A5[修改配置]
A5 --> A6[启动应用]
A6 --> A7[环境差异导致问题]
end
subgraph B[Docker 部署]
B1[编写 Dockerfile] --> B2[构建镜像]
B2 --> B3[推送镜像仓库]
B3 --> B4[服务器拉取镜像]
B4 --> B5[docker run 启动容器]
B5 --> B6[环境相对一致]
end
2. Docker 的核心价值 Docker 主要解决以下问题:
问题
Docker 的解决方式
环境不一致
镜像中包含运行环境和依赖
部署复杂
用镜像和启动命令标准化部署
应用隔离
容器拥有独立进程、网络、文件系统视图
迁移困难
镜像可以在不同主机之间分发
回滚麻烦
使用不同镜像 tag 快速回滚
本地搭环境痛苦
一条命令启动 MySQL、Redis、Nginx 等中间件
3. Docker 不是虚拟机 很多人刚学 Docker 时会把容器理解成轻量级虚拟机,这个说法可以帮助入门,但并不准确。
虚拟机是虚拟出完整硬件,然后在上面运行完整操作系统。Docker 容器则共享宿主机内核,通过 Linux 的 Namespace 和 Cgroups 做隔离与资源限制。
flowchart LR
subgraph VM[虚拟机模型]
VM1[物理机 / 宿主机 OS] --> VM2[Hypervisor]
VM2 --> VM3[Guest OS 1]
VM2 --> VM4[Guest OS 2]
VM3 --> VM5[App 1]
VM4 --> VM6[App 2]
end
subgraph Docker[容器模型]
D1[物理机 / 宿主机 OS] --> D2[Docker Engine]
D2 --> D3[Container 1]
D2 --> D4[Container 2]
D3 --> D5[App 1]
D4 --> D6[App 2]
end
对比项
虚拟机
Docker 容器
是否包含完整 OS
是
否,共享宿主机内核
启动速度
通常较慢
通常很快
资源占用
较高
较低
隔离程度
更强
较强,但弱于虚拟机
镜像大小
通常较大
通常较小
使用场景
强隔离、多 OS
应用交付、微服务、本地开发、CI/CD
一句话:
虚拟机虚拟的是一台机器,Docker 封装的是一个进程及其运行环境。
二、Docker 环境安装与基础配置 1. Docker 组件关系 Docker 并不是一个单独的命令,它背后至少包含以下组件:
组件
说明
Docker CLI
命令行客户端,也就是 docker 命令
Docker Daemon
Docker 服务端,负责镜像、容器、网络、卷等管理
containerd
容器运行时管理组件
runc
低层容器运行时,负责真正创建容器进程
Docker Registry
镜像仓库,例如 Docker Hub、Harbor、云厂商镜像仓库
BuildKit
新一代镜像构建后端,支持更好的缓存、多平台构建、secret 挂载等
整体关系如下:
flowchart LR
A[Docker CLI] -->|REST API / Unix Socket| B[Docker Daemon]
B --> C[BuildKit 构建镜像]
B --> D[containerd 管理容器生命周期]
D --> E[runc 创建容器进程]
B --> F[Images 镜像]
B --> G[Containers 容器]
B --> H[Networks 网络]
B --> I[Volumes 数据卷]
B <-->|pull / push| J[Registry 镜像仓库]
2. Linux 安装 Docker Engine 以 Ubuntu 为例,可以使用以下脚本安装 Docker Engine。
生产环境建议结合公司安全规范、内网源、版本锁定策略进行安装,不要随手复制网上的一键脚本就往生产服务器上怼。生产服务器不是许愿池。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #!/usr/bin/env bash set -e sudo apt-get remove -y docker docker-engine docker.io containerd runc || true sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg lsb-release sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpgecho \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$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 sudo systemctl enable docker sudo systemctl start docker docker version docker info
3. 普通用户执行 Docker 命令 默认情况下,执行 Docker 命令可能需要 sudo。
开发环境可以把当前用户加入 docker 用户组:
1 2 sudo usermod -aG docker $USER newgrp docker
验证:
注意:加入 docker 组的用户基本等价于拥有较高系统权限,因为它可以挂载宿主机目录、启动特权容器。所以在生产环境中,不要随便给用户加这个权限。
4. 验证 Docker 是否正常
如果能看到 Docker 拉取镜像并输出欢迎信息,说明 Docker 基础环境可用。
5. 查看 Docker 信息
重点看:
Client 版本
Server 版本
API version
OS/Arch
查看更详细的信息:
重点看:
Storage Driver,一般是 overlay2
Cgroup Driver
Cgroup Version
Docker Root Dir
Registry Mirrors
Default Runtime
Kernel Version
6. 配置 Docker daemon Docker daemon 配置文件通常是:
一个常用示例:
1 2 3 4 5 6 7 8 9 { "log-driver" : "json-file" , "log-opts" : { "max-size" : "100m" , "max-file" : "3" } , "exec-opts" : [ "native.cgroupdriver=systemd" ] , "storage-driver" : "overlay2" }
修改后重启 Docker:
1 2 sudo systemctl daemon-reload sudo systemctl restart docker
验证:
7. 配置镜像加速器 如果拉取 Docker Hub 镜像较慢,可以使用镜像加速器。不同云厂商会提供不同地址,配置方式类似:
1 2 3 4 5 { "registry-mirrors" : [ "https://your-mirror.example.com" ] }
重启 Docker:
1 sudo systemctl restart docker
三、Docker 核心概念深入理解 1. 镜像 Image 镜像是只读模板,包含应用运行所需的文件系统、依赖、环境变量、启动命令等。
拉取镜像:
查看镜像:
查看镜像详细信息:
1 docker image inspect nginx:1.27
删除镜像:
镜像通常由多个只读层组成。Dockerfile 中的指令会生成镜像层,例如:
1 2 3 FROM ubuntu:24.04 RUN apt-get update && apt-get install -y curl COPY app.jar /app/app.jar
可以抽象成:
flowchart TB
L1[Layer 1: ubuntu 基础文件系统] --> L2[Layer 2: 安装 curl]
L2 --> L3[Layer 3: 复制 app.jar]
L3 --> IMG[最终镜像 my-app:1.0.0]
2. 容器 Container 容器是镜像运行起来之后的实例。
1 docker run -d --name nginx-demo nginx:1.27
查看运行中的容器:
查看所有容器:
停止容器:
启动已停止容器:
删除容器:
强制删除:
3. 容器的可写层 镜像层是只读的。容器启动后,Docker 会在镜像层上面加一个可写层。
容器内新增、修改、删除文件,本质上都发生在这个可写层中。
flowchart TB
W[容器可写层 Writable Layer] --> R3[镜像只读层 3: 应用文件]
R3 --> R2[镜像只读层 2: 运行时依赖]
R2 --> R1[镜像只读层 1: 基础系统]
这也是为什么:
删除容器后,容器可写层会消失。
数据库数据不能只放在容器内部。
持久化数据应该使用 volume 或 bind mount。
4. 镜像 tag 和 digest 镜像常见格式:
1 2 3 nginx:1.27 redis:7 mysql:8.0
其中:
tag 是一个标签,不等于不可变版本。理论上,同一个 tag 可以被重新推送指向新内容。
更严格的方式是使用 digest:
1 nginx@sha256:xxxxxxxxxxxxxxxx
生产环境建议至少使用明确版本 tag,不建议长期使用 latest。
不推荐:
推荐:
更严格:
1 docker run nginx@sha256:xxxx
5. Registry 镜像仓库 Registry 用于存储和分发镜像。
常见仓库:
Docker Hub
Harbor
GitHub Container Registry
GitLab Container Registry
阿里云 / 腾讯云 / 华为云镜像仓库
登录:
1 docker login registry.example.com
打标签:
1 docker tag my-app:1.0.0 registry.example.com/backend/my-app:1.0.0
推送:
1 docker push registry.example.com/backend/my-app:1.0.0
拉取:
1 docker pull registry.example.com/backend/my-app:1.0.0
6. Namespace:容器隔离的基础 Docker 容器不是虚拟机,它主要依赖 Linux Namespace 实现隔离。
常见 Namespace:
Namespace
作用
PID
进程隔离,容器内有自己的进程树
NET
网络隔离,容器有自己的网卡、IP、路由表
MNT
挂载点隔离,容器看到自己的文件系统
UTS
主机名和域名隔离
IPC
进程间通信隔离
USER
用户和用户组隔离
可以简单理解为:
flowchart LR
A[宿主机 Linux Kernel] --> B[PID Namespace]
A --> C[NET Namespace]
A --> D[MNT Namespace]
A --> E[IPC Namespace]
A --> F[UTS Namespace]
A --> G[USER Namespace]
B --> H[容器进程]
C --> H
D --> H
E --> H
F --> H
G --> H
容器看到的是被隔离后的世界。它以为自己有独立系统,其实共享宿主机内核。
7. Cgroups:资源限制的基础 Cgroups 用来限制和统计进程资源,例如:
Docker 中可以这样限制资源:
1 2 3 4 5 docker run -d \ --name app \ --memory=512m \ --cpus=1.5 \ my-app:1.0.0
这表示容器最多使用 512MB 内存和大约 1.5 个 CPU。
8. UnionFS 与 overlay2 Docker 镜像分层依赖联合文件系统。现代 Docker 在 Linux 上常用 overlay2 存储驱动。
它的核心思想是:
多个只读层叠加起来形成镜像文件系统。
容器启动时增加一个可写层。
修改文件时采用 copy-on-write 机制。
举个例子:
镜像中有 /app/config.yml。
容器启动后读取该文件,直接从只读层读取。
容器内修改该文件时,Docker 会先把文件复制到可写层。
后续读取时看到的是可写层中的新版本。
这个机制带来的影响:
镜像层可以复用,所以多个容器共享同一个基础镜像层。
容器启动很快,因为不用复制完整文件系统。
频繁写入的数据不适合放在容器可写层,应该挂载 volume。
四、容器生命周期与常用命令 1. 容器生命周期 容器大致有以下状态:
stateDiagram-v2
[*] --> Created: docker create
Created --> Running: docker start
[*] --> Running: docker run
Running --> Paused: docker pause
Paused --> Running: docker unpause
Running --> Exited: docker stop / 进程退出
Exited --> Running: docker start
Exited --> Removed: docker rm
Running --> Removed: docker rm -f
2. docker run 做了什么? 下面这条命令:
1 docker run -d --name nginx-demo -p 8080:80 nginx:1.27
背后大致做了这些事:
检查本地是否存在 nginx:1.27 镜像。
如果不存在,从 Registry 拉取镜像。
创建容器可写层。
创建网络命名空间并分配 IP。
设置端口映射规则。
挂载文件系统。
启动容器主进程。
返回容器 ID。
flowchart TB
A[docker run] --> B{本地有镜像?}
B -->|没有| C[docker pull]
B -->|有| D[创建容器]
C --> D
D --> E[创建可写层]
E --> F[配置网络]
F --> G[配置挂载]
G --> H[启动主进程]
H --> I[容器 Running]
3. 常用容器命令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 docker run -d --name nginx-demo nginx:1.27 docker create --name nginx-created nginx:1.27 docker start nginx-created docker stop nginx-demo docker restart nginx-demo docker pause nginx-demo docker unpause nginx-demo docker rm nginx-demo docker rm -f nginx-demo
4. 进入容器 1 docker exec -it nginx-demo bash
如果镜像里没有 bash:
1 docker exec -it nginx-demo sh
exec 是在一个已经运行的容器中执行新命令。
5. 查看容器日志 1 2 3 4 5 6 7 8 9 10 11 12 13 14 docker logs nginx-demo docker logs -f nginx-demo docker logs --tail =100 nginx-demo docker logs --since=30m nginx-demo docker logs -t nginx-demo
6. 查看容器资源占用
查看指定容器:
7. 查看容器详情 1 docker inspect nginx-demo
常见用途:
1 2 3 4 5 6 7 8 9 10 11 docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' nginx-demo docker inspect -f '{{json .Mounts}}' nginx-demo docker inspect -f '{{json .Config.Cmd}}' nginx-demo docker inspect -f '{{json .Config.Env}}' nginx-demo
8. 查看容器内进程
9. 宿主机和容器之间复制文件 宿主机复制到容器:
1 docker cp ./app.conf nginx-demo:/etc/nginx/conf.d/app.conf
容器复制到宿主机:
1 docker cp nginx-demo:/etc/nginx/nginx.conf ./nginx.conf
10. 容器退出码 查看容器退出码:
1 docker inspect -f '{{.State.ExitCode}}' container-name
常见退出码:
退出码
含义
0
正常退出
1
一般错误
125
Docker daemon 执行命令失败
126
命令不可执行
127
命令不存在
137
通常表示被 SIGKILL 杀掉,可能是 OOM
143
通常表示收到 SIGTERM 后退出
五、Docker 网络深入理解 Docker 网络是容器部署中最容易出问题的部分之一。很多问题表面是“服务访问不了”,本质是网络模式、端口映射、监听地址、容器 DNS、宿主机防火墙之间的关系没搞清楚。
1. 查看 Docker 网络
默认通常能看到:
2. bridge 网络 bridge 是 Docker 默认网络模式。容器会连接到一个 Linux bridge 上,常见名字是 docker0。
容器之间通过虚拟网卡 veth pair 连接到 bridge。
flowchart TB
subgraph Host[宿主机]
B[docker0 bridge]
R[iptables / NAT]
B --> R
end
subgraph C1[容器 app]
E1[eth0]
end
subgraph C2[容器 redis]
E2[eth0]
end
E1 <-->|veth pair| B
E2 <-->|veth pair| B
R --> Internet[外部网络]
启动一个容器:
1 docker run -d --name nginx-bridge nginx:1.27
查看容器 IP:
1 docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' nginx-bridge
3. 默认 bridge 和自定义 bridge 的区别 默认 bridge 可以用,但不推荐复杂场景长期使用。更推荐创建自定义网络。
创建自定义网络:
1 docker network create app-net
启动 Redis:
1 2 3 4 docker run -d \ --name redis \ --network app-net \ redis:7
启动应用:
1 2 3 4 5 docker run -d \ --name app \ --network app-net \ -e REDIS_HOST=redis \ my-app:1.0.0
在同一个自定义网络中,容器可以通过容器名互相访问:
也就是说,应用配置里应该写:
1 2 spring.data.redis.host =redis spring.data.redis.port =6379
不要写容器 IP,因为容器 IP 可能变化。
4. 容器名解析和 DNS 自定义 bridge 网络中,Docker 会提供内置 DNS。容器可以通过以下方式访问同网络容器:
示例:
1 2 3 4 5 6 7 docker network create app-net docker run -d \ --name redis-primary \ --network app-net \ --network-alias redis \ redis:7
应用容器可以通过 redis 访问:
1 2 3 4 docker run --rm \ --network app-net \ alpine \ sh -c "apk add --no-cache drill >/dev/null && drill redis"
5. host 网络 host 网络表示容器直接使用宿主机网络命名空间。
1 docker run --network host nginx:1.27
特点:
不需要 -p 做端口映射。
容器监听端口就是宿主机端口。
网络隔离较弱。
Linux 服务器上比较常见。
适用场景:
对网络性能要求极高。
某些需要直接感知宿主机网络的应用。
本地调试特殊网络问题。
不适合:
6. none 网络 none 网络表示容器不配置网络。
1 docker run -it --network none alpine sh
适合:
7. macvlan 网络 macvlan 可以让容器像局域网中的独立机器一样拥有自己的 MAC 地址和 IP。
示例:
1 2 3 4 5 docker network create -d macvlan \ --subnet=192.168.1.0/24 \ --gateway=192.168.1.1 \ -o parent=eth0 \ macvlan-net
启动容器:
1 2 3 4 5 docker run -d \ --name nginx-macvlan \ --network macvlan-net \ --ip 192.168.1.50 \ nginx:1.27
macvlan 更偏高级网络场景,例如传统机房网络、某些中间件或网关需要独立 IP 的情况。一般开发环境不必上来就用。
8. 容器访问宿主机 Linux 下容器访问宿主机,常见方式:
使用宿主机 docker0 网关 IP,例如 172.17.0.1。
使用 --add-host=host.docker.internal:host-gateway。
示例:
1 2 3 4 docker run --rm \ --add-host=host.docker.internal:host-gateway \ alpine \ ping host.docker.internal
应用配置中可以写:
1 backend.url =http://host.docker.internal:8080
9. 网络排查命令 查看网络:
查看网络详情:
1 docker network inspect app-net
查看容器网络:
1 docker inspect app | grep -A 30 Networks
进入容器测试:
容器内测试 DNS:
测试端口:
临时启动一个网络诊断容器:
1 2 3 docker run --rm -it \ --network app-net \ nicolaka/netshoot
在诊断容器中可以使用:
1 2 3 4 5 6 7 ip addr ip route nslookup redis dig redis curl http://app:8080 nc -zv redis 6379 tcpdump -i any port 6379
六、端口映射与端口发布 端口映射是 Docker 中最常见也最容易误解的点。
容器内应用监听端口,并不代表宿主机或外部机器可以访问。需要通过 -p 或 --publish 发布端口。
1. 基本端口映射 1 2 3 4 docker run -d \ --name nginx-demo \ -p 8080:80 \ nginx:1.27
含义:
访问:
1 curl http://localhost:8080
关系图:
flowchart LR
A[浏览器 / curl] -->|localhost:8080| B[宿主机 8080]
B -->|Docker 端口发布 / NAT| C[容器 80]
C --> D[Nginx 进程]
2. 指定监听地址 默认:
通常表示绑定到宿主机所有地址,也就是 0.0.0.0:8080。
只允许本机访问:
1 2 3 4 docker run -d \ --name nginx-local \ -p 127.0.0.1:8080:80 \ nginx:1.27
这样外部机器无法通过服务器公网 IP 访问该服务。
3. 随机宿主机端口 1 2 3 4 docker run -d \ --name nginx-random \ -p 80 \ nginx:1.27
查看实际端口:
1 docker port nginx-random
也可以使用大写 -P,自动发布 Dockerfile 中 EXPOSE 声明的端口:
1 docker run -d -P nginx:1.27
4. TCP 和 UDP 端口 默认是 TCP。
UDP 示例:
同时发布 TCP 和 UDP:
1 2 3 4 5 docker run -d \ --name dns-demo \ -p 5353:5353/tcp \ -p 5353:5353/udp \ your-dns-image
5. EXPOSE 和 -p 的区别 Dockerfile 中:
它只是声明容器内应用使用 8080 端口,不会自动把端口暴露到宿主机。
真正端口映射需要:
1 docker run -p 8080:8080 my-app:1.0.0
对比:
写法
作用
EXPOSE 8080
镜像元数据声明,告诉使用者容器倾向监听 8080
-p 8080:8080
真正把宿主机端口映射到容器端口
-P
根据 EXPOSE 自动分配宿主机随机端口
6. 应用必须监听 0.0.0.0 这是很多新手会踩的坑。
如果应用在容器内只监听:
那么即使你写了:
宿主机也可能访问不了,因为应用只接受容器内部 localhost 请求。
容器内服务应该监听:
Spring Boot 默认通常可以通过:
1 2 3 server: address: 0.0 .0 .0 port: 8080
Node.js 示例:
1 app.listen (3000 , '0.0.0.0' )
7. 端口排查清单 如果端口访问不了,按这个顺序查:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 docker ps docker port container-name sudo ss -tunlp | grep 8080 docker exec -it container-name sh ss -tunlp curl http://127.0.0.1:8080 sudo ufw status
七、数据卷与持久化 容器的可写层不适合保存重要数据。只要容器被删除,可写层也会消失。
所以,数据库数据、上传文件、业务附件、配置文件、证书、日志等都应该使用挂载机制。
Docker 常见挂载方式有三种:
类型
说明
推荐场景
volume
Docker 管理的数据卷
数据库数据、持久化业务数据
bind mount
挂载宿主机目录或文件
开发调试、配置文件、日志目录
tmpfs
挂载到内存
临时文件、敏感临时数据
1. volume 数据卷 创建 volume:
1 docker volume create mysql-data
查看:
查看详情:
1 docker volume inspect mysql-data
使用 volume 启动 MySQL:
1 2 3 4 5 6 7 docker run -d \ --name mysql \ -e MYSQL_ROOT_PASSWORD=123456 \ -e MYSQL_DATABASE=demo \ -v mysql-data:/var/lib/mysql \ -p 3306:3306 \ mysql:8.0
这里表示:
1 Docker volume mysql-data -> 容器 /var/lib/mysql
删除容器后,volume 仍然存在:
1 2 docker rm -f mysql docker volume ls
重新启动 MySQL 并挂载同一个 volume,数据仍然存在。
2. bind mount bind mount 是把宿主机的具体路径挂载到容器内。
示例:
1 2 3 4 5 6 7 8 mkdir -p $(pwd )/htmlecho "Hello Docker" > $(pwd )/html/index.html docker run -d \ --name nginx-web \ -p 8080:80 \ -v $(pwd )/html:/usr/share/nginx/html:ro \ nginx:1.27
:ro 表示只读挂载,容器内不能修改该目录。
bind mount 很适合开发环境:
1 2 3 4 5 docker run --rm -it \ -v $(pwd ):/workspace \ -w /workspace \ node:20 \ npm install
但生产环境使用 bind mount 时要注意:
宿主机路径必须稳定。
文件权限要正确。
目录不存在时可能被 Docker 自动创建成目录。
挂载到容器已有目录时,会遮蔽容器原目录内容。
3. –mount 写法 除了 -v,Docker 还支持更明确的 --mount。
volume 示例:
1 2 3 4 5 docker run -d \ --name mysql \ --mount type =volume,source =mysql-data,target=/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=123456 \ mysql:8.0
bind 示例:
1 2 3 4 5 docker run -d \ --name nginx \ --mount type =bind ,source =$(pwd )/html,target=/usr/share/nginx/html,readonly \ -p 8080:80 \ nginx:1.27
--mount 可读性更好,更适合复杂脚本。
4. tmpfs tmpfs 把数据挂载到内存,不写磁盘。
1 2 3 4 docker run -d \ --name temp-app \ --tmpfs /tmp \ my-app:1.0.0
适合:
5. 数据卷图解 flowchart LR
A[Docker Volume mysql-data] --> B[容器 /var/lib/mysql]
C[宿主机 ./config.yml] --> D[容器 /app/config.yml]
E[宿主机内存 tmpfs] --> F[容器 /tmp]
6. 数据卷权限问题 容器内进程可能不是 root 用户,导致挂载目录无法写入。
例如:
1 2 3 4 docker run -d \ --name app \ -v $(pwd )/logs:/app/logs \ my-app:1.0.0
如果应用报错:
1 Permission denied: /app/logs
可以检查容器用户:
检查宿主机目录权限:
修复方式:
1 sudo chown -R 1000:1000 ./logs
或者在 Dockerfile 中明确用户和目录权限。
7. MySQL 数据备份脚本 使用 mysqldump 备份逻辑数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #!/usr/bin/env bash set -e CONTAINER_NAME="mysql" DB_USER="root" DB_PASSWORD="123456" DB_NAME="demo" BACKUP_DIR="./backup" BACKUP_FILE="${DB_NAME} -$(date +%Y%m%d_%H%M%S) .sql" mkdir -p "${BACKUP_DIR} " docker exec ${CONTAINER_NAME} \ mysqldump -u${DB_USER} -p${DB_PASSWORD} ${DB_NAME} \ > "${BACKUP_DIR} /${BACKUP_FILE} " echo "backup success: ${BACKUP_DIR} /${BACKUP_FILE} "
8. MySQL 数据恢复脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #!/usr/bin/env bash set -e CONTAINER_NAME="mysql" DB_USER="root" DB_PASSWORD="123456" DB_NAME="demo" BACKUP_FILE="$1 " if [ -z "${BACKUP_FILE} " ]; then echo "Usage: ./restore-mysql.sh <backup.sql>" exit 1fi cat "${BACKUP_FILE} " | docker exec -i ${CONTAINER_NAME} \ mysql -u${DB_USER} -p${DB_PASSWORD} ${DB_NAME} echo "restore success: ${BACKUP_FILE} "
9. volume 打包备份脚本 适合备份普通文件型 volume:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #!/usr/bin/env bash set -e VOLUME_NAME="$1 " BACKUP_DIR="./volume-backup" BACKUP_FILE="${VOLUME_NAME} -$(date +%Y%m%d_%H%M%S) .tar.gz" if [ -z "${VOLUME_NAME} " ]; then echo "Usage: ./backup-volume.sh <volume-name>" exit 1fi mkdir -p "${BACKUP_DIR} " docker run --rm \ -v ${VOLUME_NAME} :/volume:ro \ -v $(pwd )/${BACKUP_DIR} :/backup \ alpine \ tar czf /backup/${BACKUP_FILE} -C /volume .echo "backup success: ${BACKUP_DIR} /${BACKUP_FILE} "
10. volume 恢复脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #!/usr/bin/env bash set -e VOLUME_NAME="$1 " BACKUP_FILE="$2 " if [ -z "${VOLUME_NAME} " ] || [ -z "${BACKUP_FILE} " ]; then echo "Usage: ./restore-volume.sh <volume-name> <backup.tar.gz>" exit 1fi docker volume create ${VOLUME_NAME} docker run --rm \ -v ${VOLUME_NAME} :/volume \ -v $(pwd ):/backup \ alpine \ sh -c "cd /volume && tar xzf /backup/${BACKUP_FILE} " echo "restore success: ${VOLUME_NAME} "
八、常见应用部署案例 本章不用编排工具,只使用原生 Docker 命令部署常见服务。
1. 部署 Nginx 1 2 3 4 5 docker run -d \ --name nginx \ --restart=unless-stopped \ -p 8080:80 \ nginx:1.27
访问:
1 curl http://localhost:8080
自定义静态页面:
1 2 3 4 5 6 7 8 9 mkdir -p htmlecho "Hello Docker Nginx" > html/index.html docker run -d \ --name nginx-web \ --restart=unless-stopped \ -p 8080:80 \ -v $(pwd )/html:/usr/share/nginx/html:ro \ nginx:1.27
自定义配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 mkdir -p conf.dcat > conf.d/default.conf <<'CONF' server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html; } } CONF docker run -d \ --name nginx-custom \ -p 8080:80 \ -v $(pwd )/html:/usr/share/nginx/html:ro \ -v $(pwd )/conf.d:/etc/nginx/conf.d:ro \ nginx:1.27
2. 部署 Redis 1 2 3 4 5 6 7 8 9 docker volume create redis-data docker run -d \ --name redis \ --restart=unless-stopped \ -p 6379:6379 \ -v redis-data:/data \ redis:7 \ redis-server --appendonly yes
测试:
1 docker exec -it redis redis-cli
执行:
1 2 3 pingset name docker get name
带密码启动:
1 2 3 4 5 6 7 docker run -d \ --name redis-auth \ --restart=unless-stopped \ -p 6379:6379 \ -v redis-data:/data \ redis:7 \ redis-server --appendonly yes --requirepass 'your-password'
连接:
1 docker exec -it redis-auth redis-cli -a 'your-password'
3. 部署 MySQL 1 2 3 4 5 6 7 8 9 10 docker volume create mysql-data docker run -d \ --name mysql \ --restart=unless-stopped \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -e MYSQL_DATABASE=demo \ -v mysql-data:/var/lib/mysql \ mysql:8.0
进入 MySQL:
1 docker exec -it mysql mysql -uroot -p123456
指定字符集:
1 2 3 4 5 6 7 8 9 10 docker run -d \ --name mysql \ --restart=unless-stopped \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -e MYSQL_DATABASE=demo \ -v mysql-data:/var/lib/mysql \ mysql:8.0 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_unicode_ci
注意:MySQL 数据必须挂载 /var/lib/mysql。
4. 部署 PostgreSQL 1 2 3 4 5 6 7 8 9 10 11 docker volume create postgres-data docker run -d \ --name postgres \ --restart=unless-stopped \ -p 5432:5432 \ -e POSTGRES_USER=demo \ -e POSTGRES_PASSWORD=123456 \ -e POSTGRES_DB=demo_db \ -v postgres-data:/var/lib/postgresql/data \ postgres:16
进入:
1 docker exec -it postgres psql -U demo -d demo_db
执行 SQL:
5. 部署 RabbitMQ 1 2 3 4 5 6 7 8 9 10 11 docker volume create rabbitmq-data docker run -d \ --name rabbitmq \ --restart=unless-stopped \ -p 5672:5672 \ -p 15672:15672 \ -e RABBITMQ_DEFAULT_USER=admin \ -e RABBITMQ_DEFAULT_PASS=123456 \ -v rabbitmq-data:/var/lib/rabbitmq \ rabbitmq:3-management
访问管理后台:
用户名密码:
6. 部署 MinIO 1 2 3 4 5 6 7 8 9 10 11 12 docker volume create minio-data docker run -d \ --name minio \ --restart=unless-stopped \ -p 9000:9000 \ -p 9001:9001 \ -e MINIO_ROOT_USER=admin \ -e MINIO_ROOT_PASSWORD=12345678 \ -v minio-data:/data \ minio/minio \ server /data --console-address ":9001"
访问控制台:
7. 部署 Spring Boot 应用 假设项目已经打包:
Dockerfile:
1 2 3 4 5 6 7 8 9 10 11 FROM eclipse-temurin:17 -jreWORKDIR /app COPY target/app.jar app.jar EXPOSE 8080 ENV JAVA_OPTS="-Xms256m -Xmx512m" ENTRYPOINT ["sh" , "-c" , "java $JAVA_OPTS -jar app.jar" ]
构建:
1 docker build -t spring-demo:1.0.0 .
运行:
1 2 3 4 5 6 7 docker run -d \ --name spring-demo \ --restart=unless-stopped \ -p 8080:8080 \ -e SPRING_PROFILES_ACTIVE=prod \ -e JAVA_OPTS="-Xms256m -Xmx512m" \ spring-demo:1.0.0
查看日志:
1 docker logs -f spring-demo
8. Spring Boot 连接 Docker 网络中的 Redis 和 MySQL 创建网络:
1 docker network create app-net
启动 MySQL:
1 2 3 4 5 6 7 docker run -d \ --name mysql \ --network app-net \ -e MYSQL_ROOT_PASSWORD=123456 \ -e MYSQL_DATABASE=demo \ -v mysql-data:/var/lib/mysql \ mysql:8.0
启动 Redis:
1 2 3 4 5 docker run -d \ --name redis \ --network app-net \ -v redis-data:/data \ redis:7 redis-server --appendonly yes
启动应用:
1 2 3 4 5 6 7 8 9 10 docker run -d \ --name spring-demo \ --network app-net \ -p 8080:8080 \ -e SPRING_DATASOURCE_URL='jdbc:mysql://mysql:3306/demo?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai' \ -e SPRING_DATASOURCE_USERNAME=root \ -e SPRING_DATASOURCE_PASSWORD=123456 \ -e SPRING_DATA_REDIS_HOST=redis \ -e SPRING_DATA_REDIS_PORT=6379 \ spring-demo:1.0.0
重点:
应用连接 MySQL 的 host 写 mysql,不是 127.0.0.1。
应用连接 Redis 的 host 写 redis,不是容器 IP。
只有需要宿主机访问应用时,才给应用映射 -p 8080:8080。
MySQL 和 Redis 如果只给应用访问,可以不映射到宿主机端口。
架构图:
flowchart LR
User[浏览器 / API Client] -->|localhost:8080| App[Spring Boot 容器]
subgraph app-net[Docker 自定义网络 app-net]
App -->|mysql:3306| MySQL[MySQL 容器]
App -->|redis:6379| Redis[Redis 容器]
end
MySQL --> V1[(mysql-data volume)]
Redis --> V2[(redis-data volume)]
九、容器转为镜像 1. docker commit docker commit 可以把容器的文件系统变更提交成一个新镜像。
示例:
1 docker run -d --name nginx-custom nginx:1.27
进入容器:
1 docker exec -it nginx-custom bash
修改首页:
1 2 echo "Hello Commit" > /usr/share/nginx/html/index.htmlexit
提交镜像:
1 docker commit nginx-custom nginx-custom:1.0.0
运行新镜像:
1 2 3 4 docker run -d \ --name nginx-custom-new \ -p 8081:80 \ nginx-custom:1.0.0
访问:
1 curl http://localhost:8081
2. docker commit 的适用场景 适合:
临时调试环境保存。
现场问题复现。
快速保存容器内手动修改。
学习和实验。
不适合:
正式生产镜像。
团队协作构建。
CI/CD 自动化构建。
长期维护。
原因很简单:docker commit 不透明。别人不知道你在容器里改了什么。生产镜像应该使用 Dockerfile,这样才可追溯、可审查、可重复构建。
3. commit 不包含 volume 数据 假设 MySQL 数据挂载在 volume:
1 2 3 4 5 docker run -d \ --name mysql \ -v mysql-data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=123456 \ mysql:8.0
执行:
1 docker commit mysql mysql-with-data:1.0.0
这个镜像不会包含 mysql-data volume 中的数据。
如果要迁移数据,应该:
使用 mysqldump。
或者备份 volume。
或者使用数据库自身的备份机制。
4. docker diff 查看容器变更 查看容器相对镜像的文件变化:
1 docker diff nginx-custom
输出标记:
标记
含义
A
Added,新增文件
C
Changed,修改文件
D
Deleted,删除文件
5. docker save/load 和 export/import 的区别 保存镜像:
1 docker save -o my-app.tar my-app:1.0.0
加载镜像:
1 docker load -i my-app.tar
导出容器文件系统:
1 docker export -o container.tar container-name
导入为镜像:
1 docker import container.tar imported-image:1.0.0
区别:
命令
对象
是否保留镜像层
是否保留 tag/元数据
典型用途
docker save
镜像
是
是
镜像离线迁移
docker load
镜像 tar
是
是
恢复镜像
docker export
容器文件系统
否
否
导出容器当前文件系统
docker import
文件系统 tar
否
可重新指定
制作扁平化镜像
一般迁移镜像,用 save/load。不要一上来就 export/import,除非你明确知道自己想丢掉镜像历史和元数据。
十、Dockerfile 深入详解 Dockerfile 是构建镜像的说明书。
一个简单 Spring Boot Dockerfile:
1 2 3 4 5 6 7 8 9 FROM eclipse-temurin:17 -jreWORKDIR /app COPY target/app.jar app.jar EXPOSE 8080 ENTRYPOINT ["java" , "-jar" , "app.jar" ]
构建:
1 docker build -t spring-demo:1.0.0 .
1. FROM FROM 指定基础镜像。
1 FROM eclipse-temurin:17 -jre
建议:
Java 运行阶段使用 JRE 镜像,不要用完整 Maven/JDK 镜像。
前端构建阶段用 Node,运行阶段用 Nginx。
Go 程序可以使用非常小的运行镜像。
不要盲目追求 alpine,某些场景会遇到 musl/glibc 兼容问题。
2. WORKDIR 设置工作目录。
后续 COPY、RUN、CMD、ENTRYPOINT 都会基于该目录执行。
不要这样写:
1 2 RUN mkdir /app RUN cd /app
因为每条 RUN 都是单独执行,cd 不会影响下一条指令。
3. COPY 和 ADD COPY 用于复制文件:
1 COPY target/app.jar app.jar
ADD 也能复制,还支持自动解压 tar 文件、从 URL 添加文件。
一般建议:
普通复制用 COPY。
确实需要自动解压 tar 时再用 ADD。
4. RUN RUN 在构建镜像时执行命令。
1 2 3 RUN apt-get update \ && apt-get install -y --no-install-recommends curl \ && rm -rf /var/lib/apt/lists/*
注意:
RUN 是构建阶段执行。
RUN 会生成镜像层。
多个相关命令建议合并,减少无用层和缓存问题。
5. CMD CMD 提供容器默认命令。
1 CMD ["java" , "-jar" , "app.jar" ]
运行时容易被覆盖:
1 docker run my-app echo hello
6. ENTRYPOINT ENTRYPOINT 指定容器入口。
1 ENTRYPOINT ["java" , "-jar" , "app.jar" ]
通常服务型镜像更适合使用 ENTRYPOINT。
如果想支持 JVM 参数环境变量,可以写:
1 ENTRYPOINT ["sh" , "-c" , "java $JAVA_OPTS -jar app.jar" ]
7. CMD + ENTRYPOINT 组合 1 2 ENTRYPOINT ["java" , "-jar" , "app.jar" ] CMD ["--spring.profiles.active=prod" ]
运行时可以覆盖 CMD:
1 docker run my-app --spring.profiles.active=test
8. ENV 设置环境变量:
1 2 ENV APP_HOME=/appENV JAVA_OPTS="-Xms256m -Xmx512m"
运行时覆盖:
1 docker run -e JAVA_OPTS="-Xms512m -Xmx1024m" my-app
9. ARG 构建参数:
1 2 ARG APP_VERSION=1.0 .0 LABEL app.version=$APP_VERSION
构建时传入:
1 docker build --build-arg APP_VERSION=1.2.0 -t my-app:1.2.0 .
ARG 和 ENV 区别:
项目
ARG
ENV
作用阶段
构建阶段
构建阶段 + 运行阶段
容器运行时是否默认存在
否
是
典型用途
版本号、构建参数
运行配置
10. EXPOSE 声明容器端口:
它不等于发布端口。发布端口仍然需要:
1 docker run -p 8080:8080 my-app
11. VOLUME 声明挂载点:
不过实际生产中,我更建议在运行命令中明确挂载:
1 docker run -v app-data:/data my-app
这样部署者更清楚数据到底挂到哪里。
12. USER 指定容器运行用户。
1 2 RUN groupadd -r app && useradd -r -g app app USER app
生产建议不要用 root 运行应用。
完整示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 FROM eclipse-temurin:17 -jreRUN groupadd -r app && useradd -r -g app app WORKDIR /app COPY target/app.jar app.jar RUN chown -R app:app /app USER appEXPOSE 8080 ENTRYPOINT ["java" , "-jar" , "app.jar" ]
13. HEALTHCHECK 健康检查:
1 2 HEALTHCHECK --interval=30s --timeout =3s --retries=3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1
注意:基础镜像中必须有 curl,否则会失败。
如果没有 curl,可以考虑:
安装 curl。
使用 wget。
写一个轻量脚本。
在外部部署系统做健康检查。
14. LABEL 添加元数据:
1 2 3 LABEL maintainer="team@example.com" LABEL app.name="order-service" LABEL app.version="1.0.0"
查看:
1 docker image inspect my-app:1.0.0
15. .dockerignore .dockerignore 用于排除不需要进入构建上下文的文件。
示例:
1 2 3 4 5 6 7 8 9 10 .git .idea .vscode *.log *.tmp .DS_Store node_modules target/classes target/test-classes README.md
如果不写 .dockerignore,Docker build 可能会把大量无用文件传给构建上下文,导致:
16. Dockerfile 构建缓存 Docker 按指令逐层构建。如果某一层缓存失效,后面的层通常也会重新构建。
不推荐:
1 2 COPY . . RUN mvn clean package -DskipTests
推荐:
1 2 3 4 5 COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests
原因:
pom.xml 不变时,依赖下载层可复用。
只改源码时,不需要重新下载所有依赖。
17. Dockerfile 编写原则
原则
说明
基础镜像明确版本
避免 latest 带来的不可控变化
使用 .dockerignore
减少上下文体积,避免敏感文件进入镜像
合理利用缓存
把依赖文件复制和源码复制分开
运行时镜像尽量小
不把构建工具带到运行镜像
不写死密码
密码通过环境变量或密钥系统注入
使用非 root 用户
降低容器逃逸或应用漏洞风险
一个容器一个主进程
不要把多个服务硬塞进一个容器
日志输出到 stdout/stderr
便于 Docker 日志采集
十一、Dockerfile 多阶段构建 多阶段构建是生产 Dockerfile 的核心能力之一。
核心思想:
一个阶段负责编译,一个阶段负责运行,最终镜像只保留运行所需文件。
官方建议所有类型应用都可以考虑多阶段构建,因为它能明显减少镜像体积和攻击面。
1. 多阶段构建基本结构 1 2 3 4 5 6 7 8 9 FROM builder-image AS builderWORKDIR /build COPY . . RUN build-command FROM runtime-imageWORKDIR /app COPY --from=builder /build/output ./output CMD ["run-command" ]
流程图:
flowchart LR
A[源码] --> B[builder 阶段]
B --> C[编译产物]
C --> D[runtime 阶段]
D --> E[最终镜像]
2. Java Maven 多阶段构建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 FROM maven:3.9 -eclipse-temurin-17 AS builderWORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests FROM eclipse-temurin:17 -jreRUN groupadd -r app && useradd -r -g app app WORKDIR /app COPY --from=builder /build/target/*.jar app.jar RUN chown -R app:app /app USER appEXPOSE 8080 ENV JAVA_OPTS="-Xms256m -Xmx512m" ENTRYPOINT ["sh" , "-c" , "java $JAVA_OPTS -jar app.jar" ]
构建:
1 docker build -t order-service:1.0.0 .
运行:
1 2 3 4 5 docker run -d \ --name order-service \ -p 8080:8080 \ -e SPRING_PROFILES_ACTIVE=prod \ order-service:1.0.0
3. Java Gradle 多阶段构建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 FROM gradle:8 -jdk17 AS builderWORKDIR /build COPY build.gradle settings.gradle ./ COPY gradle ./gradle RUN gradle dependencies --no-daemon || true COPY src ./src RUN gradle clean bootJar --no-daemon FROM eclipse-temurin:17 -jreWORKDIR /app COPY --from=builder /build/build/libs/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java" , "-jar" , "app.jar" ]
4. Node.js 前端多阶段构建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 FROM node:20 AS builderWORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM nginx:1.27 -alpineCOPY --from=builder /app/dist /usr/share/nginx/html EXPOSE 80 CMD ["nginx" , "-g" , "daemon off;" ]
构建:
1 docker build -t frontend-demo:1.0.0 .
运行:
1 2 3 4 docker run -d \ --name frontend-demo \ -p 8080:80 \ frontend-demo:1.0.0
5. Go 多阶段构建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 FROM golang:1.23 AS builderWORKDIR /build COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o app ./cmd/app FROM alpine:3.20 WORKDIR /app COPY --from=builder /build/app /app/app EXPOSE 8080 ENTRYPOINT ["/app/app" ]
如果想更极致,可以用 scratch:
1 2 3 FROM scratchCOPY --from=builder /build/app /app ENTRYPOINT ["/app" ]
但 scratch 没有 shell、证书、时区等,需要自己处理,适合非常明确的场景。
6. BuildKit 缓存优化 启用 BuildKit:
1 export DOCKER_BUILDKIT=1
Maven 缓存示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 FROM maven:3.9 -eclipse-temurin-17 AS builderWORKDIR /build COPY pom.xml . RUN --mount=type =cache,target=/root/.m2 \ mvn dependency:go-offline COPY src ./src RUN --mount=type =cache,target=/root/.m2 \ mvn clean package -DskipTests FROM eclipse-temurin:17 -jreWORKDIR /app COPY --from=builder /build/target/*.jar app.jar ENTRYPOINT ["java" , "-jar" , "app.jar" ]
这样 Maven 依赖可以走构建缓存,重复构建速度更快。
7. BuildKit secret 避免泄漏密钥 不要这样:
1 2 ARG TOKENRUN curl -H "Authorization: Bearer $TOKEN " https://example.com/private.tar.gz
因为构建参数可能进入镜像历史。
BuildKit secret 示例:
1 2 3 4 5 6 FROM alpineRUN --mount=type =secret,id =mytoken \ TOKEN=$(cat /run/secrets/mytoken) && \ echo "use token safely"
构建:
1 2 3 docker build \ --secret id =mytoken,src=./token.txt \ -t secret-demo:1.0.0 .
8. 多阶段构建的常见错误 错误一:最终镜像仍使用构建镜像。
1 2 3 4 FROM maven:3.9 -eclipse-temurin-17 COPY . . RUN mvn package CMD ["java" , "-jar" , "target/app.jar" ]
问题:最终镜像包含 Maven、本地仓库、源码、构建缓存,体积大,攻击面也大。
错误二:把源码复制进运行镜像。
运行镜像只需要 jar、配置模板、启动脚本,不需要完整源码。
错误三:构建层顺序不合理导致缓存失效。
应优先复制依赖描述文件,再复制源码。
十二、日志、健康检查与重启策略 1. 容器日志原则 容器内应用推荐把日志输出到:
Docker 会接管这些日志。
查看:
如果应用把日志只写到容器内部文件,一旦容器删除,日志也可能丢失。生产中可以:
输出到 stdout/stderr,由日志系统采集。
挂载日志目录到宿主机。
使用日志驱动或外部采集器。
2. 限制 Docker 日志大小 编辑:
1 sudo vim /etc/docker/daemon.json
示例:
1 2 3 4 5 6 7 { "log-driver" : "json-file" , "log-opts" : { "max-size" : "100m" , "max-file" : "3" } }
重启:
1 sudo systemctl restart docker
单容器指定:
1 2 3 4 5 6 docker run -d \ --name app \ --log-driver=json-file \ --log-opt max-size=100m \ --log-opt max-file=3 \ my-app:1.0.0
3. 重启策略 1 2 3 4 docker run -d \ --name app \ --restart=unless-stopped \ my-app:1.0.0
常见策略:
策略
说明
no
默认,不自动重启
always
总是重启
unless-stopped
除非手动停止,否则重启
on-failure
非 0 退出时重启
服务型应用常用:
1 --restart=unless-stopped
4. 健康检查 Dockerfile 中:
1 2 HEALTHCHECK --interval=30s --timeout =3s --retries=3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1
查看状态:
查看详细健康检查结果:
1 docker inspect --format='{{json .State.Health}}' app
Spring Boot 推荐开启 actuator health:
1 2 3 4 5 6 7 8 9 management: endpoints: web: exposure: include: health,info endpoint: health: probes: enabled: true
十三、资源限制与 Java 容器化注意事项 1. 限制内存 1 2 3 4 docker run -d \ --name app \ --memory=512m \ my-app:1.0.0
2. 限制 CPU 1 2 3 4 docker run -d \ --name app \ --cpus=1.5 \ my-app:1.0.0
3. 限制 PIDs 1 2 3 4 docker run -d \ --name app \ --pids-limit=256 \ my-app:1.0.0
4. Java 内存设置 容器内运行 Java 时,不要简单地把 -Xmx 设置成容器内存上限。
例如容器限制 512MB:
不要直接:
因为 JVM 还需要:
Metaspace
Thread Stack
Direct Memory
Code Cache
GC 结构
Native 内存
可以设置:
1 -e JAVA_OPTS="-Xms256m -Xmx384m"
或者使用百分比参数:
1 -e JAVA_OPTS="-XX:InitialRAMPercentage=50 -XX:MaxRAMPercentage=75"
启动:
1 2 3 4 5 6 7 docker run -d \ --name spring-demo \ --memory=512m \ --cpus=1 \ -e JAVA_OPTS="-XX:InitialRAMPercentage=50 -XX:MaxRAMPercentage=75" \ -p 8080:8080 \ spring-demo:1.0.0
5. OOM 排查 如果容器退出码是 137,可能是被 OOM Kill。
查看退出码:
1 docker inspect -f '{{.State.ExitCode}}' app
查看是否 OOM:
1 docker inspect -f '{{.State.OOMKilled}}' app
查看宿主机内核日志:
1 dmesg -T | grep -i "killed process"
十四、Docker 安全实践 1. 不要把密钥写进镜像 不要:
不要:
1 COPY id_rsa /root/.ssh/id_rsa
推荐:
运行时环境变量注入。
配置中心。
Secret 管理系统。
BuildKit secret。
2. 不要用 root 用户运行应用 推荐:
1 2 RUN groupadd -r app && useradd -r -g app app USER app
3. 限制容器能力 Linux capability 可以控制容器权限。
默认情况下 Docker 会授予一组能力。可以主动减少:
1 2 3 4 5 docker run -d \ --name app \ --cap-drop=ALL \ --cap-add=NET_BIND_SERVICE \ my-app:1.0.0
4. 只读根文件系统 1 2 3 4 5 docker run -d \ --name app \ --read-only \ --tmpfs /tmp \ my-app:1.0.0
如果应用需要写日志或临时文件,要额外挂载目录或 tmpfs。
5. 避免特权容器 不要轻易使用:
它会给容器大量宿主机权限。除非你明确知道自己在做什么,比如运行 Docker in Docker、某些系统级调试工具,否则不要用。
6. 固定镜像版本 不要生产长期使用:
1 2 mysql:latest redis:latest
推荐:
7. 扫描镜像漏洞 可以使用:
1 docker scout cves my-app:1.0.0
也可以使用 Trivy:
1 trivy image my-app:1.0.0
8. 最小化镜像 最终镜像不应该包含:
源码
编译工具
包管理缓存
测试文件
本地配置
私钥
.git 目录
十五、Docker 排错手册 1. 容器启动失败
查看日志:
1 docker logs container-name
查看退出码:
1 docker inspect -f '{{.State.ExitCode}}' container-name
常见原因:
启动命令错误。
应用配置错误。
环境变量缺失。
端口冲突。
文件权限问题。
数据库连接失败。
容器内没有对应命令。
2. 端口访问失败 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 docker ps docker port container-name sudo ss -tunlp | grep 8080 docker exec -it container-name sh ss -tunlp curl http://127.0.0.1:8080
重点检查:
应用是否监听 0.0.0.0。
是否使用了正确的 -p 宿主机端口:容器端口。
宿主机防火墙是否放行。
云服务器安全组是否放行。
3. 容器之间无法访问 检查是否在同一个网络:
1 docker network inspect app-net
检查 DNS:
1 2 3 docker run --rm -it --network app-net nicolaka/netshoot nslookup redis nc -zv redis 6379
常见错误:
应用容器和数据库容器不在同一个网络。
应用配置写了 127.0.0.1。
数据库容器未启动完成。
服务监听地址只绑定 localhost。
4. 数据丢失 检查挂载:
1 docker inspect container-name | grep -A 30 Mounts
检查 volume:
常见错误:
数据库没有挂 volume。
使用了匿名 volume,后来找不到。
执行了 docker volume prune。
bind mount 挂错路径。
把空目录挂到了容器数据目录,遮蔽了原内容。
5. 磁盘被 Docker 占满 查看占用:
详细查看:
清理停止容器:
清理悬空镜像:
清理未使用资源:
谨慎清理所有未使用镜像:
清理 volume 要非常小心:
这可能删掉数据库数据。执行之前先确认:
6. 一键诊断脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #!/usr/bin/env bash set -e CONTAINER_NAME="$1 " if [ -z "${CONTAINER_NAME} " ]; then echo "Usage: ./docker-debug.sh <container-name>" exit 1fi echo "========== docker ps ==========" docker ps -a --filter "name=${CONTAINER_NAME} " echo "========== inspect state ==========" docker inspect -f 'Status={{.State.Status}} ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Error={{.State.Error}}' ${CONTAINER_NAME} || true echo "========== ports ==========" docker port ${CONTAINER_NAME} || true echo "========== mounts ==========" docker inspect -f '{{json .Mounts}}' ${CONTAINER_NAME} || true echo "========== networks ==========" docker inspect -f '{{json .NetworkSettings.Networks}}' ${CONTAINER_NAME} || true echo "========== last logs ==========" docker logs --tail =100 ${CONTAINER_NAME} || true echo "========== stats snapshot ==========" docker stats --no-stream ${CONTAINER_NAME} || true
十六、生产部署脚本示例 1. 单服务部署脚本 deploy.sh:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #!/usr/bin/env bash set -e APP_NAME="order-service" IMAGE_NAME="registry.example.com/backend/order-service" IMAGE_TAG="${1:-latest} " CONTAINER_NAME="order-service" HOST_PORT="8080" CONTAINER_PORT="8080" NETWORK_NAME="app-net" if ! docker network ls --format '{{.Name}}' | grep -q "^${NETWORK_NAME} $" ; then docker network create ${NETWORK_NAME} fi echo "Pull image: ${IMAGE_NAME} :${IMAGE_TAG} " docker pull ${IMAGE_NAME} :${IMAGE_TAG} if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME} $" ; then echo "Remove old container: ${CONTAINER_NAME} " docker rm -f ${CONTAINER_NAME} fi echo "Start new container..." docker run -d \ --name ${CONTAINER_NAME} \ --restart=unless-stopped \ --network ${NETWORK_NAME} \ -p ${HOST_PORT} :${CONTAINER_PORT} \ -e SPRING_PROFILES_ACTIVE=prod \ -e JAVA_OPTS="-XX:InitialRAMPercentage=50 -XX:MaxRAMPercentage=75" \ --memory=1g \ --cpus=1 \ --log-driver=json-file \ --log-opt max-size=100m \ --log-opt max-file=3 \ ${IMAGE_NAME} :${IMAGE_TAG} echo "Deploy success." docker ps --filter "name=${CONTAINER_NAME} "
执行:
1 2 chmod +x deploy.sh ./deploy.sh 1.0.0
2. 回滚脚本 rollback.sh:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #!/usr/bin/env bash set -e APP_NAME="order-service" IMAGE_NAME="registry.example.com/backend/order-service" ROLLBACK_TAG="$1 " if [ -z "${ROLLBACK_TAG} " ]; then echo "Usage: ./rollback.sh <image-tag>" exit 1fi ./deploy.sh ${ROLLBACK_TAG} echo "Rollback success: ${IMAGE_NAME} :${ROLLBACK_TAG} "
3. 零停机的朴素思路 只用原生 Docker 做严格零停机会比较麻烦,但可以实现一个朴素版本:
新容器使用临时端口启动。
健康检查通过后,切换 Nginx upstream。
reload Nginx。
停止旧容器。
流程图:
flowchart TB
A[拉取新镜像] --> B[启动新容器到临时端口]
B --> C{健康检查通过?}
C -->|否| D[停止新容器 / 部署失败]
C -->|是| E[更新 Nginx upstream]
E --> F[reload Nginx]
F --> G[停止旧容器]
G --> H[部署完成]
示例健康检查:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #!/usr/bin/env bash set -e URL="$1 " MAX_RETRY=30if [ -z "${URL} " ]; then echo "Usage: ./health-check.sh <url>" exit 1fi for i in $(seq 1 ${MAX_RETRY} ); do if curl -fsS "${URL} " > /dev/null; then echo "health check success" exit 0 fi echo "health check retry ${i} /${MAX_RETRY} " sleep 2done echo "health check failed" exit 1
十七、常用命令速查 1. 镜像命令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 docker pull nginx:1.27 docker image ls docker image inspect nginx:1.27 docker build -t my-app:1.0.0 . docker build --no-cache -t my-app:1.0.0 . docker tag my-app:1.0.0 registry.example.com/my-app:1.0.0 docker push registry.example.com/my-app:1.0.0 docker rmi my-app:1.0.0 docker history my-app:1.0.0
2. 容器命令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 docker run -d --name app -p 8080:8080 my-app:1.0.0 docker ps docker ps -a docker stop app docker start app docker restart app docker rm app docker rm -f app docker exec -it app sh docker logs -f app docker stats app docker inspect app
3. 网络命令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 docker network ls docker network create app-net docker network inspect app-net docker network connect app-net app docker network disconnect app-net app docker network rm app-net
4. 数据卷命令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 docker volume ls docker volume create mysql-data docker volume inspect mysql-data docker volume rm mysql-data docker volume prune
5. 清理命令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 docker system df docker system df -v docker container prune docker image prune docker network prune docker volume prune docker system prune docker system prune -a
十八、完整 Java 项目 Docker 模板 1. 目录结构 1 2 3 4 5 6 7 8 9 order-service/ ├── Dockerfile ├── .dockerignore ├── deploy.sh ├── docker-debug.sh ├── backup-mysql.sh ├── restore-mysql.sh ├── pom.xml └── src/
2. Dockerfile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 FROM maven:3.9 -eclipse-temurin-17 AS builderWORKDIR /build COPY pom.xml . RUN --mount=type =cache,target=/root/.m2 \ mvn dependency:go-offline COPY src ./src RUN --mount=type =cache,target=/root/.m2 \ mvn clean package -DskipTests FROM eclipse-temurin:17 -jreLABEL app.name="order-service" LABEL app.description="Spring Boot order service" RUN groupadd -r app && useradd -r -g app app WORKDIR /app COPY --from=builder /build/target/*.jar app.jar RUN chown -R app:app /app USER appEXPOSE 8080 ENV JAVA_OPTS="-XX:InitialRAMPercentage=50 -XX:MaxRAMPercentage=75" ENTRYPOINT ["sh" , "-c" , "java $JAVA_OPTS -jar app.jar" ]
3. .dockerignore 1 2 3 4 5 6 7 8 9 10 .git .idea .vscode *.log *.tmp .DS_Store target node_modules README.md .env
4. 构建脚本 build-image.sh:
1 2 3 4 5 6 7 8 9 10 11 #!/usr/bin/env bash set -e IMAGE_NAME="order-service" IMAGE_TAG="${1:-1.0.0} " export DOCKER_BUILDKIT=1 docker build -t ${IMAGE_NAME} :${IMAGE_TAG} .echo "build success: ${IMAGE_NAME} :${IMAGE_TAG} "
5. 运行脚本 run-local.sh:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #!/usr/bin/env bash set -e APP_NAME="order-service" IMAGE_NAME="order-service" IMAGE_TAG="${1:-1.0.0} " NETWORK_NAME="app-net" if ! docker network ls --format '{{.Name}}' | grep -q "^${NETWORK_NAME} $" ; then docker network create ${NETWORK_NAME} fi docker rm -f ${APP_NAME} || true docker run -d \ --name ${APP_NAME} \ --network ${NETWORK_NAME} \ --restart=unless-stopped \ -p 8080:8080 \ -e SPRING_PROFILES_ACTIVE=local \ -e JAVA_OPTS="-Xms256m -Xmx512m" \ ${IMAGE_NAME} :${IMAGE_TAG} docker logs -f ${APP_NAME}
十九、总结 Docker 的核心不是背命令,而是理解它背后的几个抽象:
镜像是只读模板。
容器是镜像运行后的进程和隔离环境。
容器可写层是临时的,重要数据必须挂载 volume。
bridge 网络解决容器通信,端口映射解决外部访问。
EXPOSE 只是声明,-p 才是发布。
Dockerfile 决定镜像质量、构建速度、安全性和可维护性。
多阶段构建是生产镜像瘦身和安全优化的核心手段。
docker commit 能救急,但不能替代 Dockerfile。
日志、健康检查、资源限制、非 root 用户、镜像版本锁定,才是生产环境真正救命的细节。
如果只是在本地跑个 Redis、MySQL,Docker 很简单。 如果要把它用于团队协作、CI/CD、生产部署,那就必须认真对待镜像构建、网络、数据、日志、安全和回滚。
Docker 的真正价值不是“把应用装进容器”,而是让环境、部署和交付变得标准化。标准化之后,团队才有资格谈自动化;自动化之后,才有资格谈规模化。
参考资料