深入理解 Docker:从入门到实践
在当今的软件开发和运维领域,Docker 已经成为一种不可或缺的技术。它通过容器化的方式,极大地简化了应用程序的开发、部署和运维流程。本文将带你全面了解 Docker,从基本概念、架构原理到实际操作,涵盖安装、常用命令、Dockerfile 构建、网络配置等核心内容。
一、Docker 总览
Docker 是一个开源的容器化平台,允许开发者将应用程序及其依赖项打包到一个轻量级、可移植的容器中。与传统的虚拟机不同,Docker 容器共享宿主机的操作系统内核,因此启动更快、资源占用更少。
为什么使用 Docker?
- 一致性:开发、测试、生产环境一致,避免“在我机器上能运行”的问题。
- 轻量级:容器共享内核,比虚拟机更节省资源。
- 可移植性:一次构建,随处运行。
- 快速部署:容器启动通常在秒级完成。
- 微服务架构:支持将应用拆分为多个独立服务,便于管理和扩展。
二、Docker 架构原理
Docker 采用客户端-服务器(C/S)架构,主要由以下几个核心组件构成:
1. Docker Daemon(守护进程)
运行在宿主机上,负责管理 Docker 对象,如镜像、容器、网络和卷。它监听 Docker API 请求并处理容器的创建、运行、停止等操作。
2. Docker Client(客户端)
用户与 Docker 交互的主要方式。当你运行 docker run
命令时,客户端会将请求发送给 Docker Daemon。
3. Docker Images(镜像)
镜像是只读模板,包含运行应用程序所需的所有内容:代码、运行时、库、环境变量和配置文件。镜像通过分层结构实现,每一层代表一个文件系统的变化。
4. Docker Containers(容器)
容器是镜像的运行实例。你可以启动、停止、移动或删除容器。每个容器都是相互隔离的,并拥有自己的文件系统、网络和进程空间。
5. Docker Registry(注册中心)
用于存储和分发 Docker 镜像。最著名的公共注册中心是 Docker Hub。你也可以搭建私有 Registry。
6. Dockerfile
一个文本文件,包含一系列指令,用于自动化构建 Docker 镜像。
当然可以。下面我们深入探讨 Docker 的底层原理,重点解析它是如何实现“轻量级”、“快速启动”和“环境一致性”的,特别是其核心机制:共享内核、镜像分层、命名空间(Namespaces)、控制组(Cgroups)以及 UnionFS(联合文件系统)。
Docker 的工作原理详解
1. 共享操作系统内核(Shared Kernel)
这是 Docker 与传统虚拟机(VM)最本质的区别。
特性 | 虚拟机(VM) | Docker 容器 |
---|---|---|
架构 | Hypervisor + Guest OS + App | Host OS + Docker Engine + Container |
是否运行完整操作系统 | 是(每个 VM 都有独立的 OS) | 否(只运行应用进程) |
内核使用方式 | 每个 VM 运行自己的内核(通过虚拟化) | 所有容器共享宿主机的 Linux 内核 |
资源开销 | 高(内存、CPU 占用大) | 低(仅应用本身资源) |
启动速度 | 慢(秒到分钟级) | 快(毫秒到秒级) |
✅ 关键点:
Docker 容器并不是一个完整的操作系统,它只是在宿主机上运行的一个或多个进程,并通过 Linux 内核提供的隔离机制,让这些进程“以为”自己运行在一个独立环境中。
这意味着:
- 你不能在 Linux 宿主机上运行 Windows 容器(除非使用 WSL2 等特殊技术)
- 但可以在 Linux 上运行各种基于 Linux 的发行版容器(如 Ubuntu、CentOS、Alpine),因为它们都使用相同的 Linux 内核
2. 如何实现“复用”?—— 镜像分层(Layered File System)
Docker 镜像是由多个只读层(layers)组成的。每一层代表对文件系统的一次修改(比如安装软件、复制文件等)。这种设计实现了高效复用和节省存储空间。
示例:Dockerfile 构建过程中的分层
FROM ubuntu:20.04 # 基础层:Ubuntu 镜像
RUN apt-get update # 第二层:执行更新命令 → 新增一个 layer
RUN apt-get install -y nginx # 第三层:安装 Nginx → 又一个 layer
COPY index.html /var/www/html # 第四层:复制文件
CMD ["nginx", "-g", "daemon off;"]
构建后,镜像结构如下:
Layer 4 (可读写层,容器运行时) ← CMD 执行入口↓
Layer 3 ← COPY index.html ...↓
Layer 2 ← RUN apt-get install nginx↓
Layer 1 ← RUN apt-get update↓
Base Layer ← FROM ubuntu:20.04
复用机制说明:
- 如果两个不同的镜像都基于
ubuntu:20.04
,那么这个基础层只会存储一份。 - 当你再次构建时,如果某一层没有变化(例如
apt-get update
命令未改变),Docker 会直接复用缓存层,无需重新执行。 - 多个容器启动自同一镜像时,它们共享这些只读层,只有最上面的“可写层”是各自独立的。
这就是为什么 Docker 镜像体积小、构建快、部署迅速的原因之一。
3. 隔离机制:Linux Namespaces(命名空间)
为了让容器看起来像是一个独立的系统,Docker 使用了 Linux 内核的 Namespaces 技术,为每个容器提供独立的视图。
常见的 Namespace 类型:
Namespace | 功能 | 容器中表现 |
---|---|---|
PID (Process ID) | 进程隔离 | 容器内只能看到自己的进程,ps aux 不会显示宿主机其他进程 |
NET (Network) | 网络隔离 | 拥有独立的网络栈(IP、端口、路由表) |
MNT (Mount) | 文件系统挂载点隔离 | 容器有自己的挂载目录结构 |
UTS (Unix Timesharing System) | 主机名和域名隔离 | 容器可以有自己的 hostname |
IPC (Inter-Process Communication) | 进程间通信隔离 | 信号量、消息队列等独立 |
USER | 用户 ID 隔离 | 容器内用户与宿主机用户映射不同(支持安全增强) |
✅ 举例:即使你在容器里以 root 身份运行程序,由于 USER namespace
的存在,它在宿主机上可能只是一个普通用户权限,从而提升安全性。
4. 资源限制:Control Groups(Cgroups)
虽然 Namespaces 提供了“隔离”,但如果没有资源控制,某个容器可能会耗尽宿主机的所有 CPU 或内存。
Cgroups(Control Groups) 是 Linux 内核功能,用于:
- 限制资源使用(CPU、内存、磁盘 I/O、网络带宽)
- 统计资源消耗
- 优先级控制
- 进程冻结/恢复
实际命令示例:
# 限制容器最多使用 50% 的 CPU 和 512MB 内存
docker run -d \
--cpus=0.5 \
--memory=512m \
--name limited-app \
nginx
你可以通过以下命令查看资源限制效果:
docker stats limited-app
输出将显示该容器的实际 CPU、内存、网络使用情况。
5. 文件系统:UnionFS(联合文件系统)
UnionFS 是一种“分层”文件系统,允许将多个文件系统“叠加”成一个统一的视图。Docker 利用这一点实现镜像的分层和容器的可写层。
工作原理:
- 镜像的每一层都是只读层
- 当容器启动时,Docker 在最顶层添加一个可写层(Writable Layer)
- 所有对文件的修改(新建、删除、修改)都发生在这一层
- 下层的原始数据保持不变
⚠️ 注意:“写时复制”(Copy-on-Write, CoW)策略:
- 当你尝试修改一个位于底层的文件时,Docker 会先将该文件复制到可写层,然后进行修改。
- 原始层仍然保持不变,保证了镜像的不可变性和多容器共享。
6. 容器是如何运行起来的?流程总结
- 用户执行:
docker run -d nginx
- Docker Client 发送请求给 Docker Daemon
- Daemon 检查本地是否有
nginx
镜像,没有则从 Registry 拉取 - Daemon 创建一个新的容器对象
- 为容器分配:
- 一个独立的 Namespace 集合(PID、NET、MNT 等)
- Cgroup 来限制资源
- 联合文件系统挂载点(只读层 + 可写层)
- 启动容器内的主进程(如
nginx
) - 返回容器 ID,运行完成
整个过程通常在 1 秒以内完成,远快于启动一个虚拟机。
总结:Docker 轻量高效的四大支柱
技术 | 作用 |
---|---|
共享内核 | 避免重复运行操作系统,极大降低资源开销 |
镜像分层 + UnionFS | 实现镜像复用、缓存加速、节省磁盘空间 |
Namespaces | 提供进程、网络、文件系统等维度的隔离,营造“独立环境”假象 |
Cgroups | 控制资源使用,防止“流氓容器”拖垮宿主机 |
正是这四项 Linux 内核技术的结合,使得 Docker 成为现代云原生基础设施的核心组件。
补充:容器 ≠ 虚拟机
维度 | 容器 | 虚拟机 |
---|---|---|
隔离级别 | 进程级隔离 | 硬件级隔离(更强) |
性能损耗 | 极低(接近原生) | 较高(需模拟硬件) |
启动速度 | 快(<1s) | 慢(>10s) |
存储占用 | 小(MB 级) | 大(GB 级) |
安全性 | 依赖内核,风险略高 | 更强(完全隔离) |
使用场景 | 微服务、CI/CD、开发测试 | 多租户、异构系统、强隔离需求 |
✅ 推荐选择:
- 开发、测试、微服务部署 → Docker 容器
- 需要运行不同操作系统或极高安全要求 → 虚拟机
三、Docker 安装指南
1. CentOS 安装 Docker
# 卸载旧版本
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
# 安装依赖
sudo yum install -y yum-utils
sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
# 安装 Docker Engine
sudo yum install docker-ce docker-ce-cli containerd.io
# 启动并设置开机自启
sudo systemctl start docker
sudo systemctl enable docker
# 验证安装
docker --version
2. Debian/Ubuntu 安装 Docker
# 更新包索引
sudo apt-get update
# 安装依赖
sudo apt-get install ca-certificates curl gnupg
# 添加 Docker 官方 GPG 密钥
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# 设置仓库
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 安装 Docker
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# 启动服务
sudo systemctl start docker
sudo systemctl enable docker
3. Windows 安装 Docker
Windows 用户推荐使用 Docker Desktop for Windows:
- 下载 Docker Desktop
- 安装并启动
- 确保已启用 WSL 2 (Windows Subsystem for Linux) 或 Hyper-V
- 安装完成后,Docker 会自动运行
注意:Windows 10/11 Pro 或 Enterprise 版本支持 Hyper-V,家庭版需使用 WSL 2 后端。
四、Docker 常用命令详解
基础命令
命令 | 说明 |
---|---|
docker version | 查看 Docker 版本 |
docker info | 显示 Docker 系统信息 |
docker --help | 查看帮助 |
镜像相关命令
命令 | 说明 |
---|---|
docker images | 列出本地镜像 |
docker pull ubuntu:20.04 | 从仓库拉取镜像 |
docker build -t myapp:v1 . | 构建镜像 |
docker rmi image_id | 删除镜像 |
docker rmi $(docker images -q) | 删除所有镜像 |
docker tag old_image new_name | 重命名镜像 |
容器相关命令
命令 | 说明 |
---|---|
docker run -d -p 8080:80 nginx | 后台运行容器,映射端口 |
docker run -it ubuntu /bin/bash | 交互式运行容器 |
docker ps | 查看运行中的容器 |
docker ps -a | 查看所有容器(包括已停止) |
docker stop container_id | 停止容器 |
docker start container_id | 启动已停止的容器 |
docker restart container_id | 重启容器 |
docker rm container_id | 删除容器 |
docker rm -f container_id | 强制删除运行中的容器 |
docker exec -it container_id /bin/bash | 进入运行中的容器 |
docker logs container_id | 查看容器日志 |
docker inspect container_id | 查看容器详细信息 |
docker stats | 实时查看容器资源使用情况 |
命令区别示例
docker run
vsdocker start
run
是创建并启动新容器start
是启动已存在的容器
docker stop
vsdocker kill
stop
发送 SIGTERM,允许优雅关闭kill
发送 SIGKILL,立即终止
docker rm
vsdocker rmi
rm
删除容器rmi
删除镜像
五、Dockerfile 构建与最佳实践
Dockerfile 示例
# 使用基础镜像
FROM ubuntu:20.04
# 维护者信息
LABEL maintainer="dev@example.com"
# 更新包并安装软件
RUN apt-get update && apt-get install -y \nginx \curl \&& rm -rf /var/lib/apt/lists/*
# 暴露端口
EXPOSE 80
# 复制文件到容器
COPY index.html /var/www/html/
# 设置工作目录
WORKDIR /var/www/html
# 容器启动时执行的命令
CMD ["nginx", "-g", "daemon off;"]
Dockerfile 指令详解
指令 | 说明 |
---|---|
FROM | 指定基础镜像 |
RUN | 执行命令(构建时) |
CMD | 容器启动命令(可被覆盖) |
ENTRYPOINT | 入口点(不易被覆盖) |
COPY | 复制文件到镜像 |
ADD | 类似 COPY,支持 URL 和自动解压 |
ENV | 设置环境变量 |
ARG | 构建参数 |
WORKDIR | 设置工作目录 |
EXPOSE | 声明端口 |
VOLUME | 创建挂载点 |
USER | 切换用户 |
ONBUILD | 触发器指令 |
当然可以!下面将详细讲解 Dockerfile 中常用的指令,包括每个命令的作用、语法、使用场景、注意事项,以及相关指令之间的区别。最后会给出一个完整的、生产级别的构建示例。
一、Dockerfile 常用指令详解
✅ 最佳实践建议:所有 Dockerfile 指令推荐使用
exec
形式(数组语法) 而非 shell 形式,以避免信号传递问题和隐式 shell 启动。
1. FROM
—— 指定基础镜像
FROM ubuntu:20.04
# 或使用多阶段构建
FROM golang:1.21 AS builder
- 作用:定义镜像的起点,所有后续操作基于此镜像。
- 必须是第一个非注释指令。
- 支持指定别名(用于多阶段构建):
AS <name>
- 推荐使用带标签的官方镜像(如
alpine
,debian
,ubuntu
),避免使用latest
标签(不利于可重现性)。
⚠️ 注意:选择轻量基础镜像(如
alpine
)可显著减小最终镜像体积。
2. RUN
—— 构建时执行命令
RUN apt-get update && apt-get install -y nginx
RUN ["apt-get", "clean", "/var/lib/apt/lists/*"]
- 作用:在镜像构建过程中执行命令,并将结果保存为新的一层。
- 支持两种语法:
shell
形式:RUN yum install -y httpd
exec
形式:RUN ["yum", "install", "-y", "httpd"]
(推荐)
- 每个
RUN
指令会创建一个新层,因此建议将多个命令合并以减少层数。
✅ 优化技巧:
RUN apt-get update && \apt-get install -y \nginx \curl \vim && \rm -rf /var/lib/apt/lists/*
3. CMD
—— 容器启动时的默认命令
CMD ["nginx", "-g", "daemon off;"]
CMD ["--port=8080"] # 配合 ENTRYPOINT 使用
- 作用:指定容器运行时的默认命令或参数。
- 一个 Dockerfile 中只能有一个有效
CMD
(最后一个生效)。 - 可被
docker run
命令行参数完全覆盖。
示例:
docker run myapp echo "hi" # 完全覆盖 CMD
4. ENTRYPOINT
—— 定义容器的可执行入口
ENTRYPOINT ["java", "-jar", "/app.jar"]
- 作用:让容器像一个“可执行程序”一样运行。
- 容器启动时,
ENTRYPOINT
是固定主命令,CMD
或docker run
参数作为其参数传入。 - 必须使用
--entrypoint
才能覆盖。
✅ 与 CMD
的核心区别:
组合 | docker run 行为 |
---|---|
ENTRYPOINT ["echo"] CMD ["hello"] | docker run img → 输出 hello docker run img world → 输出 world |
CMD ["echo", "hello"] | docker run img → 输出 hello docker run img world → 输出 world (覆盖整个 CMD) |
总结:
ENTRYPOINT + CMD
= 主程序 + 默认参数
5. COPY
vs ADD
—— 复制文件到镜像
COPY
(推荐)
COPY ./src /app/src
COPY requirements.txt /app/
- 作用:从构建上下文复制文件或目录到镜像中。
- 只支持本地路径。
- 简单、安全、透明,推荐用于大多数场景。
ADD
(慎用)
ADD https://example.com/config.json /app/
ADD archive.tar.gz /app/ # 自动解压
- 功能比
COPY
多:- 支持 URL 下载
- 自动解压
tar.gz
等压缩包
- 不推荐滥用,因为行为不够透明(自动解压可能引发意外)。
✅ 最佳实践:
- 本地文件 → 用
COPY
- 远程文件或需解压 → 显式用
RUN curl/wget && tar
,更可控
6. WORKDIR
—— 设置工作目录
WORKDIR /app
- 作用:为后续的
RUN
,CMD
,ENTRYPOINT
,COPY
,ADD
等指令设置当前工作目录。 - 如果目录不存在,会自动创建。
- 推荐使用绝对路径。
- 可多次使用,路径会叠加。
✅ 示例:
WORKDIR /app WORKDIR src # 实际路径为 /app/src
7. ENV
—— 设置环境变量
ENV NODE_ENV=production
ENV PATH=/app/bin:$PATH
- 作用:定义环境变量,在构建和运行时都可用。
- 可在后续指令中使用
${VAR_NAME}
引用。 - 常用于设置语言、路径、配置参数等。
⚠️ 注意:
ENV
设置的变量在容器运行时仍存在,可用于应用配置。
8. ARG
—— 构建参数
ARG BUILD_VERSION
ARG NODE_ENV=development
RUN echo "Building version: ${BUILD_VERSION}"
- 作用:定义仅在构建阶段使用的变量。
- 不会出现在最终镜像的环境变量中(比
ENV
更安全)。 - 可通过
docker build --build-arg VAR=value
传入。
✅ 与 ENV
的区别:
特性 | ARG | ENV |
---|---|---|
是否存在于运行时容器 | ❌ 否 | ✅ 是 |
是否可用于 RUN 指令 | ✅ 是 | ✅ 是 |
是否可被 --build-arg 覆盖 | ✅ 是 | ❌ 否 |
安全性(敏感信息) | 更高 | 较低(建议不用) |
敏感信息(如密钥)应使用 Docker Secrets 或外部配置管理,不要写在
ARG
或ENV
中。
9. EXPOSE
—— 声明端口
EXPOSE 80/tcp
EXPOSE 443
- 作用:声明容器在运行时会监听的端口。
- 仅是文档说明,不会自动映射端口。
- 实际端口映射需在
docker run -p
时指定。
示例:
docker run -p 8080:80 myapp # 将宿主机 8080 映射到容器 80
10. VOLUME
—— 定义挂载点
VOLUME ["/data", "/config"]
- 作用:创建一个挂载点,用于持久化数据或与宿主机共享目录。
- 在容器运行时,Docker 会自动创建匿名卷或绑定挂载。
- 常用于数据库、配置文件等需要持久化的场景。
⚠️ 注意:
VOLUME
指令在构建阶段不会复制数据到卷中。如果需要初始化数据,应在RUN
中操作目标路径。
11. USER
—— 切换用户
USER www-data
USER 1000:1000
- 作用:指定后续指令以哪个用户身份运行。
- 提高安全性,避免以
root
运行应用。 - 推荐在
CMD
或ENTRYPOINT
前切换到非 root 用户。
✅ 最佳实践:
# 创建用户
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
12. ONBUILD
—— 触发器指令(已过时,不推荐)
ONBUILD COPY . /app/src
ONBUILD RUN npm install
- 作用:当当前镜像被其他镜像
FROM
时,自动执行这些指令。 - 已被现代实践取代(如使用多阶段构建或脚本化构建流程)。
- 不推荐使用。
13. STOPSIGNAL
—— 设置停止信号
STOPSIGNAL SIGTERM
- 作用:指定
docker stop
时发送给容器主进程的信号。 - 默认是
SIGTERM
,允许优雅关闭。 - 对 Java、Node.js 等应用很重要。
14. HEALTHCHECK
—— 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \CMD curl -f http://localhost/ || exit 1
- 作用:定义如何检查容器是否健康。
- Docker 会定期执行该命令,失败则标记容器为
unhealthy
。 - 对编排系统(如 Kubernetes、Swarm)非常重要。
15. LABEL
—— 添加元数据
LABEL maintainer="dev@example.com"
LABEL version="1.0.0"
LABEL description="A web application"
- 作用:为镜像添加键值对形式的元信息。
- 可通过
docker inspect
查看。
二、指令关联与区别总结
指令对 | 区别 |
---|---|
COPY vs ADD | COPY 安全简单;ADD 支持 URL 和自动解压,行为不透明 |
CMD vs ENTRYPOINT | CMD 可被覆盖;ENTRYPOINT 固定入口,参数可变 |
ENV vs ARG | ENV 运行时存在;ARG 仅构建时使用 |
EXPOSE vs -p | EXPOSE 是声明;-p 是实际映射 |
VOLUME vs -v | VOLUME 是声明;-v 是运行时挂载 |
三、完整构建示例:Node.js Web 应用(生产级)
# ================================
# 多阶段构建:第一阶段 - 构建
# ================================
FROM node:18-alpine AS builder
# 设置工作目录
WORKDIR /app
# 复制 package 文件并安装依赖(利用缓存)
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# 复制源码
COPY src ./src
# 构建应用(如果需要)
# RUN npm run build
# ================================
# 第二阶段 - 运行时
# ================================
FROM node:18-alpine
# 设置环境变量
ENV NODE_ENV=production \PORT=3000
# 创建非 root 用户
RUN addgroup -g 1001 -S nodejs && \adduser -S nodejs -u 1001 && \mkdir -p /home/nodejs/app && \chown -R nodejs:nodejs /home/nodejs/app
# 切换到非 root 用户
USER nodejs
# 设置工作目录
WORKDIR /home/nodejs/app
# 从构建阶段复制依赖和源码
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/src ./src
# 暴露端口
EXPOSE 3000
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \CMD node -e 'require("http").get("http://localhost:3000")' || exit 1
# 入口点(可执行程序)
ENTRYPOINT ["node", "src/index.js"]
# 默认参数(可被覆盖)
CMD ["--port=3000"]
四、构建与运行
# 构建镜像
docker build -t my-node-app:1.0 .
# 运行容器
docker run -d \
-p 3000:3000 \
--name myapp \
my-node-app:1.0 \
--port=3000
# 查看日志
docker logs myapp
# 进入容器(调试)
docker exec -it myapp sh
五、总结
指令 | 用途 | 是否推荐 |
---|---|---|
FROM | 基础镜像 | ✅ 必须 |
RUN | 构建命令 | ✅ |
COPY | 复制文件 | ✅ 推荐 |
ADD | 复制+解压+下载 | ⚠️ 慎用 |
CMD | 默认命令 | ✅ |
ENTRYPOINT | 入口程序 | ✅ |
ENV | 环境变量 | ✅ |
ARG | 构建参数 | ✅ |
EXPOSE | 声明端口 | ✅ |
VOLUME | 挂载点 | ✅ |
USER | 切换用户 | ✅ 安全必备 |
HEALTHCHECK | 健康检查 | ✅ 生产推荐 |
LABEL | 元数据 | ✅ |
六、最佳实践总结
- 使用多阶段构建减少镜像体积
- 优先使用
COPY
而非ADD
- 使用
exec
形式避免信号问题 - 非 root 用户运行应用
- 合理使用
.dockerignore
避免不必要的文件进入构建上下文 - 添加
HEALTHCHECK
提升可观测性 - 避免在镜像中存储敏感信息
构建命令
# 构建镜像
docker build -t mywebapp:latest .
# 指定 Dockerfile 路径
docker build -f ./path/Dockerfile.prod -t myapp:prod .
# 使用构建参数
docker build --build-arg VERSION=1.2.3 -t myapp:v1.2.3 .
六、Docker 网络详解
Docker 提供了多种网络模式,用于管理容器间的通信。
1. 默认网络驱动
网络模式 | 说明 |
---|---|
bridge | 默认模式,容器通过虚拟网桥通信 |
host | 容器共享宿主机网络命名空间 |
none | 容器无网络 |
overlay | 用于 Swarm 集群 |
macvlan | 为容器分配 MAC 地址,使其像物理机一样出现在网络中 |
2. 网络命令
命令 | 说明 |
---|---|
docker network ls | 列出网络 |
docker network create mynet | 创建网络 |
docker network inspect mynet | 查看网络详情 |
docker network connect mynet container1 | 将容器连接到网络 |
docker network disconnect mynet container1 | 断开连接 |
docker network rm mynet | 删除网络 |
3. 自定义 Bridge 网络示例
# 创建自定义网络
docker network create --driver bridge mynet
# 启动两个容器并连接到同一网络
docker run -d --name web --network mynet nginx
docker run -d --name db --network mynet mysql:5.7
# 容器间可通过名称通信(如 web 容器可访问 db)
4. 端口映射
# 将容器 80 端口映射到宿主机 8080
docker run -d -p 8080:80 nginx
# 指定 IP 和端口
docker run -d -p 127.0.0.1:8080:80 nginx
# 随机端口映射
docker run -d -P nginx
七、一些例子
当然可以!下面是 Java 应用使用 Docker 环境变量(-e
)的完整实战例子,包括:
- Java 代码如何读取环境变量
- Maven 构建
- Dockerfile 打包
docker run -e
传参- 连接数据库的实际场景
目标
我们写一个简单的 Java 程序,它从环境变量中读取数据库配置,并打印出来:
DB Host: mysql-db
DB Port: 3306
然后打包成 Docker 镜像并运行。
一、Java 项目结构
java-env-demo/
├── src/
│ └── main/java/
│ └── com/example/App.java
├── pom.xml
└── Dockerfile
二、1. 编写 Java 代码:src/main/java/com/example/App.java
package com.example;
public class App {
public static void main(String[] args) {
// 读取环境变量
String dbHost = System.getenv("DB_HOST"); // 如:mysql-db
String dbPort = System.getenv("DB_PORT"); // 如:3306
String dbName = System.getenv("DB_NAME"); // 如:myapp
String dbUser = System.getenv("DB_USER"); // 如:root
String dbPassword = System.getenv("DB_PASSWORD"); // 如:123456
// 如果没设置,默认值
dbHost = dbHost != null ? dbHost : "localhost";
dbPort = dbPort != null ? dbPort : "3306";
dbName = dbName != null ? dbName : "test";
dbUser = dbUser != null ? dbUser : "root";
// 打印配置
System.out.println("==================================");
System.out.println("✅ Java 应用启动成功!");
System.out.println(" 数据库配置:");
System.out.println(" Host : " + dbHost);
System.out.println(" Port : " + dbPort);
System.out.println(" Database : " + dbName);
System.out.println(" User : " + dbUser);
// 密码不打印(安全)
System.out.println("==================================");
// 模拟应用运行(防止容器退出)
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
二、2. Maven 配置:pom.xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>java-env-demo</artifactId>
<version>1.0</version>
<packaging>jar</packaging><properties><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target><exec.mainClass>com.example.App</exec.mainClass></properties><build><finalName>app</finalName><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.3.0</version><configuration><archive><manifest><mainClass>com.example.App</mainClass></manifest></archive></configuration></plugin></plugins></build>
</project>
二、3. 构建 jar 包
mvn clean package
生成:target/app.jar
三、编写 Dockerfile
# 使用官方 OpenJDK 镜像
FROM openjdk:11-jre-slim
# 创建应用目录
WORKDIR /app
# 将打包好的 jar 文件复制到容器
COPY target/app.jar app.jar
# 声明运行时端口(可选)
EXPOSE 8080
# 启动命令
CMD ["java", "-jar", "app.jar"]
四、构建 Docker 镜像
docker build -t java-env-app:1.0 .
五、运行容器并传入环境变量
步骤 1:创建自定义网络(用于容器通信)
docker network create java-app-network
步骤 2:启动 MySQL 容器(模拟数据库)
docker run -d \
--name mysql-db \
--network java-app-network \
-e MYSQL_ROOT_PASSWORD=123456 \
-e MYSQL_DATABASE=myapp \
mysql:8.0
步骤 3:启动 Java 应用容器,通过 -e
传配置
docker run -d \
--name java-app \
--network java-app-network \
-e DB_HOST=mysql-db \
-e DB_PORT=3306 \
-e DB_NAME=myapp \
-e DB_USER=root \
-e DB_PASSWORD=123456 \
java-env-app:1.0
六、查看日志,验证环境变量生效
docker logs java-app
输出:
==================================
✅ Java 应用启动成功!数据库配置:Host : mysql-dbPort : 3306Database : myappUser : root
==================================
✅ 成功!Java 程序正确读取了 -e
设置的环境变量。
七、进阶:在 Spring Boot 中使用
如果你用的是 Spring Boot,环境变量可以直接在 application.yml
中使用:
spring:
datasource:
url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:demo}
username: ${DB_USER:root}
password: ${DB_PASSWORD}
或者用 @Value
注解:
@Value("${DB_HOST:localhost}")
private String dbHost;
运行时传参方式完全一样:
docker run -e DB_HOST=mysql-db -e DB_PORT=3306 spring-boot-app
八、最佳实践建议(Java 场景)
建议 | 说明 |
---|---|
✅ 使用 System.getenv("KEY") 读取 | 简单直接 |
✅ 变量名全大写 + 下划线 | DB_HOST , REDIS_URL |
✅ 提供默认值 | System.getenv("DB_HOST", "localhost") |
✅ 敏感信息不要打印日志 | 避免 System.out.println(password) |
✅ 生产用 Secrets | Docker Swarm Secrets / Kubernetes Secrets |
✅ 使用 .env 文件管理 | 开发时更方便 |
九、总结
你已经学会了:
- ✅ Java 如何通过
System.getenv()
读取-e
设置的环境变量 - ✅ 如何构建 Java 应用并打包成 Docker 镜像
- ✅ 如何用
-e DB_HOST=mysql-db
传数据库配置 - ✅ Java 容器如何通过自定义网络连接 MySQL 容器
- ✅ Spring Boot 中的高级用法
一句话记住:
“Java 应用通过
System.getenv()
拿到-e
传来的‘口令’,就能知道数据库在哪、怎么连!”
这是现代 Java 微服务部署的标准做法,掌握它,你就能轻松部署 Spring Boot、Dubbo、Vert.x 等任何 Java 应用!
好的!这是一个非常全面且深入的问题,涉及 Docker 网络的核心原理。我们来一步步 从零开始,详细讲解:
创建自定义网络 → 容器 IP 分配 → 端口映射 → 多容器映射同一宿主端口 → 网络基础(IP/子网)
目标
我们通过一个完整例子,回答以下问题:
- ✅ 如何创建自定义网络?
- ✅ 容器的 IP 是怎么分配的?
- ✅ 端口映射
-p
是怎么工作的? - ✅ 多个容器可以映射到宿主机的
8888
端口吗?会冲突吗? - ✅ IP 地址分类和子网是什么?Docker 用的是哪一类?
一、创建自定义网络(Bridge 模式)
# 创建一个名为 myapp-net 的自定义桥接网络
docker network create myapp-net
查看网络详情
docker network inspect myapp-net
输出(简化):
[
{
"Name": "myapp-net",
"Driver": "bridge",
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
✅ 关键信息:
- 子网(Subnet):
172.18.0.0/16
→ 可用 IP 范围:172.18.0.2
~172.18.255.254
- 网关(Gateway):
172.18.0.1
→ 宿主机在该网络的“代表” - 驱动(Driver):
bridge
→ Linux 虚拟网桥
二、启动容器并加入网络
# 启动第一个容器(web1)
docker run -d \
--name web1 \
--network myapp-net \
-p 8888:80 \
nginx
# 启动第二个容器(web2)
docker run -d \
--name web2 \
--network myapp-net \
-p 8889:80 \
nginx
三、容器的 IP 是怎么来的?(IP 分配原理)
1. 查看容器 IP
docker inspect web1 | grep IPv4Address
# 输出: "IPv4Address": "172.18.0.2/16"
docker inspect web2 | grep IPv4Address
# 输出: "IPv4Address": "172.18.0.3/16"
2. IP 分配过程
步骤 | 说明 |
---|---|
① Docker Daemon | 检查 myapp-net 的子网 172.18.0.0/16 |
② 选择可用 IP | 找一个未被使用的 IP(如 172.18.0.2 ) |
③ 创建 veth pair | 一对虚拟网卡: - 一端在容器内( eth0 )- 一端在宿主机(如 vethxxxxx ) |
④ 连接到网桥 | 将宿主机端的 veth 连接到虚拟网桥 br-abc123 |
⑤ 配置 IP | 在容器内设置 eth0 的 IP 为 172.18.0.2 |
⑥ 启动容器 | 容器启动,网络就绪 |
容器 IP 来源:由 Docker 在自定义网络的子网范围内自动分配,通常是按顺序分配(
0.2
,0.3
, …)。
四、端口映射 -p
原理:容器 80 → 宿主 8888
命令:
-p 8888:80
意思是:
“把宿主机的
8888
端口映射到容器的80
端口”
原理:基于 iptables
的 NAT(网络地址转换)
# 宿主机上实际添加的 iptables 规则
-A DOCKER -p tcp -d 0/0 --dport 8888 -j DNAT --to-destination 172.18.0.2:80
数据流向(用户访问 http://localhost:8888
):
用户请求↓
[宿主机:8888] ← iptables DNAT 转换↓
[容器 web1:172.18.0.2:80]↓
Nginx 返回响应↓
[容器 → 宿主机] ← iptables SNAT 转换(自动)↓
返回给用户
✅ 关键点:
- 容器内部监听的是
80
端口 - 外部通过宿主机
8888
端口访问 - Docker 自动维护
iptables
规则实现转发
五、多个容器映射到宿主机 8888 端口?会冲突吗?
❌ 情况 1:两个容器都映射 8888:80
# 第一个可以
docker run -d -p 8888:80 --name web1 --network myapp-net nginx
# 第二个会失败!
docker run -d -p 8888:80 --name web2 --network myapp-net nginx
错误:
Error: port is already allocated
❌ 会冲突!同一个宿主端口只能被一个容器占用。
✅ 情况 2:不同宿主端口,相同容器端口(推荐)
# web1 映射 8888 → 80
docker run -d -p 8888:80 --name web1 nginx
# web2 映射 8889 → 80
docker run -d -p 8889:80 --name web2 nginx
✅ 成功!访问:
http://localhost:8888
→ web1http://localhost:8889
→ web2
✅ 情况 3:同一个宿主端口,但不同 IP(多网卡场景)
如果宿主机有多个 IP(如 192.168.1.10
, 192.168.1.11
):
# 绑定到不同 IP 的 8888 端口
docker run -d -p 192.168.1.10:8888:80 --name web1 nginx
docker run -d -p 192.168.1.11:8888:80 --name web2 nginx
✅ 成功!因为监听的是不同 IP 的同一端口。
✅ 情况 4:使用反向代理统一入口(生产推荐)
用 Nginx 或 Traefik 做反向代理,统一监听 8888
,然后根据路径或域名转发:
# nginx.conf
server {listen 8888;location /app1/ {proxy_pass http://web1:80/;}location /app2/ {proxy_pass http://web2:80/;}
}
✅ 实现:单端口 → 多服务
六、IP 地址分类与子网基础(Docker 网络基础)
1. IPv4 地址分类(A/B/C 类)
类别 | 范围 | 子网掩码 | 用途 |
---|---|---|---|
A 类 | 1.0.0.0 ~ 126.255.255.255 | 255.0.0.0 (/8) | 大型网络(政府、ISP) |
B 类 | 128.0.0.0 ~ 191.255.255.255 | 255.255.0.0 (/16) | 中型网络(企业) |
C 类 | 192.0.0.0 ~ 223.255.255.255 | 255.255.255.0 (/24) | 小型网络(家庭) |
D 类 | 224.0.0.0 ~ 239.255.255.255 | - | 组播 |
E 类 | 240.0.0.0 ~ 255.255.255.255 | - | 保留 |
2. 私有 IP 地址(RFC 1918)
这些 IP 不会在互联网上路由,专用于内部网络(Docker 用的就是这些):
范围 | 子网 | 用途 |
---|---|---|
10.0.0.0/8 | 10.0.0.0 ~ 10.255.255.255 | 大型私有网络 |
172.16.0.0/12 | 172.16.0.0 ~ 172.31.255.255 | 中型私有网络 ✅ Docker 默认用这个 |
192.168.0.0/16 | 192.168.0.0 ~ 192.168.255.255 | 家庭/小型网络 |
Docker 默认桥接网络用
172.17.0.0/16
自定义网络从172.18.0.0/16
开始递增(172.19
,172.20
…)
3. 子网(Subnet)与 CIDR 表示法
172.18.0.0/16
表示:- 网络地址:
172.18.0.0
- 子网掩码:
255.255.0.0
- 可用 IP 数:
2^(32-16) - 2 = 65534
个 - 可用范围:
172.18.0.1
~172.18.255.254
- 网络地址:
/16
表示前 16 位是网络号,后 16 位是主机号。
4. 为什么 Docker 用 172.x
而不用 192.168
?
192.168
常用于家庭路由器(如192.168.1.1
)- 如果 Docker 也用
192.168
,可能和宿主机网络冲突 172.16.0.0/12
范围大,足够 Docker 动态分配
七、总结:完整原理图
宿主机
┌─────────────────────────────────────────────────────────────┐
│ │
│ iptables 规则 │
│ -A DOCKER -p tcp --dport 8888 -j DNAT --to 172.18.0.2:80 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Linux Bridge: br-abc123 (172.18.0.1) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ veth-A │ │ veth-B │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ │ │ │ │ │
│ └─────────┼─────────────────────┼─────────────────────┘ │
│ │ │ │
│ ┌───▼───┐ ┌───▼───┐ │
│ │ web1 │ │ web2 │ │
│ │172.18.0.2│ │172.18.0.3│ │
│ └───────┘ └───────┘ │
│ │
│ 宿主端口: 8888 8889 │
│ 映射到: web1:80 web2:80 │
│ │
└─────────────────────────────────────────────────────────────┘
八、关键结论
问题 | 回答 |
---|---|
容器 IP 怎么来? | Docker 在自定义网络的子网内自动分配(如 172.18.0.2 ) |
端口映射原理? | iptables DNAT 实现宿主端口 → 容器 IP:端口 转发 |
多个容器映射 8888? | ❌ 不行!宿主端口唯一,会冲突 |
如何解决? | 用不同宿主端口,或反向代理统一入口 |
Docker 用什么 IP? | 私有 IP 172.16.0.0/12 范围,避免冲突 |
子网 /16 是什么? | 表示前 16 位是网络号,可用 6.5 万个 IP |
一句话记住:
“Docker 为每个自定义网络创建一个‘虚拟局域网’,容器 IP 自动分配,端口映射靠
iptables
转发,宿主端口不能重复占用。”
掌握这些原理,你就能游刃有余地设计和调试任何 Docker 网络架构!