Docker 深度入门:从环境、核心原理到镜像构建与生产部署

欢迎你来读这篇博客。

这篇文章是一篇系统性的 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 commitsave/loadexport/import 的区别
  • Dockerfile 指令详解、缓存机制、构建优化
  • Dockerfile 多阶段构建:Java、Node.js、Go 案例
  • 健康检查、日志、资源限制、安全实践、排错脚本

Docker 是一个很典型的“入门很快,写好很难”的技术。你可以三分钟跑起来一个 Nginx,也可以三天排查一个网络和数据卷权限问题。别怕,Docker 这东西表面像集装箱,深入之后像俄罗斯套娃,不过我们今天一层一层拆。

一、为什么需要 Docker?

在没有 Docker 之前,应用部署常见流程大概是这样:

  1. 申请服务器。
  2. 安装 JDK、Node.js、Python、Nginx、MySQL、Redis。
  3. 修改配置文件。
  4. 上传应用包。
  5. 启动服务。
  6. 出问题后开始怀疑人生。

常见问题包括:

  • 开发环境能跑,测试环境不能跑。
  • 测试环境能跑,生产环境不能跑。
  • 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

# 1. 卸载旧版本,避免包冲突
sudo apt-get remove -y docker docker-engine docker.io containerd runc || true

# 2. 安装基础依赖
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release

# 3. 添加 Docker 官方 GPG key
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.gpg

# 4. 添加 Docker apt 仓库
echo \
"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

# 5. 安装 Docker Engine、CLI、containerd、Buildx 插件
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin

# 6. 设置开机自启并启动 Docker
sudo systemctl enable docker
sudo systemctl start docker

# 7. 验证
docker version
docker info

3. 普通用户执行 Docker 命令

默认情况下,执行 Docker 命令可能需要 sudo

开发环境可以把当前用户加入 docker 用户组:

1
2
sudo usermod -aG docker $USER
newgrp docker

验证:

1
docker ps

注意:加入 docker 组的用户基本等价于拥有较高系统权限,因为它可以挂载宿主机目录、启动特权容器。所以在生产环境中,不要随便给用户加这个权限。

4. 验证 Docker 是否正常

1
docker run hello-world

如果能看到 Docker 拉取镜像并输出欢迎信息,说明 Docker 基础环境可用。

5. 查看 Docker 信息

1
docker version

重点看:

  • Client 版本
  • Server 版本
  • API version
  • OS/Arch

查看更详细的信息:

1
docker info

重点看:

  • Storage Driver,一般是 overlay2
  • Cgroup Driver
  • Cgroup Version
  • Docker Root Dir
  • Registry Mirrors
  • Default Runtime
  • Kernel Version

6. 配置 Docker daemon

Docker daemon 配置文件通常是:

1
/etc/docker/daemon.json

一个常用示例:

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

验证:

1
docker info

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 pull nginx:1.27

查看镜像:

1
docker image ls

查看镜像详细信息:

1
docker image inspect nginx:1.27

删除镜像:

1
docker rmi 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

查看运行中的容器:

1
docker ps

查看所有容器:

1
docker ps -a

停止容器:

1
docker stop nginx-demo

启动已停止容器:

1
docker start nginx-demo

删除容器:

1
docker rm nginx-demo

强制删除:

1
docker rm -f nginx-demo

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

其中:

1
镜像名:tag

tag 是一个标签,不等于不可变版本。理论上,同一个 tag 可以被重新推送指向新内容。

更严格的方式是使用 digest:

1
nginx@sha256:xxxxxxxxxxxxxxxx

生产环境建议至少使用明确版本 tag,不建议长期使用 latest

不推荐:

1
docker run nginx:latest

推荐:

1
docker run nginx:1.27

更严格:

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 用来限制和统计进程资源,例如:

  • CPU
  • 内存
  • 磁盘 IO
  • 网络 IO
  • 进程数量

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 机制。

举个例子:

  1. 镜像中有 /app/config.yml
  2. 容器启动后读取该文件,直接从只读层读取。
  3. 容器内修改该文件时,Docker 会先把文件复制到可写层。
  4. 后续读取时看到的是可写层中的新版本。

这个机制带来的影响:

  • 镜像层可以复用,所以多个容器共享同一个基础镜像层。
  • 容器启动很快,因为不用复制完整文件系统。
  • 频繁写入的数据不适合放在容器可写层,应该挂载 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

背后大致做了这些事:

  1. 检查本地是否存在 nginx:1.27 镜像。
  2. 如果不存在,从 Registry 拉取镜像。
  3. 创建容器可写层。
  4. 创建网络命名空间并分配 IP。
  5. 设置端口映射规则。
  6. 挂载文件系统。
  7. 启动容器主进程。
  8. 返回容器 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

# 查看最后 100 行
docker logs --tail=100 nginx-demo

