重新认识 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 Compose 启动开发环境

这类场景里,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 官方文档还提醒过一个容易忽略的点:发布容器端口可能绕过宿主机上 ufwfirewalld 的部分规则。云服务器上更应该同时依赖安全组、内网地址绑定和服务自身认证,而不是只相信本机防火墙。

启动:

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 psdocker statsdocker logs 和磁盘占用。
  • 升级镜像前看 release notes,升级后保留回滚路径。
  • 对重要数据做宿主机级备份,而不是以为容器还在数据就安全。

这套做法不复杂,但能避免很多“容器跑起来了,后来不好维护”的问题。

总结

我对 Docker 的认知变化,基本经历了三个阶段。

最开始,它是统一开发环境的工具:把 JDK、MySQL、后端服务和网络关系用 Compose 固化下来,减少项目启动成本。

后来,Docker Desktop 在 macOS/Windows 上的体感让我觉得它很重,尤其是文件系统、热更新和资源占用。

再后来,在 Linux 服务器上实际跑 Redis,才发现 Docker Engine 的运行成本和 Docker Desktop 的开发机体验不能混为一谈。对轻量服务来说,Linux 上的 Docker 很实用,关键是配好持久化、资源限制和安全边界。

Docker 不是性能负担的代名词,也不是万能部署答案。它更像一层可复制的运行环境描述。用得克制、边界清楚,就很适合个人项目和小型服务。

扩展阅读