我第一次接触 Docker,是在一个凌晨两点的上线现场。当时要在三台服务器上部署一套微服务应用,依赖的中间件有 Nginx、Redis、MySQL、Nacos,每台机器的系统版本还不一样。运维同事用一个 Compose 文件,敲了两条命令,五分钟内把整条链路的服务全部拉起来了。那一刻我就意识到,容器化这东西不只是“又一个部署工具”,它直接改变了我们对环境、交付和运维的认知方式。
这篇随笔不是官方文档的翻译,也不是命令手册的堆砌,而是从我这些年实际使用 Docker 的经验里,把核心概念、安装选型、实战部署、排错思路串成一条线。里面的每一段命令、每一处配置都是我在真实环境里跑过的。如果你刚接触容器化,或者已经用了一阵子但总在奇怪的问题上卡壳,这篇文章应该能帮你把碎片化的知识拼成一张完整的图。
1. 先说清楚 Docker 到底是什么:别急着敲命令
很多人学 Docker 的第一步就是装环境、跑 hello-world,然后跟着教程敲了一堆命令,最后发现还是没搞懂这东西到底在解决什么问题。我建议你先花十分钟把三个核心概念想明白,后面所有操作都会顺理成章。
1.1 容器和虚拟机的本质区别:共用内核和自带内核
传统的虚拟机,比如 VMware 或 VirtualBox,是在宿主机之上模拟出一整套完整的硬件环境,然后在虚拟硬件里装一个完整的操作系统。这意味着每个虚拟机都包含一个完整的 Guest OS,占用几个 GB 的磁盘空间,启动时间以分钟计。
Docker 容器的思路完全不同。它利用 Linux 内核的 Namespace 和 Cgroup 两大特性,在同一个宿主机内核上把进程、文件系统、网络、用户空间隔离成一个个独立的“沙箱”。容器不包含操作系统内核,它只打包应用和执行应用所需的运行环境,比如库文件、依赖、配置。
这里有个很关键的推论:因为所有容器共享宿主机内核,所以 Linux 容器只能在 Linux 上原生运行。Windows 上的 Docker Desktop 是通过 WSL2(Windows Subsystem for Linux)创建一个轻量级 Linux 虚拟机,在虚拟机里跑容器引擎,这才实现了 Windows 下运行 Linux 容器。
用生活化的类比来说:虚拟机是“整租”,你自己装修、自己买家电;容器是“合租”,厨房卫生间都是公用的,你只需要带上自己的牙刷和毛巾。
1.2 镜像、容器、仓库:三件套的关系和核心命令
这三个概念是理解 Docker 的基石,很多人混淆是因为没抓住它们之间的关系。
- 镜像(Image):一个只读的模板,包含应用代码、运行环境、依赖库、配置文件。镜像是由一层一层的只读文件系统叠加而成的,这一层一层的结构就是 Docker 能高效分发和增量存储的根本原因。
- 容器(Container):镜像运行时的实例。当镜像被运行,Docker 会在镜像层之上加一个可写层,应用的所有写入操作都在这个可写层进行。容器可以启动、停止、删除,删除后可写层里的数据也会消失。
- 仓库(Repository):集中存放镜像的地方。官方的是 Docker Hub,国内也有各种镜像加速源。你可以把它理解成应用商店,需要什么镜像就拉什么下来。
对应到命令上,核心的链路是:
bash复制# 拉取镜像
docker pull nginx:latest
# 查看本地镜像列表
docker images
# 基于镜像创建并启动容器
docker run -d --name web-nginx -p 8080:80 nginx:latest
# 查看运行中的容器
docker ps
# 进入容器内部交互式操作
docker exec -it web-nginx bash
# 停止和删除容器
docker stop web-nginx
docker rm web-nginx
# 删除镜像
docker rmi nginx:latest
这套命令你不需要死记,理解了层次关系后自然就能顺下来:pull 拿镜像,run 起容器,exec 进容器,ps 查状态,rm 清环境。
1.3 什么时候该用 Docker,什么时候不该用
这个判断比任何命令都重要。我见过很多人把 Docker 当成万能药,什么项目都往里塞,结果给自己制造了一堆麻烦。
适合用 Docker 的场景:
- 环境不一致问题突出:开发环境跑得好好的,一到测试或生产就崩溃,Docker 把整个运行环境打包后,这个问题直接消失。
- 多版本依赖共存:同一个机器需要跑多个不同版本的 Java、Python、MySQL,用容器隔离是最干净的方案。
- 微服务架构:服务多了之后,每个服务单独部署、单独扩缩容,Docker 加编排工具是标配。
- 快速搭建临时环境:比如测试某个软件、跑个靶场、搭个数据库,用完即焚,不污染宿主机。
- CI/CD 流水线:打包、测试、发布各环节都需要干净且一致的构建环境,容器是最佳载体。
不适合用 Docker 的场景:
- 需要极致性能的计算密集型任务:容器有轻微的性能损耗,虽然很小,但高频计算、高性能计算集群一般不用。
- 桌面级 GUI 应用:虽然可以通过 X11 或 VNC 做图形转发,但体验一般,不如原生安装。
- 需要实时访问宿主机特殊硬件的场景:比如某些加密狗、特殊 USB 设备,容器里适配很麻烦。
- 单机单应用且没有环境冲突:杀鸡不必用牛刀,多一层反而增加运维复杂度。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:安装 Docker 背后的原理和选型
环境安装这部分最容易踩坑的是 Windows 用户。我看到热搜里大量关于“Docker Desktop 安装”“虚拟化未检测到”“D盘安装”“一直 starting”的问题,本质上都是没理解 Docker Desktop 在 Windows 上的运行机制。这里把 Linux 和 Windows 两条路线都说清楚。
2.1 Linux 上安装 Docker:官方源和镜像加速
在 Linux 上安装 Docker 有两种途径,一种是直接用发行版自带的软件源安装(比如 apt 或 yum),另一种是使用 Docker 官方提供的安装脚本。我推荐先用官方脚本安装,因为发行版源里的版本往往偏旧,后续会遇到兼容性问题。
以 Ubuntu 22.04 为例,官方推荐的方式是先卸载旧版本,然后通过官方 apt 源安装:
bash复制# 卸载可能存在的旧版本
sudo apt-get remove docker docker-engine docker.io containerd runc
# 安装依赖
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release
# 添加 Docker 官方 GPG 密钥
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# 设置镜像仓库
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 安装 Docker
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
CentOS 7 的安装思路一致,只是包管理器从 apt 换成了 yum,这里不展开。
安装完成后,需要把当前用户加入 docker 组,否则每次执行 docker 命令都要加 sudo:
bash复制sudo usermod -aG docker $USER
# 重新登录终端后生效
然后是配置镜像加速器。这一步非常重要,特别是国内网络环境,直接从 Docker Hub 拉镜像速度会非常慢。在 /etc/docker/daemon.json 中写入:
json复制{
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://dockerproxy.com",
"https://docker.mirrors.ustc.edu.cn"
]
}
保存后重启 Docker 服务:
bash复制sudo systemctl daemon-reload
sudo systemctl restart docker
提示:镜像加速器只是拉取镜像时使用,不影响已拉取镜像的运行。不同加速源的可用性会变化,建议多配几个备选。
2.2 Windows 上安装 Docker Desktop:WSL2 是核心
Windows 上的 Docker Desktop 从 2022 年开始默认基于 WSL2 运行。它是这样工作的:WSL2 会在 Windows 上运行一个轻量级虚拟机,这个虚拟机内嵌了一个完整的 Linux 内核,Docker Engine 就跑在这个虚拟机里。Windows 上的 Docker CLI 通过管道与虚拟机内的 Engine 通信。
理解了这条链路,很多安装问题就豁然开朗了。比如“Virtualization support not detected”这类报错,本质是 Docker Desktop 在初始化 WSL2 虚拟机时发现宿主机的虚拟化功能没有开启或不可用。
排查和修复顺序如下:
- 进入任务管理器,性能选项卡,查看“虚拟化”是否显示“已启用”。如果显示“已禁用”,需要进 BIOS/UEFI,找到 Intel VT-x(Intel 平台)或 AMD-V(AMD 平台),将其开启。
- 确认 Windows 功能中的“适用于 Linux 的 Windows 子系统”和“虚拟机平台”两个选项都已勾选。控制面板 > 程序 > 启用或关闭 Windows 功能。
- 如果 BIOS 已开启、功能已勾选,但虚拟化仍提示未检测到,可能是 Windows 的 Hyper-V 与第三方安全软件冲突,或者机器本身不支持,建议换装 Linux 环境或使用老版本 Docker Toolbox(不推荐,过时严重)。
Docker Desktop 默认安装在 C 盘,镜像和 WSL 的虚拟磁盘也默认存放在 C 盘。用一段时间后 C 盘会被撑爆。热搜里“Docker Desktop 安装到 D 盘”的需求非常常见。
做法是:不是改 Docker Desktop 的安装路径,而是把 WSL 发行版的虚拟磁盘迁移走。WSL2 默认的发行版文件(ext4.vhdx)存放在用户目录下,迁移步骤是:
powershell复制# 查看当前 WSL 发行版列表
wsl --list -v
# 从 Docker Desktop 中进入的发行版通常叫 docker-desktop
# 关闭 Docker Desktop 后执行导出导入迁移
wsl --shutdown
# 导出发行版(注意备份)
wsl --export docker-desktop D:\wsl-backup\docker-desktop.tar
# 注销原发行版
wsl --unregister docker-desktop
# 重新导入到指定目录
wsl --import docker-desktop D:\Docker-Docker\wsl\docker-desktop D:\wsl-backup\docker-desktop.tar --version 2
同理可以迁移 docker-desktop-data 发行版。整个过程不难,但要注意:执行迁移前必须先退出 Docker Desktop,而且最好先备份原始数据,防止操作失误导致镜像全部丢失。
2.3 Docker 服务起不来的常见原因:从系统级排查
安装完成后,很多人会遇到 Docker 服务一直起不来的问题。Linux 上典型的错误是:
/var/run/docker.sock权限错误:通常是没有正确加入 docker 组,或者组变更后没重新登录。- 内核模块未加载:比如 iptables 相关模块有问题,会导致 Docker 无法初始化网络。
- 存储驱动不兼容:老系统上 overlay2 可能无法正常挂载,需要检查内核版本和文件系统类型。
- cgroup 版本不匹配:Docker 新版要求 cgroup v2,老版本系统还在用 cgroup v1,需要检查内核启动参数。
排查套路很固定:先看服务状态,再看日志,再看内核参数。
bash复制sudo systemctl status docker
sudo journalctl -xu docker
日志是最诚实的信息来源。比如常见的 failed to start daemon: Devices cgroup isn't mounted,说明 cgroup 文件系统没有正确挂载,执行 sudo mount -t cgroup -o devices cgroup /sys/fs/cgroup/devices 或者检查 /etc/fstab 即可。
Windows 上常见的错误还有“We've detected that you have an incompatible version of Windows”,这个是因为 Docker Desktop 新版对 Windows 版本有硬性要求,老版本 Windows 10 需要升级到 21H2 以上,或者装旧版 Docker Desktop。我建议直接升级系统,装旧版只是临时方案,后续功能缺失很影响体验。
3. 必须跑通的 5 个实战场景:从 MySQL 到青龙面板
当环境就绪,真正开始用 Docker 之后,你会发现大部分需求都集中在几个经典场景上。把这些场景跑熟,Docker 的日常用法你已经掌握了大半。
3.1 第一个容器:用 Nginx 理解端口映射和日志
从最简单的 Nginx 开始,不是为了学 Nginx,而是为了理解 Docker 的端口映射机制和日志机制:
bash复制docker run -d --name my-nginx -p 8080:80 nginx:alpine
这条命令做了四件事:从镜像仓库拉取 nginx 的 alpine 版本镜像(如果没有本地缓存的话);基于该镜像创建并启动一个名为 my-nginx 的容器;把宿主机的 8080 端口映射到容器内的 80 端口;容器以后台守护方式运行。
此时在浏览器访问 http://localhost:8080,能看到 Nginx 的默认欢迎页。
这里有一个非常关键的细节:容器内 IP 是动态变化的,端口映射才是你与外界通信的稳定通道。一个容器可以映射多个端口,也可以不映射端口仅供容器间内部通信。
查看容器日志的标准姿势:
bash复制docker logs my-nginx
docker logs -f my-nginx # 实时跟踪日志输出
实操心得:容器日志默认输出到 stdout/stderr,Docker 会将其捕获到宿主机。生产环境一定要配置日志轮转或挂载到独立的日志收集系统,否则一个长时间运行的容器能把磁盘写满。
3.2 安装 MySQL 8.0 并完成初始化配置
数据库容器是使用频率最高的场景。MySQL 8.0 的容器化部署要注意几个关键点:数据持久化、时区设置、字符集、远程访问授权。
先看一个生产环境典型的启动命令:
bash复制docker run -d \
--name mysql8 \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=MyStrongPass123 \
-e TZ=Asia/Shanghai \
-v /data/mysql/conf:/etc/mysql/conf.d \
-v /data/mysql/data:/var/lib/mysql \
--restart=always \
mysql:8.0
拆解一下每个参数的含义:
-e MYSQL_ROOT_PASSWORD=MyStrongPass123:设置 root 密码。这是官方镜像提供的初始化机制,会在首次启动数据目录为空时执行。如果数据目录已经初始化过了,这个环境变量不会再生效。-v /data/mysql/data:/var/lib/mysql:把容器内的数据目录挂载到宿主机的/data/mysql/data。这是容器化数据库的核心操作,容器删了,数据还在。-e TZ=Asia/Shanghai:设置容器时区。MySQL 8.0 默认时区是 UTC,不设置的话,你写入的时间会和本地时间差 8 小时。--restart=always:设置容器退出后自动重启,这是生产环境必不可少的,防止机器重启后数据库没起来。
启动后,用客户端连接测试:
bash复制# 进入容器内部直接使用 mysql 客户端
docker exec -it mysql8 mysql -uroot -p
# 或者在宿主机上连接容器映射的端口
mysql -h 127.0.0.1 -P 3306 -uroot -p
如果你用的是 MySQL 8.0 默认的认证插件 caching_sha2_password,用老客户端(比如 Python 的 MySQLdb 或旧版 Navicat)连接时可能报认证失败。解决办法是在容器内执行:
sql复制ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '你的密码';
FLUSH PRIVILEGES;
提示:容器内默认 root 用户只允许 localhost 连接,如果需要远程访问,要创建远程用户或修改 root 的 host 为
%,并且授权。但生产环境强烈不建议开放 root 的远程访问权限。
初始化配置方面,如果要在容器里修改 MySQL 配置,比如调整最大连接数、开启慢查询日志,可以把自定义配置写入 /data/mysql/conf/my.cnf(即容器的 /etc/mysql/conf.d/),然后重启容器。注意配置文件里的 socket 路径、错误日志路径都要指向容器内的标准位置,不能直接照搬宿主机配置。
3.3 Redis 主从复制:两个容器搞定一主一从
Redis 的容器化部署是最简单的场景之一,因为 Redis 几乎无状态(如果不做持久化的话),而且官方镜像对容器支持非常友好。
一键启动单机 Redis:
bash复制docker run -d --name redis \
-p 6379:6379 \
-v /data/redis/data:/data \
-v /data/redis/conf/redis.conf:/etc/redis/redis.conf \
redis:7.0 redis-server /etc/redis/redis.conf
主从复制的场景在热搜里出现频率很高,这通常是一主一从或者一主多从的读写分离架构。用 Docker 搭建主从很简单,关键在于让从节点知道主节点的地址。在 Docker 网络中,容器之间通过容器名可以直接互相访问,前提是它们在同一个自定义网络里。
先创建一个自定义网络:
bash复制docker network create redis-net
启动主节点:
bash复制docker run -d --name redis-master \
--network redis-net \
-p 6379:6379 \
redis:7.0 redis-server --appendonly yes
启动从节点:
bash复制docker run -d --name redis-slave \
--network redis-net \
-p 6380:6379 \
redis:7.0 redis-server --appendonly yes --slaveof redis-master 6379
验证主从状态:
bash复制docker exec -it redis-slave redis-cli info replication
看到 role:slave 和 master_host:redis-master,说明从节点已成功连接主节点。
这里有个很容易踩的坑:如果两个容器不在同一个自定义网络里,从节点用 --slaveof redis-master 6379 是解析不了这个主机名的,只能填主节点容器的 IP。而容器 IP 在创建时是动态分配的,一旦主节点容器重启,IP 会变,整个主从链路就断了。所以一定要用自定义网络 + 容器名通信,而不是用 IP。
3.4 青龙面板与依赖管理:容器里处理复杂依赖的典型手法
青龙面板(qinglong)是一个定时任务管理面板,很多人在 NAS 或服务器上用 Docker 部署它来跑脚本任务。热搜里的“青龙 依赖管理”是一个很典型的话题,因为它多次踩中容器化的痛点:容器环境是干净的,但应用运行需要很多额外的系统依赖和 Python/Node.js 依赖。
基础的部署命令:
bash复制docker run -d \
--name qinglong \
-p 5700:5700 \
-v /data/qinglong/config:/ql/config \
-v /data/qinglong/log:/ql/log \
-v /data/qinglong/db:/ql/db \
-v /data/qinglong/scripts:/ql/scripts \
-v /data/qinglong/jbot:/ql/jbot \
whyour/qinglong:latest
部署完成后面板里提示缺少依赖,很多人就在面板的“依赖管理”里一个一个装。但我更推荐直接进入容器的终端执行安装,这样能看到完整的安装日志,排查问题更直观:
bash复制docker exec -it qinglong bash
# 在容器内安装 Python 依赖
pip3 install requests aiohttp
# 安装 Node.js 依赖
npm install -g axios
如果容器内缺少系统级依赖,比如编译所需的 gcc、make,需要先执行:
bash复制apk add --no-cache gcc make build-base
因为青龙的官方镜像用的是 Alpine Linux,包管理器不是 apt/yum,而是 apk。
这里值得展开讲的是一个通用方法论:容器内安装依赖的方式取决于基础镜像的类型。Alpine 用 apk,Debian/Ubuntu 用 apt,CentOS 用 yum。拿到一个镜像,先看它在 Docker Hub 上的文档确认基础系统,再选择对应的包管理器,能少走很多弯路。
关于持久化,青龙的四个数据目录必须全部挂载出来:config(配置文件)、log(日志)、db(数据库)、scripts(脚本)。否则容器升级或重建后,你辛苦配置的定时任务和脚本全部丢失。
3.5 用 Docker 快速搭建 DVWA 靶场
DVWA(Damn Vulnerable Web Application)是一个用于安全测试的漏洞练习平台。Docker 让靶场搭建从“手动配置 Web 服务器 + 数据库 + PHP 环境”变成了“一条命令的事”,这一波体验是真的好。
bash复制docker run -d \
--name dvwa \
-p 8088:80 \
vulnerables/web-dvwa:latest
启动后浏览器访问 http://localhost:8088,默认账号密码是 admin/password。
这样一个带 SQL 注入、XSS、文件上传等常见漏洞的靶场就起来了,用完直接 docker stop dvwa,不会污染宿主机。这个场景完美展示了容器化在安全测试、教学演练中的价值:你不需要为一个学习环境专门准备一台服务器,容器就是最好的隔离沙箱。
如果要模拟真实的靶场环境,可以用 docker-compose 同时启动 DVWA 和 MySQL,这个在后面编排的章节会详细说。
3.6 镜像拉取慢和镜像源配置的实操经验
镜像拉取慢的问题,前面在环境配置时已经提过要配置加速器。但结合实战场景,我再补充一些细节:
- 如果加速器配置后仍提示拉取超时,可以试着重启 Docker 服务,确认 daemon.json 没有语法报错。它要求严格的 JSON 格式,多一个逗号都会导致整个服务起不来。
- 部分镜像仓库(比如自建的 Harbor、公司的私有仓库)拉取时可能提示
x509: certificate signed by unknown authority,这是因为 Docker 默认不信任私有证书。解决办法是在 daemon.json 中配置"insecure-registries": ["registry.example.com:5000"]。 - 有时候不是走加速器就能解决的,比如一些通过 build 构建出来的镜像体积特别大,拉取时等待时间长。此时可以用
docker pull加上--platform参数指定架构,避免跨架构传输的问题。 - 拉取 Docker Hub 官方镜像时,建议尽量使用标签明确的版本,比如
mysql:8.0、redis:7.0,少用latest。最新标签会给你“惊喜”,比如某个依赖版本的大版本升级直接导致应用崩溃。
4. 数据持久化与容器网络:生产环境必须掌握的底层机制
容器跑起来很简单,但要让容器可靠地长期运转,必须理解数据持久化和网络通信这两个核心问题。很多半路出家的开发者在这里翻车,容器一删数据全没了,或者多个容器之间连不通。
4.1 数据卷的三种挂载方式
Docker 提供三种数据持久化方式,各有适用场景。
Bind mount(绑定挂载):把宿主机的一个目录挂载到容器内目录。典型场景是配置文件、日志文件、代码目录:
bash复制docker run -d --name nginx -v /home/user/nginx/html:/usr/share/nginx/html nginx
优点是最直观,宿主机上修改文件,容器内立即生效,非常适合开发环境热更新。缺点是宿主机目录和容器生命周期完全解耦,换一台机器部署时,需要保证目录内容同步。
Named volume(命名卷):由 Docker 管理的卷,存储在 Docker 数据目录下(Linux 上默认 /var/lib/docker/volumes/):
bash复制docker volume create mysql-data
docker run -d --name mysql -v mysql-data:/var/lib/mysql mysql:8.0
命名卷的优点是所有数据由 Docker 统一管理,备份和迁移都比较方便,而且不受宿主机文件系统权限的影响。缺点是查找和修改文件不如 bind mount 直观。
tmpfs mount(临时挂载):数据存储在宿主机内存中,容器停止后数据即消失。适用于敏感数据或临时数据:
bash复制docker run -d --name redis --tmpfs /data redis:7.0
实操心得:数据库数据目录强烈建议用命名卷或绑定挂载,绝对不能把数据库数据放在容器的可写层里。容器可写层会随容器的删除而消失,而且写性能也不如挂载卷稳定。
4.2 容器网络模式:bridge、host、none 和自定义网络
Docker 默认提供几种网络模式,理解它们的区别是排查通信问题的基础。
- bridge(默认):容器通过 Docker 创建的虚拟网桥(docker0)与外网通信。每个容器有自己的 IP,容器之间可以互相访问,但外部访问需要端口映射。
- host:容器直接使用宿主机网络栈,没有独立的 IP 和端口映射。容器监听哪个端口,宿主机的对应端口就对外提供服务。好处是网络性能几乎没有损耗,坏处是端口隔离能力消失,容易冲突。
- none:容器没有网络配置,适合完全不需要网络的场景。
- 自定义网络:用
docker network create创建的网络,支持容器名解析、支持 bridge 模式的自定义子网。这是生产环境最推荐的网络模式。
自定义网络的好处我在 Redis 主从的例子里已经演示过。这里再补充一个关键点:自定义 bridge 网络支持使用容器名作为主机名互相访问,让服务之间可以通过逻辑名称找到对方,而不是依赖变化的 IP。这在微服务架构中至关重要。
bash复制# 创建自定义网络,指定子网范围
docker network create --driver bridge --subnet 172.20.0.0/16 my-net
# 启动两个容器到该网络
docker run -d --name app1 --network my-net nginx
docker run -d --name app2 --network my-net nginx
# 在 app1 内访问 app2,使用容器名即可
docker exec -it app1 ping app2
4.3 容器日志、资源限制和优雅停机
生产环境中,容器日志是排障的第一抓手。但如果不加限制,容器日志会像一个“永不停止的打字机”一样,把宿主机磁盘写满。推荐在启动容器时配置日志轮转:
bash复制docker run -d \
--name app \
--log-driver json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
nginx
这样单份日志最大 10MB,保留最近 3 份,旧日志自动清理。
资源限制同样是生产环境不能跳过的一步。不限制资源的容器会“吞噬”宿主机所有可用资源。一个内存泄漏的服务的经典事故现场,就是宿主机被 OOM Killer 杀掉无辜进程。提前加好限制:
bash复制docker run -d \
--name my-app \
--memory=512m \
--cpus=0.5 \
--restart=always \
nginx
优雅停机也是一个容易被忽视的细节。docker stop 在默认情况下会等待 10 秒后发送 SIGKILL 强制终止容器。如果应用需要优雅处理未完成的请求或任务,建议用 --stop-timeout 增加等待时间:
bash复制docker run -d --name app --stop-timeout=30 nginx
5. Docker Compose 编排:从单容器到微服务的跨越
跑单独容器只是入门,真正开启容器化高效工作模式的是 Docker Compose。Compose 用一份 YAML 文件描述多个容器的配置、依赖关系、网络和数据卷,一条命令完成整个应用栈的启停。
5.1 Compose 文件结构和常用指令解析
一个典型的 Compose 文件(以部署一个前后端分离项目为例):
yaml复制version: '3.8'
services:
nginx:
image: nginx:stable-alpine
container_name: web-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/html:/usr/share/nginx/html
networks:
- app-net
depends_on:
- backend
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: app-backend
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_HOST=db
volumes:
- ./logs:/app/logs
networks:
- app-net
depends_on:
- db
restart: always
db:
image: mysql:8.0
container_name: app-db
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: myapp
volumes:
- db-data:/var/lib/mysql
networks:
- app-net
restart: always
volumes:
db-data:
networks:
app-net:
driver: bridge
部分核心指令的解读:
build:指定从 Dockerfile 构建镜像,context是构建上下文目录。depends_on:声明服务依赖关系,Compose 会先启动被依赖的服务。但注意它只控制启动顺序,不等待服务真正可用(比如数据库还没初始化完成时,后端可能已经连接失败)。真实场景中通常需要在应用层做重试机制。volumes:在编排文件里定义命名卷,多个服务可以共享同一个命名卷。networks:服务之间要通信,必须加入同一个网络。上面配置中三个服务都在 app-net 里,后端通过db:3306就能访问 MySQL。
5.2 微服务项目的 Compose 部署实战思路
微服务项目用 Compose 部署时,不同服务通常对应不同的构建上下文。一个标准的做法是:每个微服务有一个自己的 Dockerfile,Compose 统一编排。
解决服务间的服务发现是核心。例如 Spring Boot 项目会用 Nacos 做注册中心,那么 Nacos 本身也作为一个 Compose 服务。服务互相注册时,地址要用服务名而不是 localhost。比如 Nacos 的地址配置为 nacos:8848,数据库地址配置为 db:3306。
我踩过一个很经典的坑:在 depends_on 里指定了数据库,但它不会等待数据库初始化完成。MySQL 容器启动后需要几十秒才能接受连接,而 Java 应用启动也很快,第一次连接数据库大概率失败。解决思路是让应用层做多重重试,或者用 healthcheck 机制等待依赖服务健康:
yaml复制services:
db:
image: mysql:8.0
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 10
backend:
image: my-backend
depends_on:
db:
condition: service_healthy
这样 Compose 会一直等到数据库健康检查通过后再启动后端服务。
5.3 常用 Compose 操作命令和时间差的关键细节
Compose 环境下的常用命令:
bash复制# 后台启动整个栈
docker compose up -d
# 查看服务状态
docker compose ps
# 查看日志
docker compose logs -f
# 只重新构建某个服务并启动
docker compose up -d --build backend
# 停止并移除所有容器(卷默认不会删除)
docker compose down
# 停止并移除所有容器和命名卷(谨慎使用)
docker compose down -v
有一个细节需要特别注意:改了 Compose 文件后,docker compose up -d 会智能地只对发生变更的服务进行重建。比如只修改了后端服务的镜像版本,其他服务不会动,这个过程比想象中平滑得多。
6. 镜像是怎么构建的:Dockerfile 编写和镜像瘦身
前面的部署都在消费现成的镜像,到了这一步,你需要开始为自己的项目构建镜像。这是从“会用 Docker”进阶到“用好 Docker”的关键跳跃。Dockerfile 的编写质量,直接影响镜像大小、构建速度和运行安全。
6.1 Dockerfile 核心指令和基本过程
一个最简单的 Java Spring Boot 项目的 Dockerfile:
dockerfile复制FROM openjdk:17-jdk-slim
WORKDIR /app
COPY target/app.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
核心指令拆解:
FROM:指定基础镜像,所有指令都在这个镜像之上执行。选择基础镜像是一个学问,slim版本比完整版小很多,alpine版本更小但可能缺少 glibc 导致某些应用跑不起来。WORKDIR:设置工作目录,后续的 COPY、RUN、CMD、ENTRYPOINT 都会基于这个目录执行。COPY:从构建上下文复制文件到镜像中。注意 COPY 的源路径是相对于构建上下文的。RUN:在构建过程中执行命令,通常用于安装依赖、编译代码。EXPOSE:声明容器运行时监听的端口,仅起文档作用,不会自动映射宿主机端口。CMD和ENTRYPOINT:指定容器启动时执行的命令,两者的区别在于 CMD 可以被docker run后面的参数覆盖,而 ENTRYPOINT 通常作为固定入口使用。
6.2 多阶段构建:一个命令解决“构建环境”和“运行环境”分离
多阶段构建是 Dockerfile 里最实用的技巧之一。它允许你在一个 Dockerfile 中使用多个 FROM,每个 FROM 开启一个新的构建阶段。前面的阶段用于编译、构建,最后阶段只拷贝产物和运行时依赖,大幅减少镜像体积。
以 Java 项目为例:
dockerfile复制# 第一阶段:使用 Maven 构建项目
FROM maven:3.8-openjdk-17 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests
# 第二阶段:只保留运行环境和产物
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY --from=build /app/target/app.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
这个 Dockerfile 的构建逻辑是:第一阶段的 Maven 镜像里有完整 JDK 和 Maven 工具链,把源码拷贝进去执行打包;打包完成后,第二阶段只把 jar 文件拷贝到精简的 JRE 运行时镜像中。
最终的生产镜像只包含运行环境,不包含 Maven、编译中间文件、测试报告等无用内容,体积可以从 800MB 降到 250MB 左右。
前端项目的多阶段构建同理,第一阶段用 node 镜像执行 npm install && npm run build,第二阶段用 nginx 镜像托管构建产物:
dockerfile复制FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
6.3 镜像瘦身和构建上下文优化的实操经验
镜像体积直接关系到拉取速度、存储占用和启动速度。我总结了几个亲测有效的瘦身方法:
- 优先选择 slim 或 alpine 基础镜像:openjdk:17-jdk 约 400MB,openjdk:17-jdk-slim 约 250MB,alpine 版本更小。要注意的是 alpine 用的 musl libc,不是 glibc,某些 Java 的 JNI 库和原生依赖可能不兼容。
- 合并 RUN 指令,清理中间产物:
dockerfile复制RUN apt-get update && \
apt-get install -y --no-install-recommends some-package && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
--no-install-recommends 参数只装必需依赖,不装推荐包;apt-get clean 清除缓存;删除 /var/lib/apt/lists/* 清理索引文件。这些操作能减少几十到上百 MB 的空间。
- 利用 .dockerignore 排除无关文件:在构建上下文目录创建
.dockerignore文件,排除 node_modules、target、.git、日志等目录,防止这些大文件被 COPY 进镜像。
dockerignore复制node_modules
target
.git
*.log
.idea
- 使用
docker build --no-cache避免缓存导致的陈旧构建:但平时建议保留缓存,能显著加速重复构建。 - 用
docker system df查看空间占用,定期清理无用镜像和构建缓存:
bash复制docker system prune -a
docker builder prune
7. 常见问题与排查套路:我踩过的坑都在这
Docker 使用频率高了,一定会遇到各种怪问题。这里整理一份比较全的排错手册,每个问题都是我或我身边同事真实遇到过、验证过解决方式的。
7.1 Docker Desktop 启动失败类问题
问题:Docker Desktop 一直转圈,或者卡在 starting
排查顺序:先看右下角 Docker 图标的状态 → 打开 Docker Desktop 的诊断日志 → 确认 WSL 状态。
常见原因集中在三个方向:
- WSL2 内核未更新。打开 PowerShell 执行
wsl --update,重启 Docker Desktop。 - Windows 版本过旧。有些功能依赖新版本的系统 API。
- 与 Hyper-V 冲突,或者宿主机本身开过 VT-x 但没有真正生效。
问题:启动时提示 virtualisation support not detected
这个前面环境安装部分已经详细说过,核心就是检查 BIOS 虚拟化开关和 Windows 功能里“虚拟机平台”是否勾选。
7.2 Docker 命令权限问题
问题:Got permission denied while trying to connect to the Docker daemon socket或Cannot connect to the Docker daemon at unix:///var/run/docker.sock
这个问题几乎每个 Linux 新手都会遇到。原因很直接:docker.sock 的所有者是 root,普通用户无权访问。
bash复制# 把当前用户加入 docker 组
sudo usermod -aG docker $USER
# 重新登录或执行 newgrp docker 刷新会话
newgrp docker
注意:newgrp docker 只对当前终端会话生效,新开的终端还是要重新登录一次。加入 docker 组等于给了用户 root 级的权力(Docker 组的用户可以操作宿主机),生产环境要谨慎分配权限。
7.3 容器启动后立刻退出的问题
问题:docker run 后容器状态一直是 Exited
这是新手最容易懵的情况。排查方法很有效:docker logs <容器名>,看容器输出内容。如果日志为空,再用 docker inspect <容器名> 看 State 字段里的 ExitCode 和 Error。
常见的退出原因:
- 前台启动的应用没有保持前台运行,比如在容器里运行
nginx而不是nginx -g "daemon off;",容器启动后没有进程挂起,直接退出。 - 命令行参数写错,比如配置文件路径不存在。
- 环境变量缺失,导致应用启动抛异常后退出。比如 MySQL 没有设置
MYSQL_ROOT_PASSWORD时,有的版本会拒绝启动。 - 镜像架构与宿主机不匹配,比如在 ARM 机器上跑 x86 镜像。
7.4 端口占用和映射冲突
问题:端口绑定失败,提示 address already in use
解决方案:要么停掉占用端口的老容器,要么换新端口:
bash复制# 查看端口被哪个容器占用
docker ps -a | grep 端口
# 或者用 lsof 查看是哪个进程占用
lsof -i:8080
如果是宿主机进程占用(比如你自己的服务占了 8080),那就换容器映射的宿主机端口,例如改成 -p 8081:80。
7.5 容器内时区不对、中文乱码、DPI 不匹配
这些是小毛病但很磨人。
时区问题:容器默认 UTC。统一处理方式有两种:
bash复制# 运行时指定
docker run -d -e TZ=Asia/Shanghai ...
# 或进入容器内修改
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
echo "Asia/Shanghai" > /etc/timezone
中文乱码:容器缺少中文字体或者 locale 设置不正确。安装字体,设置环境变量 LANG=C.UTF-8。
DPI 不匹配:主要出现在图形化应用,比如在容器里跑 VNC 桌面时分辨率不对。通常是设置 DISPLAY 环境变量和屏幕分辨率,这已经不是 Docker 本身的范畴,但确实常见于容器化桌面场景。
7.6 容器时间久了磁盘越来越大
这是长期运行容器的通病。除了日志文件累计,还有悬挂镜像(<none> 镜像)和构建缓存膨胀。
bash复制# 查看空间占用
docker system df
# 清理悬挂镜像
docker image prune
# 清理全部未使用的镜像、容器、网络、数据卷(谨慎)
docker system prune -a --volumes
# 针对日志文件,找到位置后手动清理
ls -lh /var/lib/docker/containers/*/*-json.log
cat /dev/null > /var/lib/docker/containers/*/*-json.log
更优雅的方式是配置日志轮转,前面网络限制和日志部分已经提到。
7.7 MySQL 容器连接到宿主机上的其他服务
有时候容器内应用要访问宿主机上的 MySQL 或 Redis,这时候不能写 localhost 或 127.0.0.1,因为那是指容器自身。Linux 下正确的写法是使用宿主机在容器网络中的网关地址,通常是 172.17.0.1(默认 bridge 网络)。Windows 和 Mac 上 Docker Desktop 提供了特殊地址 host.docker.internal 来访问宿主机。
如果嫌 IP 写死不优雅,可以在启动容器时加上 --add-host=host.docker.internal:host-gateway,然后容器内直接用 host.docker.internal 这个域名访问宿主机服务:
bash复制docker run -d --add-host=host.docker.internal:host-gateway my-app
8. 从 Docker 到容器化思维:几个值得长期坚持的实践习惯
聊完具体的命令和排错,最后想聊聊更长远的东西。Docker 用久了,你会发现它真正改变的是你的部署思维和运维习惯。
8.1 镜像版本管理:只认 tag,不认 latest
latest 标签听起来人畜无害,但它是不稳定的。每次 docker pull xxx:latest 都可能拿到不同的镜像内容。正确做法是:
- 构建镜像时使用明确的版本号 tag,比如
myapp:1.2.3,配合 Git commit hash 更严谨:myapp:1.2.3-abc1234。 - 生产环境部署时锁定具体 tag,回滚也方便:
docker run myapp:1.2.2即可回到上一个稳定版本。 - CI/CD 流水线中打 tag 时,使用构建时间和 commit 组合,方便追溯版本和代码的对应关系。
8.2 容器是“宠物”,不是“牛”
过去我们维护服务器,倾向于把服务器当“宠物”,手动配置环境、手动升级包,希望它一直健康活着。容器化思维更像是养“牛群”,不做个性化维护,出问题就直接换一头。
这个思维的转变,表现在操作习惯上就是:
- 不手动进入容器修改配置。需要修改配置时,重新构建镜像或通过环境变量注入配置。
- 容器状态异常时,不救容器,直接删掉重启新的。
- 重建容器是常态,所以数据卷的挂载必须从一开始就配置好。
8.3 用 Compose 文件代替文档记录部署步骤
在没有容器化以前,团队交接部署流程全靠文档,文档总是会过时。有了 Compose 文件,部署步骤本身就是代码,你只需要一条 docker compose up -d。这不仅减少了沟通成本,也避免了“照着文档做但环境不对”的尴尬。
建议从一开始就把 Compose 文件纳入版本管理,和代码仓库放在一起。这样每次部署都有一个可追溯、可复现的起点。
我在实际使用中越来越强烈的感受是:Docker 的核心价值不在于它用了什么底层技术,而在于它把“环境的可复制性”做到了极致。过去我为环境问题熬夜调试的经历太多了,现在用容器化之后,至少“我这能跑,你那你不能跑”这类问题彻底消失了。
如果你的学习路线还卡在“不知道装什么、不知道从哪里开始”的阶段,建议先别想太深,直接去拉一个 Nginx 镜像跑起来,再跑一个 MySQL,然后把 Dockerfile 从零写一遍。只有亲手动过、踩过坑、看过日志、解决过冲突,这些东西才会真正变成你自己的。