# 查看最近 30 分钟日志
docker logs --since=30m nginx-demo

# 带时间戳
docker logs -t nginx-demo

6. 查看容器资源占用

1
docker stats

查看指定容器:

1
docker stats nginx-demo

7. 查看容器详情

1
docker inspect nginx-demo

常见用途:

1
2
3
4
5
6
7
8
9
10
11
# 查看 IP
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. 查看容器内进程

1
docker top nginx-demo

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 网络

1
docker network ls

默认通常能看到:

1
2
3
bridge
host
none

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
redis:6379

也就是说,应用配置里应该写:

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 下容器访问宿主机,常见方式:

  1. 使用宿主机 docker0 网关 IP,例如 172.17.0.1
  2. 使用 --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 ls

查看网络详情:

1
docker network inspect app-net

查看容器网络:

1
docker inspect app | grep -A 30 Networks

进入容器测试:

1
docker exec -it app sh

容器内测试 DNS:

1
nslookup redis

测试端口:

1
nc -zv redis 6379

临时启动一个网络诊断容器:

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
宿主机 8080 -> 容器 80

访问:

1
curl http://localhost:8080

关系图:

flowchart LR
    A[浏览器 / curl] -->|localhost:8080| B[宿主机 8080]
    B -->|Docker 端口发布 / NAT| C[容器 80]
    C --> D[Nginx 进程]

2. 指定监听地址

默认:

1
-p 8080:80

通常表示绑定到宿主机所有地址,也就是 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。

1
-p 8080:80/tcp

UDP 示例:

1
-p 5353:5353/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 中:

1
EXPOSE 8080

它只是声明容器内应用使用 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

这是很多新手会踩的坑。

如果应用在容器内只监听:

1
127.0.0.1:8080

那么即使你写了:

1
-p 8080:8080

宿主机也可能访问不了,因为应用只接受容器内部 localhost 请求。

容器内服务应该监听:

1
0.0.0.0:8080

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
# 1. 容器是否运行
docker ps

# 2. 端口是否映射
docker port container-name

# 3. 宿主机端口是否监听
sudo ss -tunlp | grep 8080

# 4. 容器内应用是否监听
docker exec -it container-name sh
ss -tunlp

# 5. 容器内本地访问是否成功
curl http://127.0.0.1:8080

# 6. 应用是否绑定 0.0.0.0
# 如果只绑定 127.0.0.1,要修改应用配置

# 7. 防火墙 / 安全组是否放行
sudo ufw status

七、数据卷与持久化

容器的可写层不适合保存重要数据。只要容器被删除,可写层也会消失。

所以,数据库数据、上传文件、业务附件、配置文件、证书、日志等都应该使用挂载机制。

Docker 常见挂载方式有三种:

类型 说明 推荐场景
volume Docker 管理的数据卷 数据库数据、持久化业务数据
bind mount 挂载宿主机目录或文件 开发调试、配置文件、日志目录
tmpfs 挂载到内存 临时文件、敏感临时数据

1. volume 数据卷

创建 volume:

1
docker volume create mysql-data

查看:

1
docker volume ls

查看详情:

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)/html
echo "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
docker exec -it app id

检查宿主机目录权限:

1
ls -ld ./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 1
fi

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 1
fi

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 1
fi

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 html
echo "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.d
cat > 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
ping
set 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:

1
select version();

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

访问管理后台:

1
http://localhost:15672

用户名密码:

1
admin / 123456

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"

访问控制台:

1
http://localhost:9001

7. 部署 Spring Boot 应用

假设项目已经打包:

1
target/app.jar

Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
FROM eclipse-temurin:17-jre

WORKDIR /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.html
exit

提交镜像:

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-jre

WORKDIR /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

设置工作目录。

1
WORKDIR /app

后续 COPYRUNCMDENTRYPOINT 都会基于该目录执行。

不要这样写:

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=/app
ENV 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 .

ARGENV 区别:

项目 ARG ENV
作用阶段 构建阶段 构建阶段 + 运行阶段
容器运行时是否默认存在
典型用途 版本号、构建参数 运行配置

10. EXPOSE

声明容器端口:

1
EXPOSE 8080

它不等于发布端口。发布端口仍然需要:

1
docker run -p 8080:8080 my-app

11. VOLUME

声明挂载点:

1
VOLUME ["/data"]

不过实际生产中,我更建议在运行命令中明确挂载:

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-jre

RUN groupadd -r app && useradd -r -g app app

WORKDIR /app

COPY target/app.jar app.jar

RUN chown -R app:app /app

USER app

EXPOSE 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 builder
WORKDIR /build
COPY . .
RUN build-command

FROM runtime-image
WORKDIR /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
# syntax=docker/dockerfile:1

FROM maven:3.9-eclipse-temurin-17 AS builder

WORKDIR /build

