欢迎你来读这篇博客。
这篇文章是一篇系统性的 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 -esudo apt-get remove -y docker docker-engine docker.io containerd runc || true sudo apt-get updatesudo apt-get install -y ca-certificates curl gnupg lsb-releasesudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpgsudo 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/nullsudo apt-get updatesudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-pluginsudo systemctl enable dockersudo 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 10 11 { "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-reloadsudo 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 run:从镜像启动容器的核心命令 在 Docker 的日常使用中,docker run 是最常用、也最容易被低估的命令。
很多人刚开始学 Docker 的时候,会把 docker run 简单理解为“启动一个容器”。这个理解不能说错,但不够完整。更准确地说,docker run 做的是一整套组合动作:
1 docker pull + docker create + docker start
也就是说,当我们执行:
Docker 大致会做这些事情:
检查本地是否存在 nginx 镜像;
如果本地没有,就尝试从远程镜像仓库拉取;
基于镜像创建一个新的容器;
按照参数配置容器的端口、网络、环境变量、数据卷、资源限制等;
启动容器,并执行镜像中定义的默认命令。
所以,docker run 不是单纯的“启动按钮”,它更像是 Docker 容器运行时的总开关。
docker run 的基本语法 docker run 的基本语法如下:
1 docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
其中:
部分
说明
OPTIONS
容器运行参数,例如端口、挂载、环境变量、网络等
IMAGE
要运行的镜像名称
COMMAND
可选,用于覆盖镜像默认启动命令
ARG
可选,传递给命令的参数
最简单的例子:
这条命令会基于 nginx 镜像启动一个容器。
不过,如果直接这样运行,终端会被 Nginx 容器占用。实际使用中,我们通常会加上 -d 参数,让容器后台运行:
如果希望指定镜像版本,应该写清楚 tag:
1 docker run -d nginx:1.27
不要在生产环境中过度依赖 latest。latest 看起来很新,但它不是一个稳定版本承诺,更像是“今天它心情不错,明天它可能换人”。生产环境建议显式指定版本。
docker run 的执行流程 可以用下面这张图理解 docker run 的执行过程:
flowchart TD
A[执行 docker run] --> B{本地是否存在镜像?}
B -- 不存在 --> C[从镜像仓库拉取镜像]
B -- 存在 --> D[创建容器]
C --> D
D --> E[应用运行参数]
E --> F[启动容器主进程]
F --> G{主进程是否持续运行?}
G -- 是 --> H[容器保持运行]
G -- 否 --> I[容器退出]
这里有一个非常关键的点:
容器的生命周期由容器中的主进程决定。
如果主进程结束,容器就会退出。
所以执行下面的命令:
你会发现容器启动后马上退出。
原因不是 Ubuntu 镜像坏了,而是它没有一个持续运行的前台进程。
如果要进入 Ubuntu 容器,需要这样写:
1 docker run -it ubuntu:24.04 bash
前台运行与后台运行 默认情况下,docker run 是前台运行。
例如:
此时终端会被容器占用。
如果希望容器在后台运行,需要使用 -d 或 --detach:
后台运行后,Docker 会返回一个容器 ID:
查看运行中的容器:
查看所有容器,包括已经停止的容器:
实际部署服务时,通常都会使用后台运行:
1 docker run -d --name my-nginx nginx
指定容器名称:–name 如果不指定容器名称,Docker 会自动生成一个随机名称,例如:
1 2 3 hopeful_morse angry_turing sleepy_lovelace
这些名字挺有诗意,但排查问题时不太友好。
建议使用 --name 显式指定容器名称:
1 docker run -d --name my-nginx nginx
之后可以直接通过容器名操作容器:
1 2 3 4 5 6 7 8 9 docker stop my-nginx docker start my-nginx docker restart my-nginx docker logs my-nginx docker exec -it my-nginx bash
如果容器名已经存在,再次创建会报错:
1 docker: Error response from daemon: Conflict. The container name "/my-nginx" is already in use.
解决方式是删除旧容器:
或者换一个新名字:
1 docker run -d --name my-nginx-2 nginx
容器退出后自动删除:–rm 有些容器只是临时使用,比如测试命令、进入临时环境、执行一次性任务。这类容器没有长期保留价值,可以使用 --rm。
例如:
1 docker run --rm alpine echo "hello docker"
容器执行完成后会自动删除,不会残留在 docker ps -a 中。
临时进入 Ubuntu:
1 docker run --rm -it ubuntu:24.04 bash
退出后容器自动删除:
--rm 适合临时测试,不适合数据库、消息队列、业务服务这类需要保留状态的容器。
错误示例:
1 docker run --rm -d --name mysql mysql:8.4
数据库容器退出后直接删除,这就不是容器了,这是“数据消失术”。
交互式运行:-it 进入容器时,经常会看到 -it:
1 docker run -it ubuntu:24.04 bash
其中:
参数
作用
-i
保持标准输入打开
-t
分配一个伪终端
通常 -i 和 -t 会一起使用。
例如进入 Alpine:
1 docker run --rm -it alpine sh
注意,Alpine 默认没有 bash,所以一般使用 sh。
如果是 Ubuntu、Debian 这类镜像,可以使用:
1 docker run --rm -it ubuntu:24.04 bash
端口映射:-p 容器内部的端口默认只能在容器网络中访问,宿主机不能直接访问。
如果要让宿主机访问容器内服务,需要使用 -p 或 --publish 做端口映射。
格式如下:
例如启动 Nginx:
1 2 3 4 docker run -d \ --name my-nginx \ -p 8080:80 \ nginx
含义是:
访问:
就可以看到 Nginx 页面。
如果只希望本机访问,不希望局域网其他机器访问,可以绑定到 127.0.0.1:
1 2 3 4 docker run -d \ --name my-nginx \ -p 127.0.0.1:8080:80 \ nginx
如果服务有多个端口,可以写多个 -p:
1 2 3 4 5 docker run -d \ --name my-service \ -p 8080:8080 \ -p 9090:9090 \ my-service:1.0
注意端口映射顺序不要写反。
正确格式是:
例如:
不是:
除非你真的想让宿主机的 80 端口映射到容器的 8080 端口。
EXPOSE 和 -p 的区别 Dockerfile 中经常会看到:
这表示镜像声明自己会使用 8080 端口。
但是,EXPOSE 只是声明,不代表宿主机可以访问这个端口。
真正让宿主机访问容器端口的是 -p:
1 docker run -p 8080:8080 my-app:1.0
可以这样理解:
说明书写得再漂亮,门没开,别人还是进不来。
环境变量:-e 很多镜像都通过环境变量进行配置,比如 MySQL、Redis、Nacos、Spring Boot 应用等。
使用 -e 或 --env 设置环境变量:
1 2 3 4 docker run -d \ --name mysql \ -e MYSQL_ROOT_PASSWORD=123456 \ mysql:8.4
多个环境变量可以写多个 -e:
1 2 3 4 5 6 docker run -d \ --name my-app \ -e SPRING_PROFILES_ACTIVE=prod \ -e SERVER_PORT=8080 \ -e JAVA_OPTS="-Xms512m -Xmx512m" \ my-app:1.0
如果环境变量较多,可以使用 --env-file:
1 2 3 4 docker run -d \ --name my-app \ --env-file ./app.env \ my-app:1.0
app.env 示例:
1 2 3 SPRING_PROFILES_ACTIVE=prod SERVER_PORT=8080 JAVA_OPTS=-Xms512m -Xmx512m
需要注意,不要把数据库密码、Token、密钥等敏感配置提交到 Git 仓库中。否则 Docker 还没背锅,Git 历史记录已经先把你卖了。
数据挂载:-v 容器本身是临时的。如果容器被删除,容器内部写入的数据也会随之丢失。
所以,对于数据库数据、上传文件、日志文件、配置文件等,需要使用数据挂载。
常见写法是 -v:
1 2 3 4 5 docker run -d \ --name mysql \ -e MYSQL_ROOT_PASSWORD=123456 \ -v mysql-data:/var/lib/mysql \ mysql:8.4
含义是:
1 Docker 数据卷 mysql-data -> 容器目录 /var/lib/mysql
也可以挂载宿主机目录:
1 2 3 4 5 docker run -d \ --name nginx \ -v /opt/nginx/html:/usr/share/nginx/html \ -p 8080:80 \ nginx
含义是:
1 宿主机 /opt/nginx/html -> 容器 /usr/share/nginx/html
两种方式的区别:
类型
示例
适合场景
volume
mysql-data:/var/lib/mysql
数据库、服务数据、Docker 管理的数据
bind mount
/opt/nginx/html:/usr/share/nginx/html
配置文件、静态资源、开发目录映射
简单来说:
1 2 数据长期交给 Docker 管理,用 volume; 需要和宿主机明确共享文件,用 bind mount。
推荐使用 –mount 表达复杂挂载 -v 写法简单,但是语义不够清晰。复杂场景下,更推荐使用 --mount。
数据卷挂载:
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.4
宿主机目录挂载:
1 2 3 4 5 docker run -d \ --name nginx \ --mount type =bind ,source =/opt/nginx/html,target=/usr/share/nginx/html \ -p 8080:80 \ nginx
只读挂载:
1 2 3 4 5 docker run -d \ --name nginx \ --mount type =bind ,source =/opt/nginx/nginx.conf,target=/etc/nginx/nginx.conf,readonly \ -p 8080:80 \ nginx
对比一下:
1 -v /opt/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
和:
1 --mount type =bind ,source =/opt/nginx/nginx.conf,target=/etc/nginx/nginx.conf,readonly
-v 更短,--mount 更清晰。
脚本、生产部署、团队协作时,建议优先使用 --mount,少一点“猜谜式运维”。
设置工作目录:-w 可以通过 -w 或 --workdir 指定容器内工作目录。
例如使用 Node 镜像临时执行构建:
1 2 3 4 5 docker run --rm -it \ -w /app \ -v $(pwd ):/app \ node:22 \ npm install
含义是:
将当前目录挂载到容器 /app;
设置容器工作目录为 /app;
使用 Node 镜像执行 npm install;
执行完后自动删除容器。
使用 Maven 构建 Java 项目:
1 2 3 4 5 6 docker run --rm -it \ -w /app \ -v $(pwd ):/app \ -v ~/.m2:/root/.m2 \ maven:3.9-eclipse-temurin-21 \ mvn clean package -DskipTests
这种方式很适合本机不想安装 Node、Maven、Gradle、JDK 等环境的场景。
容器用完就走,不污染本机环境,干净得像没来过一样。
覆盖默认启动命令 镜像通常会在 Dockerfile 中定义默认启动命令,例如:
1 CMD ["nginx" , "-g" , "daemon off;" ]
使用 docker run IMAGE COMMAND 可以覆盖默认命令。
例如:
1 docker run --rm nginx nginx -v
这不会启动 Nginx 服务,而是执行:
再比如:
1 docker run --rm ubuntu:24.04 date
表示启动 Ubuntu 容器后执行 date 命令,执行完成后容器退出。
覆盖 ENTRYPOINT 有些镜像设置了 ENTRYPOINT,这时直接在镜像后面追加命令不一定能完全覆盖默认行为。
可以使用 --entrypoint 强制指定入口命令:
1 docker run --rm -it --entrypoint sh nginx
或者:
1 docker run --rm -it --entrypoint bash ubuntu:24.04
这个参数在调试镜像时非常有用。
例如某个应用镜像启动后立刻退出,你想进去看看文件结构,可以这样:
1 2 3 docker run --rm -it \ --entrypoint sh \ my-app:1.0
进入后可以检查:
1 2 3 4 5 6 7 ls -lahpwd env cat /etc/os-release
网络模式:–network 容器默认会连接到 Docker 的 bridge 网络。
查看 Docker 网络:
创建自定义网络:
1 docker network create app-net
启动 MySQL:
1 2 3 4 5 6 docker run -d \ --name mysql \ --network app-net \ -e MYSQL_ROOT_PASSWORD=123456 \ -v mysql-data:/var/lib/mysql \ mysql:8.4
启动应用容器:
1 2 3 4 5 6 docker run -d \ --name my-app \ --network app-net \ -e DB_HOST=mysql \ -p 8080:8080 \ my-app:1.0
在同一个自定义网络中,容器可以通过容器名互相访问。
也就是说,应用容器里可以通过下面的地址连接 MySQL:
而不是写宿主机 IP。
这是 Docker 单机部署中非常重要的一点:
容器之间通信,优先使用自定义网络,不要到处写死 IP。
host 网络模式 在 Linux 环境中,可以使用 host 网络模式:
1 2 3 4 docker run -d \ --name my-app \ --network host \ my-app:1.0
host 网络模式表示容器直接使用宿主机网络。
这意味着:
不需要 -p 做端口映射;
容器监听的端口就是宿主机端口;
网络隔离性会变弱;
端口冲突风险更高。
普通业务服务一般优先使用 bridge 网络和 -p 端口映射。
只有在明确需要特殊网络访问、高性能网络、监控采集、主机网络探测等场景时,再考虑 host 网络。
重启策略:–restart 默认情况下,容器异常退出后不会自动重启。
可以使用 --restart 设置重启策略:
策略
含义
no
默认策略,不自动重启
always
容器停止后总是自动重启
unless-stopped
除非手动停止,否则自动重启
on-failure
只有异常退出时才重启
例如启动 Redis:
1 2 3 4 docker run -d \ --name redis \ --restart unless-stopped \ redis:7
对于单机部署的基础服务,比较常用的是:
1 --restart unless-stopped
它比 always 更温和。
如果你手动停止容器,Docker 不会在下次启动 Docker 服务时强行把它拉起来。
限制 CPU 和内存 如果不限制资源,容器在异常情况下可能吃满宿主机 CPU 或内存。
限制内存:
1 2 3 4 docker run -d \ --name my-app \ -m 512m \ my-app:1.0
限制 CPU:
1 2 3 4 docker run -d \ --name my-app \ --cpus="1.5" \ my-app:1.0
同时限制 CPU 和内存:
1 2 3 4 5 docker run -d \ --name my-app \ --cpus="2" \ -m 1g \ my-app:1.0
对于 Java 应用,建议同时配置 JVM 参数:
1 2 3 4 5 6 7 docker run -d \ --name spring-app \ --cpus="2" \ -m 1g \ -e JAVA_OPTS="-Xms512m -Xmx512m" \ -p 8080:8080 \ spring-app:1.0
如果不限制资源,Java 应用可能会根据容器资源或宿主机资源做出不符合预期的内存判断。
容器不是保险箱,资源限制才是刹车片。
指定运行用户:-u 默认情况下,很多容器内部进程是以 root 用户运行的。为了安全,可以使用 -u 指定用户。
1 2 3 4 docker run -d \ --name my-app \ -u 1000:1000 \ my-app:1.0
格式是:
这个参数在挂载宿主机目录时尤其重要。
否则可能出现:
容器内无法写入挂载目录;
容器生成的文件在宿主机上归属 root;
CI/CD 构建产物权限混乱;
为了解决权限问题被迫 chmod 777。
不要一遇到权限问题就 chmod 777。这相当于门锁坏了,直接把门拆了,短期省事,长期刺激。
特权模式:–privileged --privileged 会给容器更高权限:
1 2 3 4 docker run -d \ --name some-container \ --privileged \ some-image
特权模式下,容器可以访问更多宿主机资源。
它适合某些特殊场景,例如:
Docker in Docker;
需要访问宿主机设备;
某些系统级工具;
特殊网络或内核能力需求。
但是普通业务容器不建议默认使用 --privileged。
因为它会明显削弱容器隔离性。
如果只是为了解决某个权限问题,应该优先检查:
文件挂载权限;
容器运行用户;
是否需要指定 capability;
是否真的需要访问宿主机设备。
能不用 --privileged,就不要用。
设置 hosts:–add-host 有时容器内需要访问某个固定域名,但又不想配置 DNS,可以使用 --add-host 添加 hosts 记录。
1 2 3 4 docker run -d \ --name my-app \ --add-host api.local:192.168.1.100 \ my-app:1.0
进入容器后,/etc/hosts 中会出现类似记录:
这样容器内访问:
就会解析到指定 IP。
不过这个方式更适合临时调试。长期环境建议使用 DNS、服务发现或 Docker 自定义网络。
设置主机名:–hostname 可以通过 --hostname 设置容器内部看到的主机名:
1 2 3 4 docker run -d \ --name my-app \ --hostname app-node-1 \ my-app:1.0
进入容器后查看:
输出:
这个参数在日志、监控、集群节点识别时比较有用。
健康检查:–health-cmd 可以在 docker run 时给容器配置健康检查。
例如 Spring Boot 应用:
1 2 3 4 5 6 7 8 9 docker run -d \ --name my-app \ -p 8080:8080 \ --health-cmd="curl -f http://localhost:8080/actuator/health || exit 1" \ --health-interval=30s \ --health-timeout=5s \ --health-retries=3 \ --health-start-period=60s \ my-app:1.0
常见参数:
参数
含义
--health-cmd
健康检查命令
--health-interval
检查间隔
--health-timeout
单次检查超时时间
--health-retries
连续失败多少次后标记为 unhealthy
--health-start-period
容器启动后的宽限期
查看容器状态:
可能看到:
1 2 3 healthy unhealthy starting
对于 Spring Boot 应用,可以配合 Actuator:
需要注意,健康检查命令依赖容器内部环境。如果镜像里没有 curl,健康检查会失败。
这时可以换成 wget,或者在镜像中加入必要的健康检查工具。
docker run 常用参数速查表
参数
作用
示例
-d
后台运行
docker run -d nginx
--name
指定容器名称
--name my-nginx
--rm
容器退出后自动删除
docker run --rm alpine echo hi
-it
交互式终端
docker run -it ubuntu bash
-p
端口映射
-p 8080:80
-e
设置环境变量
-e MYSQL_ROOT_PASSWORD=123456
--env-file
从文件读取环境变量
--env-file ./app.env
-v
数据卷或目录挂载
-v mysql-data:/var/lib/mysql
--mount
更清晰的数据挂载
--mount type=volume,source=data,target=/data
--network
指定网络
--network app-net
--restart
设置重启策略
--restart unless-stopped
-m
限制内存
-m 512m
--cpus
限制 CPU
--cpus="2"
-w
设置工作目录
-w /app
-u
指定运行用户
-u 1000:1000
--entrypoint
覆盖入口命令
--entrypoint sh
--hostname
设置容器主机名
--hostname app-node-1
--add-host
添加 hosts 解析
--add-host api.local:192.168.1.100
--privileged
特权模式
--privileged
--health-cmd
健康检查命令
`–health-cmd=”curl -f http://localhost:8080/health
exit 1”`
实战示例:启动 Nginx 1 2 3 4 5 docker run -d \ --name nginx \ -p 8080:80 \ --restart unless-stopped \ nginx:1.27
访问:
查看日志:
停止容器:
删除容器:
实战示例:启动 Redis 1 2 3 4 5 6 7 docker run -d \ --name redis \ -p 6379:6379 \ -v redis-data:/data \ --restart unless-stopped \ redis:7 \ redis-server --appendonly yes
这里做了几件事:
-p 6379:6379:暴露 Redis 端口;
-v redis-data:/data:持久化 Redis 数据;
--restart unless-stopped:容器异常退出后自动重启;
redis-server --appendonly yes:开启 AOF 持久化。
进入 Redis:
1 docker exec -it redis redis-cli
实战示例:启动 MySQL 1 2 3 4 5 6 7 8 docker run -d \ --name mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -e MYSQL_DATABASE=test_db \ -v mysql-data:/var/lib/mysql \ --restart unless-stopped \ mysql:8.4
进入 MySQL:
1 docker exec -it mysql mysql -uroot -p
查看日志:
如果 MySQL 启动失败,优先看日志,不要靠感觉重启。
1 2 3 docker ps -a docker logs mysql
实战示例:启动 Spring Boot 应用 假设已经构建好了镜像:
1 docker build -t spring-app:1.0 .
启动容器:
1 2 3 4 5 6 7 8 docker run -d \ --name spring-app \ -p 8080:8080 \ -e SPRING_PROFILES_ACTIVE=prod \ -e JAVA_OPTS="-Xms512m -Xmx512m" \ -v /opt/spring-app/logs:/app/logs \ --restart unless-stopped \ spring-app:1.0
查看日志:
1 docker logs -f spring-app
进入容器:
1 docker exec -it spring-app sh
如果是 Java 服务,建议关注:
JVM 参数是否生效;
容器内时区是否正确;
日志目录是否挂载;
端口是否正确映射;
配置文件是否来自正确环境。
实战示例:临时使用 Maven 构建项目 如果本机没有安装 Maven,可以使用 Maven 容器临时构建:
1 2 3 4 5 6 docker run --rm -it \ -w /app \ -v $(pwd ):/app \ -v ~/.m2:/root/.m2 \ maven:3.9-eclipse-temurin-21 \ mvn clean package -DskipTests
这个命令非常适合临时构建 Java 项目。
其中:
参数
说明
--rm
构建完成后删除容器
-w /app
设置工作目录
-v $(pwd):/app
挂载当前项目目录
-v ~/.m2:/root/.m2
复用本地 Maven 依赖缓存
maven:3.9-eclipse-temurin-21
使用 Maven + JDK21 镜像
mvn clean package -DskipTests
执行构建命令
这类用法的优点是:
1 2 3 本机不用装环境; 构建环境可复现; 用完即删,不污染系统。
docker run 常见问题排查 容器启动后立刻退出 查看所有容器:
查看退出日志:
常见原因:
主进程执行完了;
启动命令写错;
配置文件错误;
环境变量缺失;
端口或权限问题;
应用启动时报错。
例如:
会立刻退出,因为没有持续运行的前台进程。
可以改成:
1 docker run -it ubuntu:24.04 bash
或者:
1 docker run -d ubuntu:24.04 sleep infinity
端口访问不了 先看容器是否运行:
再看端口是否映射:
检查容器日志:
进入容器内部测试:
然后测试服务是否在容器内部监听:
1 curl http://localhost:8080
常见原因:
-p 没写;
-p 写反了;
应用只监听了 127.0.0.1;
容器内服务没有启动成功;
宿主机防火墙拦截;
端口被其他进程占用。
数据丢失 如果你这样启动数据库:
1 2 3 4 docker run -d \ --name mysql \ -e MYSQL_ROOT_PASSWORD=123456 \ mysql:8.4
容器删除后,数据也会跟着消失。
更合理的方式是:
1 2 3 4 5 docker run -d \ --name mysql \ -e MYSQL_ROOT_PASSWORD=123456 \ -v mysql-data:/var/lib/mysql \ mysql:8.4
数据库类服务一定要挂载数据卷。
不挂载数据卷跑数据库,就像把工资放进一次性纸杯里,风一吹就很刺激。
环境变量修改后不生效 容器创建之后,很多运行参数不能通过 docker start 修改。
错误理解:
1 docker start -e SPRING_PROFILES_ACTIVE=prod my-app
这是不对的。
如果要修改环境变量,通常需要删除旧容器并重新创建:
1 2 3 4 5 6 7 8 docker stop my-app docker rm my-app docker run -d \ --name my-app \ -e SPRING_PROFILES_ACTIVE=prod \ my-app:1.0
镜像是模板,容器是实例。
容器创建时的参数,很多都是实例级配置,不是随便 start 一下就能改。
docker run 与 Dockerfile、Docker Compose 的关系 docker run 负责启动容器:
1 docker run -d -p 8080:8080 my-app:1.0
Dockerfile 负责定义镜像怎么构建:
1 2 3 FROM eclipse-temurin:21 -jreCOPY app.jar /app/app.jar ENTRYPOINT ["java" , "-jar" , "/app/app.jar" ]
Docker Compose 负责定义一组容器如何一起运行:
1 2 3 4 5 6 7 services: app: image: my-app:1.0 ports: - "8080:8080" environment: SPRING_PROFILES_ACTIVE: prod
可以简单理解:
1 2 3 Dockerfile 解决“镜像怎么做”; docker run 解决“容器怎么跑”; Docker Compose 解决“一组容器怎么一起跑”。
如果只是临时测试,docker run 很方便。
如果命令越来越长,参数越来越多,服务之间还有依赖关系,就应该考虑使用 Docker Compose。
例如下面这种命令:
1 2 3 4 5 6 7 8 9 10 11 12 docker run -d \ --name my-app \ --network app-net \ -p 8080:8080 \ -e SPRING_PROFILES_ACTIVE=prod \ -e DB_HOST=mysql \ -e REDIS_HOST=redis \ -v /opt/my-app/logs:/app/logs \ --restart unless-stopped \ --cpus="2" \ -m 1g \ my-app:1.0
已经开始变得很长了。
这时用 Compose 会更清晰,也更适合版本管理。
小结 docker run 是 Docker 中最核心的命令之一。它不仅仅是启动容器,还把镜像、命令、端口、网络、环境变量、数据卷、资源限制、重启策略等内容组合在一起。
学习 docker run 时,重点掌握下面这些内容:
-d:后台运行;
--name:指定容器名称;
--rm:退出后自动删除;
-it:交互式终端;
-p:端口映射;
-e:环境变量;
-v / --mount:数据挂载;
--network:容器网络;
--restart:重启策略;
--cpus / -m:资源限制;
--entrypoint:覆盖入口命令;
--health-cmd:健康检查。
真正理解 docker run 之后,再学习 Docker Compose、Docker Swarm 或 Kubernetes,就会轻松很多。
因为无论上层编排工具多复杂,本质上都在回答几个问题:
1 2 3 4 5 6 7 容器基于哪个镜像启动? 容器需要哪些端口? 容器需要哪些环境变量? 容器的数据放在哪里? 容器加入哪个网络? 容器挂了以后要不要重启? 容器最多能用多少资源?
把这些问题想清楚,Docker 就不再是黑盒,而是一个可控、可调、可迁移的运行环境。
五、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-namesudo ss -tunlp | grep 8080 docker exec -it container-name sh ss -tunlp curl http://127.0.0.1:8080sudo 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-namesudo 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 的真正价值不是“把应用装进容器”,而是让环境、部署和交付变得标准化。标准化之后,团队才有资格谈自动化;自动化之后,才有资格谈规模化。
参考资料