重新认识 Docker:开发环境、Linux 性能开销与 Redis 实战
从早期用 Docker 统一开发环境,到后来在 Linux 服务器上部署 Redis,重新梳理 Docker 在开发机和服务器上的真实成本、适用边界和实践细节。
我最早接触 Docker,是想解决开发环境不一致的问题。老项目依赖低版本 Node、JDK、Maven、MySQL,换一台机器就可能跑不起来。Docker 的吸引力很直接:把项目依赖的运行环境写进配置文件,让别人拉下代码后用一条命令启动。
后来在 macOS 和 Windows 上长期使用 Docker Desktop,又形成了另一个印象:Docker 很重。启动后风扇转、内存占用上去、文件监听和热更新偶尔还会出问题。这个印象又让我在低配置 Linux 服务器上不敢轻易使用 Docker。
English version: Rethinking Docker: Development Environments, Linux Overhead, and Redis in Practice
直到需要在轻量服务器上部署 Redis 做配置同步,我才重新把这两段经验放在一起看。结论是:Docker 的价值和成本必须区分场景讨论。开发机上的 Docker Desktop、Linux 服务器上的 Docker Engine、用 Compose 编排开发环境、用容器跑 Redis,并不是同一个问题。
Docker 最适合解决什么开发环境问题
2017 年我用 Docker 改过一个 Spring Boot demo。当时的问题很典型:
- 项目需要 JDK 1.8 和 Maven。
- 后端依赖 MySQL。
- 不同开发者的系统不一样。
- 只靠 README 让别人手动装环境,失败概率很高。
那时最朴素的目标是:别人克隆项目后,不需要在本机安装 MySQL,也不需要追问数据库账号密码和初始化脚本,直接 docker-compose up 就能看到接口返回。
这个方向今天仍然成立。Docker 很适合把这些东西从开发者电脑上剥离出去:
- 数据库,例如 MySQL、PostgreSQL、Redis。
- 消息队列、搜索引擎、对象存储模拟器等中间件。
- 需要固定系统依赖的后端服务。
- 多个服务之间的网络关系。
- 初始化脚本、测试数据和本地端口映射。
当时的 demo 用一个 web 容器跑 Spring Boot,用一个 MySQL 容器提供数据库,再由 Compose 统一启动。浏览器打开 localhost:8080 就能看到接口结果。