COPY pom.xml .
RUN mvn dependency:go-offline

COPY src ./src
RUN mvn clean package -DskipTests

FROM eclipse-temurin:17-jre

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 app

EXPOSE 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
# syntax=docker/dockerfile:1

FROM gradle:8-jdk17 AS builder

WORKDIR /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-jre

WORKDIR /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
# syntax=docker/dockerfile:1

FROM node:20 AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM nginx:1.27-alpine

COPY --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
# syntax=docker/dockerfile:1

FROM golang:1.23 AS builder

WORKDIR /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 scratch
COPY --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
# syntax=docker/dockerfile:1.7

FROM maven:3.9-eclipse-temurin-17 AS builder

WORKDIR /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-jre
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

这样 Maven 依赖可以走构建缓存,重复构建速度更快。

7. BuildKit secret 避免泄漏密钥

不要这样:

1
2
ARG TOKEN
RUN curl -H "Authorization: Bearer $TOKEN" https://example.com/private.tar.gz

因为构建参数可能进入镜像历史。

BuildKit secret 示例:

1
2
3
4
5
6
# syntax=docker/dockerfile:1.7

FROM alpine
RUN --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、本地仓库、源码、构建缓存,体积大,攻击面也大。

错误二:把源码复制进运行镜像。

1
COPY . .

运行镜像只需要 jar、配置模板、启动脚本,不需要完整源码。

错误三:构建层顺序不合理导致缓存失效。

应优先复制依赖描述文件,再复制源码。


十二、日志、健康检查与重启策略

1. 容器日志原则

容器内应用推荐把日志输出到:

  • stdout
  • stderr

Docker 会接管这些日志。

查看:

1
docker logs -f app

如果应用把日志只写到容器内部文件,一旦容器删除,日志也可能丢失。生产中可以:

  • 输出到 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 ps

查看详细健康检查结果:

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:

1
--memory=512m

不要直接:

1
-Xmx512m

因为 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
ENV DB_PASSWORD=123456

不要:

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. 避免特权容器

不要轻易使用:

1
--privileged

它会给容器大量宿主机权限。除非你明确知道自己在做什么,比如运行 Docker in Docker、某些系统级调试工具,否则不要用。

6. 固定镜像版本

不要生产长期使用:

1
2
mysql:latest
redis:latest

推荐:

1
2
mysql:8.0
redis:7

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 ps -a

查看日志:

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:

1
docker volume ls

常见错误:

  • 数据库没有挂 volume。
  • 使用了匿名 volume,后来找不到。
  • 执行了 docker volume prune
  • bind mount 挂错路径。
  • 把空目录挂到了容器数据目录,遮蔽了原内容。

5. 磁盘被 Docker 占满

查看占用:

1
docker system df

详细查看:

1
docker system df -v

清理停止容器:

1
docker container prune

清理悬空镜像:

1
docker image prune

清理未使用资源:

1
docker system prune

谨慎清理所有未使用镜像:

1
docker system prune -a

清理 volume 要非常小心:

1
docker volume prune

这可能删掉数据库数据。执行之前先确认:

1
docker volume ls

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 1
fi

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 1
fi

./deploy.sh ${ROLLBACK_TAG}

echo "Rollback success: ${IMAGE_NAME}:${ROLLBACK_TAG}"

3. 零停机的朴素思路

只用原生 Docker 做严格零停机会比较麻烦,但可以实现一个朴素版本:

  1. 新容器使用临时端口启动。
  2. 健康检查通过后,切换 Nginx upstream。
  3. reload Nginx。
  4. 停止旧容器。

流程图:

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=30

if [ -z "${URL}" ]; then
echo "Usage: ./health-check.sh <url>"
exit 1
fi

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 2
done

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
# 查看 volume
docker volume ls

# 创建 volume
docker volume create mysql-data

# 查看详情
docker volume inspect mysql-data

# 删除 volume
docker volume rm mysql-data

# 清理未使用 volume,谨慎执行
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 磁盘占用
docker system df

# 查看详细占用
docker system df -v

# 清理停止容器
docker container prune

# 清理悬空镜像
docker image prune

# 清理无用网络
docker network prune

# 清理未使用 volume,谨慎执行
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
# syntax=docker/dockerfile:1.7

FROM maven:3.9-eclipse-temurin-17 AS builder

WORKDIR /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-jre

LABEL 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 app

EXPOSE 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 的真正价值不是“把应用装进容器”,而是让环境、部署和交付变得标准化。标准化之后,团队才有资格谈自动化;自动化之后,才有资格谈规模化。

参考资料


Docker 深度入门:从环境、核心原理到镜像构建与生产部署
https://allendericdalexander.github.io/2026/06/07/devops/cri/docker-deep-dive-blog/
作者
AtLuoFu
发布于
2026年6月7日
许可协议