这类场景里,Docker 解决的是“环境可复制”。它不只是省掉安装步骤,更重要的是把口口相传的环境知识变成仓库里的配置。
开发机上的坑:文件系统和热更新
开发环境并不是只要容器能启动就结束了。前端项目还有热更新、文件监听、依赖安装和大量小文件读写。
我后来在 Docker for Windows 上遇到过一个问题:React 项目放在 Windows 文件系统里,通过 volume 挂到容器内,页面能启动,但编辑文件后容器里的 webpack 不会触发重新编译。文件内容已经同步到容器里,问题出在文件变更通知没有可靠传递。
那个年代的解决方案很偏 workaround:额外跑一个 watcher,把 Windows 里的文件变动转成容器能感知的变动。它解决了当时的问题,但不是一个今天还值得推荐的默认方案。
今天更稳妥的判断是:
- Windows 开发尽量使用 WSL2,并把项目放在 Linux 发行版的文件系统里,而不是放在 Windows 盘再挂载进去。
- macOS 上如果遇到大量小文件 I/O 或热更新变慢,要减少 bind mount 的范围,依赖目录尽量用 named volume。
- 前端热更新如果必须跨宿主机和容器边界,必要时启用工具自身的 polling 模式,但它会增加 CPU 开销。
- 对纯前端项目,不必为了“统一环境”强行把所有开发流程都塞进容器。很多时候本机 Node + 容器中间件更舒服。
Docker Desktop 的文档也把 Windows 上的 WSL2 工作流作为重要路径,并建议代码放在 Linux 发行版内获得更好的开发体验。这个建议和早期踩坑的方向是一致的。
所以,开发机上的 Docker 是一把工具,不是宗教。它适合统一数据库、中间件和后端依赖;对高频热更新的前端开发,要根据文件系统表现做取舍。
为什么 Linux 服务器上的 Docker 轻很多
我以前觉得 Docker 重,主要来自 macOS 和 Windows 上的体验。但这两个系统不能直接运行 Linux 容器,需要 Docker Desktop 在背后准备 Linux 环境。
在 Windows 上,Docker Desktop 通常通过 WSL2 后端运行;在 macOS 上,也需要一个 Linux 虚拟化环境来承载容器。资源占用和文件系统映射开销,很大一部分来自这层虚拟化和宿主机/虚拟机之间的边界。
Linux 服务器上的 Docker Engine 则不同。Docker 官方文档对容器的描述很直接:容器是运行在宿主机上的进程,只是拥有自己的文件系统、网络和进程树隔离。实现隔离主要依赖 Linux 内核能力:
- namespace:隔离进程、网络、挂载点、主机名等视图。
- cgroups:限制和统计 CPU、内存、I/O 等资源。
- union filesystem/overlayfs:让镜像层和容器可写层组合起来。
这意味着在 Linux 上,容器不是一台完整虚拟机。它仍然有开销,但开销通常远小于“每个服务一台虚拟机”的模型。
IBM Research 的容器性能研究也给过类似结论:在很多 CPU、内存和网络基准测试里,Linux 容器接近裸机表现;明显差异更多出现在特定 I/O、网络路径或存储驱动场景。这个结论不能简单翻译成“Docker 永远无损耗”,但足以说明:把 macOS/Windows 上 Docker Desktop 的体感,直接套到 Linux 服务器上是不准确的。
更准确的说法是:
- Docker Desktop:开发体验工具,便利性强,但包含虚拟化层和文件系统映射成本。
- Docker Engine on Linux:服务器运行时,直接使用 Linux 内核能力,适合部署轻量服务。
- Docker Desktop for Linux 也会运行 VM,它和服务器上直接安装 Docker Engine 不是一回事。
这也是我后来敢在轻量服务器上用 Docker 跑 Redis 的原因。
Linux 上也不是完全没有成本
把 Docker 放到 Linux 服务器上,并不代表可以完全不管资源。
几个成本仍然存在:
- 镜像和容器层会占用磁盘,需要定期清理不用的镜像。
- 日志默认可能写到 Docker 管理目录,长时间运行要配置日志轮转。
- bridge 网络和端口映射有一点网络开销。
- overlayfs 对某些写密集型场景不一定是最佳选择。
- bind mount、volume、权限、UID/GID 需要认真处理。
- 容器默认不会自动限制内存,服务失控时仍可能拖垮宿主机。
所以合理做法不是“因为 Docker 很轻就随便跑”,而是给服务加上边界:限制内存、限制日志、持久化数据、明确端口暴露范围。
在 Ubuntu 上安装 Docker Engine
服务器上建议安装 Docker Engine,而不是 Docker Desktop。Docker 官方文档提供了 Ubuntu 的 apt 仓库安装方式,命令会随版本演进,长期以官方页面为准。
一组常见步骤如下:
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
安装后启动并设置开机自启:
sudo systemctl enable --now docker
docker --version
docker compose version
如果不想每次都写 sudo,可以把当前用户加入 docker 组:
sudo usermod -aG docker "$USER"
这个操作需要重新登录才生效。也要注意,能访问 Docker socket 的用户基本等同于能获得宿主机 root 权限,不应该随便给普通账号开放。
用 Compose 跑一个受限制的 Redis
我当时的目标是部署一个轻量 Redis,用来做配置同步。Redis 很适合作为 Docker 实战样本:镜像成熟、启动快、资源占用低,同时又涉及端口、持久化、内存限制和安全配置。
先创建目录:
mkdir -p ~/services/redis-config/data
cd ~/services/redis-config
准备 .env:
REDIS_PASSWORD=change-this-password
准备 compose.yaml:
services:
redis:
image: redis:8-alpine
container_name: redis-config
restart: unless-stopped
ports:
- "127.0.0.1:6379:6379"
command:
- redis-server
- --appendonly
- "yes"
- --maxmemory
- "64mb"
- --maxmemory-policy
- allkeys-lru
- --requirepass
- "${REDIS_PASSWORD:?set REDIS_PASSWORD}"
volumes:
- ./data:/data
mem_limit: 128m
这里有几个关键选择:
- 使用
redis:8-alpine,固定主版本,避免latest随时间漂移。 - 端口绑定到
127.0.0.1,默认只允许本机访问。 - 开启 AOF,把数据写到挂载目录。
- 设置 Redis 自身的
maxmemory和淘汰策略。 - 设置容器内存上限,避免 Redis 或异常情况吃掉整台机器。
- 使用
restart: unless-stopped,服务器重启后自动恢复。
如果 Redis 只是同机应用使用,绑定 127.0.0.1 是更稳妥的默认值。如果需要跨服务器访问或做复制,应该绑定内网 IP,并用云安全组只放行对端内网地址。不要把 Redis 直接暴露到公网。
Docker 官方文档还提醒过一个容易忽略的点:发布容器端口可能绕过宿主机上 ufw 或 firewalld 的部分规则。云服务器上更应该同时依赖安全组、内网地址绑定和服务自身认证,而不是只相信本机防火墙。
启动:
docker compose up -d
docker compose ps
验证:
docker compose exec redis redis-cli
进入后执行:
AUTH change-this-password
PING
返回 PONG 就说明 Redis 正常工作。
观察资源:
docker stats redis-config --no-stream
free -h
如果要看 Redis 自身的内存统计,可以进入 redis-cli 后执行:
AUTH change-this-password
INFO memory
在我的轻量服务器上,一个空载 Redis 容器的内存占用只有几 MB 到十几 MB 级别,CPU 基本可以忽略。实际数据会随 Redis 版本、数据量、配置和宿主机环境变化,但这个量级足以说明:低配置 Linux 服务器跑一个轻量 Redis 容器并不夸张。
什么时候适合用 Docker
这些实践放在一起后,我对 Docker 的判断更清晰了。
适合用 Docker 的场景:
- 需要统一数据库、中间件、后端依赖的开发环境。
- 服务器上部署轻量服务,希望减少手工安装和环境污染。
- 多个服务需要明确网络关系、启动顺序和环境变量。
- 希望通过镜像版本固定运行时。
- 希望服务可以快速迁移到另一台 Linux 机器。
需要谨慎的场景:
- 前端项目在 macOS/Windows 上强依赖大量文件监听和热更新。
- 数据库写入很重,需要仔细评估磁盘、volume、备份和恢复。
- 服务器内存极低,例如 512MB,还要跑多个服务。
- 只会
docker run,但没有规划日志、持久化、安全和升级。 - 把 Docker 当成安全边界,以为容器里出问题不会影响宿主机。
Docker 的最佳位置,是把运行环境变成代码,同时给服务加上清晰边界。它不是为了替代所有本机开发工具,也不是为了掩盖运维设计。
一套更稳妥的默认实践
如果是个人服务器或小项目,我会按下面的方式使用 Docker:
- Linux 服务器安装 Docker Engine,不安装 Docker Desktop。
- 使用
docker compose管理服务,而不是把超长docker run命令散落在笔记里。 - 镜像固定主版本,例如
redis:8-alpine,不要长期依赖latest。 - 数据写到明确的 volume 或宿主机目录。
- 容器设置重启策略和内存上限。
- 服务端口默认绑定
127.0.0.1或内网 IP。 - 需要公网访问时,前面放 Nginx/Caddy/网关,不让数据库类服务裸露。
- 定期查看
docker ps、docker stats、docker logs和磁盘占用。 - 升级镜像前看 release notes,升级后保留回滚路径。
- 对重要数据做宿主机级备份,而不是以为容器还在数据就安全。
这套做法不复杂,但能避免很多“容器跑起来了,后来不好维护”的问题。
总结
我对 Docker 的认知变化,基本经历了三个阶段。
最开始,它是统一开发环境的工具:把 JDK、MySQL、后端服务和网络关系用 Compose 固化下来,减少项目启动成本。
后来,Docker Desktop 在 macOS/Windows 上的体感让我觉得它很重,尤其是文件系统、热更新和资源占用。
再后来,在 Linux 服务器上实际跑 Redis,才发现 Docker Engine 的运行成本和 Docker Desktop 的开发机体验不能混为一谈。对轻量服务来说,Linux 上的 Docker 很实用,关键是配好持久化、资源限制和安全边界。
Docker 不是性能负担的代名词,也不是万能部署答案。它更像一层可复制的运行环境描述。用得克制、边界清楚,就很适合个人项目和小型服务。