好几年前我刚接触服务器部署的时候,也干过一件蠢事:拿到一台全新的服务器,先装个 Nginx,再装 MySQL,然后又装 Redis,每装一个都要处理一堆依赖、编译报错、版本冲突。等折腾完一轮,系统已经变得又脏又乱,换台机器又得从头再来一遍。后来我彻底转向“主机只装 Docker,所有服务都进容器”的思路,才发现这才是省心省力的正解。这篇文章就结合我自己实际配置一台“只支持 Docker 的服务器”的过程,把方案设计、环境准备、镜像加速、常用服务部署和故障排查完整过一遍,给正在折腾 Docker 的你看个明白。
这个方案的核心思路很简单:主机系统能跑多干净就多干净,只保留一个 Docker 运行时,其余 MySQL、Redis、Nginx、定时任务面板等等全都用容器来跑。它特别适合下面这几类人:
- 手上有一台配置不高的小内存 VPS 或云主机,想省资源又不想被各种编译折腾;
- 需要在多台机器上快速复现同一套环境,比如测试环境、预发环境;
- 刚接触 Linux 服务器,不想一上来就被各种“源码编译装软件”劝退的人;
- 或者你已经踩过“装 A 软件破坏了 B 软件依赖”这种坑的老手。
1. 整体设计思路与方案选型
1.1 为什么选择“主机只装 Docker”这套方案
先说结论:如果你想长期维护一台服务器,把服务容器化是性价比最高的路子。原因其实不复杂,就是隔离和标准化。
我以前在裸机上部署过 MySQL 8.0,过程极其痛苦:要自己解压二进制包、建 mysql 用户、初始化数据目录、配置 systemd 服务、处理日志轮转,还要考虑 glibc 版本兼容性。一旦系统升级或者误操作删了什么依赖,MySQL 可能直接起不来。而用 Docker 部署的话,这些都是镜像里现成的东西。
用一句生活化的话来类比:裸机装软件就像你在自己家里开餐厅,锅碗瓢盆、食材储藏、灶具维修全都得自己管;Docker 部署则像直接租了个中央厨房,东西都是现成的,你只需要把菜谱(compose 文件)带过去,随时可以开张,不带任何私人物品也能干活。
这套方案还能保证多台机器环境一致。我在本地的 Ubuntu 上写好了 docker-compose.yml,测试没问题,丢到线上的 CentOS、Debian 服务器上,同样能跑起来,基本不会出现“本地好好的,线上就炸”的问题。
1.2 系统选型:Ubuntu 24.04 LTS 是首选
服务器操作系统我首选Ubuntu 24.04 LTS。原因有三点:
第一,Ubuntu 的 apt 源对 Docker 官方源支持最好,官方安装文档直接提供apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin,不用自己折腾依赖关系。
第二,24.04 LTS 的内核版本是 6.8,对 cgroup v2、iptables、overlay2 这些 Docker 存储和网络底层组件的支持都很成熟,很少出现内核模块加载失败的问题。
第三,Ubuntu 社区活跃,你遇到的 Docker 报错,90% 都能在网上搜到现成的解决方案。如果你手头是 CentOS 7 这类老系统,建议优先考虑重装成 Ubuntu,省下的折腾时间远超重装成本。
1.3 Docker Engine 与系统资源的取舍
很多人一开始容易混淆:Docker Desktop 是给 Windows/macOS 上的开发者用的,带图形界面、自带虚拟机;而我们做服务器部署,用的应该是Docker Engine,它是以守护进程方式运行在 Linux 系统里的,资源占用极小,纯命令行操作。
服务器上跑 Docker Engine + containerd + 几个常用容器,空载状态下内存占用也就 100~200MB 左右,对于 2GB 内存的小服务器来说完全没压力。我刚买的那台 2C2G 的小主机,跑起来 MySQL 8.0(分配 512MB)加 Redis 主从加青龙面板,内存还能剩下 400MB 多,非常够用。
2. 核心细节解析与实操要点
2.1 服务器底座的系统初始化
拿到云服务器或者物理机以后,第一步不是急着装 Docker,而是先把系统的“底子”打好。这一步做得好,后面能省掉很多不必要的坑。
我一般会做这么四件事:
- 更新系统软件源并升级现有软件包:
apt update && apt upgrade -y,这步是为了让系统处于最新状态,避免安装 Docker 时出现依赖冲突; - 设置主机名和时区:用
timedatectl set-timezone Asia/Shanghai把时区改成北京时间。很多容器默认用的是 UTC 时区,如果主机时区不对,容器里的日志时间也全是乱的; - 配置时间同步。Docker 容器默认共享宿主机的网络命名空间和内核时钟,如果宿主机时间不准,容器里跑的任何业务都会受影响,比如登录票据校验失败、定时任务执行时间错乱。我强烈建议装一个 NTP 服务,
apt install chrony -y,然后systemctl enable --now chronyd,用公共 NTP 服务器做时间校准; - 调整防火墙规则。如果你用iptables 或者 ufw,得提前看清默认策略。很多云服务器安全组默认只放行 22 端口,你提前把 80、443 和容器映射的端口放行了,免得后面容器的端口映射好了,外部访问却一直超时。
2.2 Docker 安装:官方源一步到位
系统初始化好以后,就能装 Docker 了。我更推荐用 Docker 官方提供的 apt 源安装,而不是直接用系统自带源,好处是版本最新、bug 修复及时。
操作顺序是这样的:
# 安装依赖工具 apt install apt-transport-https ca-certificates curl gnupg lsb-release -y # 添加 Docker 官方 GPG 密钥 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg # 添加 Docker apt 源 echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null # 更新并安装 apt update apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y这里有个坑要注意:如果服务器在国内,download.docker.com可能访问不稳定,安装就可能卡在curl这一步。我自己的处理办法是把 GPG 密钥和 apt 源地址换成一个国内可正常访问的公共镜像源,具体根据你可以访问的网络环境选。
装完以后执行systemctl enable --now docker,然后docker version,看到客户端和服务端版本都有输出,说明 Docker 已经活了。
2.3 配置镜像加速,解决拉取超时
服务端能跑起来只是第一步,紧接着就会遇到“拉镜像拉不动”的经典问题。docker pull mysql:8.0半天没动静,甚至直接超时,非常打击信心。
解决办法是配置 registry mirror,也就是镜像加速器。你需要在/etc/docker/daemon.json里加上这样一个配置:
{ "registry-mirrors": ["https://你自己的加速地址"], "log-opts": { "max-size": "10m", "max-file": "3" } }注意两点:
第一,registry-mirrors的地址一定要填你能正常访问的公共镜像加速地址。不同网络环境下可用性不一样,建议先curl -I探一下通不通,再写进配置文件。改完配置文件必须执行systemctl restart docker才生效。
第二,我顺手把容器的日志大小限制加上了。不然 Docker 默认把容器 stdout 日志全部保留,跑几个月的 MySQL 日志文件能把磁盘塞满,严重时整个服务器都会卡死。max-size: 10m表示单日志文件最大 10MB,超出自动轮转,这是我在生产环境里踩过磁盘满的坑之后才加上的。
2.4 docker-compose 的统一编排
装完 Docker Engine 以后,我建议再装 docker-compose-plugin,这样就能用docker compose命令直接读 YAML 文件,把所有容器的启动参数、网络、数据卷集中到一个文件里管理。
为什么推荐 compose 而不是一个个docker run?因为容器一多,命令参数的记忆成本就上来了,而且容器之间的网络连接还得自己建 bridge 网络,非常麻烦。用 compose 以后,不同服务之间只要通过服务名就能互相访问,比如 PHP 容器里连 MySQL,主机名直接写服务名mysql,不用记 IP,非常方便。
一个最小的docker-compose.yml长这样:
version: "3.8" services: mysql: image: mysql:8.0 container_name: mysql8 restart: always ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: yourpassword volumes: - ./mysql-data:/var/lib/mysql networks: - app-net networks: app-net: driver: bridge后续加服务,比如加 Redis、加 Nginx,只需要在services:下面补一段配置即可,维护成本非常低。
3. 实操过程与核心环节实现
3.1 用容器跑 MySQL 8.0 并配置远程访问
MySQL 是几乎每个服务器都躲不开的组件。在 Docker 里跑 MySQL 8.0,有几个关键点必须处理好,否则你会在远程连接和数据持久化上反复踩坑。
先说我现在的标准配置(mysql-compose.yml):
services: mysql8: image: mysql:8.0 container_name: mysql8 restart: always ports: - "3306:3306" environment: TZ: Asia/Shanghai MYSQL_ROOT_PASSWORD: 你的强密码 command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --default-authentication-plugin=mysql_native_password volumes: - /data/mysql/conf:/etc/mysql/conf.d - /data/mysql/data:/var/lib/mysql networks: - app-net networks: app-net: driver: bridge这里有几个容易踩的坑,逐个说:
持久化数据目录必须挂载出来。如果容器是你docker run临时起的,里面产生的数据只存在容器的可写层。一旦容器被删除(比如升级镜像、误操作docker rm),数据也跟着没了。我吃过一次亏,某次docker-compose down -v把整个 MySQL 数据卷一起删掉,几十条业务数据直接蒸发。所以我建议,所有有状态服务,数据目录一定要挂载到宿主机。
远程连接的问题。很多人连不上 MySQL 第一反应是去调容器内部的授权表,其实不对。容器外面连不上,90% 是宿主机防火墙/安全组没放行 3306 端口,或者端口映射没写对。先在宿主机上ss -lntp | grep 3306确认端口在监听,再去云控制台检查安全组规则,最后才考虑 MySQL 内部的 root 用户是不是只允许 localhost 登录。
MySQL 8.0 的认证插件。如果你是拿 5.7 时期的客户端工具去连 8.0 的库,经常会报Authentication plugin 'caching_sha2_password' cannot be loaded。这个报错国外的 Stack Overflow 上一堆人问,一句话解释:MySQL 8.0 默认认证插件改成了 caching_sha2_password,老版的客户端不支持。所以我直接在启动命令里把默认插件强制指回mysql_native_password,这样 Navicat、老项目的 JDBC 驱动都能直接兼容。
3.2 Redis 主从复制:一行配置实现高可用
Redis 在 Docker 里跑起来比 MySQL 简单得多,但很多人的需求不只是“能跑”,而是“主从复制”,也就是一台 Redis 写入数据,另外一台自动同步备份。
我在服务器上实际搭的 Redis 主从结构是这样的:
services: redis-master: image: redis:7.0 container_name: redis-master restart: always ports: - "6379:6379" command: ["redis-server", "--requirepass", "主库密码", "--appendonly", "yes"] volumes: - /data/redis-master:/data networks: - app-net redis-slave: image: redis:7.0 container_name: redis-slave restart: always ports: - "6380:6379" command: > sh -c 'sleep 5 && redis-server --port 6379 --replicaof redis-master 6379 --masterauth 主库密码 --appendonly yes' depends_on: - redis-master volumes: - /data/redis-slave:/data networks: - app-net这里最核心的就是从库的--replicaof redis-master 6379和--masterauth 主库密码。前者是告诉从库“你去 6379 端口找主库”,后者是同步时需要的认证密码,不写的话第一次握手就直接失败。
为什么redis-slave启动命令里要加sleep 5?因为容器编排里depends_on只保证主库容器起来了,不保证主库内部已经能接受连接。不加延迟的话,从库可能一启动就发现主库连不上,然后不断重试。虽然 Redis 本身有重试机制,但加了延迟会让日志干净很多,也不容易误判为故障。
这套配置跑起来以后,我在主库写入数据,从库几毫秒内就能拉到,复制延迟用redis-cli info replication的master_repl_offset一对比就能看出来。实际体验非常稳。
3.3 青龙面板与依赖管理:小内存机器的利器
青龙面板的部署是我最近经常被问到的。很多做自动化任务的朋友,手头并没有独立服务器,就是一台便宜的小 VPS,2GB 内存已经算大方了。这种机器上你要再装一个 Python 环境再配一个 node 环境,内存分分钟爆表。而青龙面板已经把这些运行环境全都打包进镜像了,你只需要拉镜像跑起来即可。
我用的是官方镜像,启动方式很简单:
services: qinglong: image: whyour/qinglong:latest container_name: qinglong restart: always ports: - "5700:5700" volumes: - /data/qinglong/config:/ql/config - /data/qinglong/log:/ql/log - /data/qinglong/db:/ql/db - /data/qinglong/scripts:/ql/scripts environment: - TZ=Asia/Shanghai问题来了:很多用户跑完以后在面板里装依赖,比如你要跑 Python 脚本,得手动装requests、pandas,点半天按钮没反应,或者干脆报错。这个其实是历史老版本的通病。现在新版面板的依赖管理已经改成你直接选择“Python3 / Node.js / Linux”分类,然后在对应分类下面写包名,面板会在容器内部自动 pip/npm 安装。
如果依赖安装卡住,十有八九是网络问题。解决方法是给容器配置代理或调整镜像源,但更简单粗暴的是把 pip 源和 npm 源都改成你能正常访问的镜像站点,然后重启容器再试。
3.4 Nginx 反向代理:容器与宿主机端口规划
多服务跑起来以后,你就得面对另一问题:怎么让这些容器对外提供服务?直接暴露一堆乱七八糟的端口给用户,既不安全也不优雅。所以我最后都会加一个 Nginx 容器做反向代理,统一入口,按域名转发到不同容器。
比如我有三个服务:一个 Web 站点走 80 端口,一个青龙面板走 5700 端口,一个 API 服务走 8080 端口。在宿主机上我只把 80 和 443 端口暴露出去,其他服务全部关在 Docker 内网里,通过 Nginx 容器中转。
Nginx 容器部署起来也很简单:
services: nginx: image: nginx:1.27-alpine container_name: nginx restart: always ports: - "80:80" - "443:443" volumes: - /data/nginx/conf.d:/etc/nginx/conf.d - /data/nginx/html:/usr/share/nginx/html - /data/nginx/logs:/var/log/nginx networks: - app-net然后/data/nginx/conf.d目录下放一个站点配置文件,把请求按域名转发到对应的服务名加端口上:
server { listen 80; server_name ql.example.com; location / { proxy_pass http://qinglong:5700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }注意proxy_pass里用的是qinglong:5700而不是宿主机 IP 加端口。这是因为 Nginx 容器和青龙容器都在同一个 Docker 自定义网络app-net中,Docker 自带的 DNS 解析会把qinglong直接解析到对应的容器 IP,这个机制非常好用,不需要你去查容器 IP 再写死。
4. 常见问题与排查技巧实录
4.1 “Virtualization support wasn’t detected” 类报错的真相
这个报错通常出现在 Docker Desktop 的用户身上,典型的就是在 Windows 上安装 Docker Desktop 后,启动时弹窗提示 virtualization support 没有被检测到。这个报错跟我这篇文章主题不完全一样,但既然热搜里出现频率很高,还是值得说清楚底层的原理。
本质上,Docker Desktop 在 Windows 上并不是直接跑的 Linux 容器,而是要在 Hyper-V 或者 WSL2 里建一个轻量级 Linux 虚拟机,再在虚拟机里跑容器。如果你的 CPU 虚拟化功能没开,BIOS/UEFI 里 Intel VT-x 或 AMD-V 被禁用了,或者 Windows 的 Hyper-V 功能没启用,那 Docker Desktop 自然就起不来。
要排查的话,按这个顺序来:
- 重启进 BIOS,找到 CPU 虚拟化相关的设置,Intel 平台一般是
Intel Virtualization Technology,AMD 平台一般是SVM Mode,改成 Enabled; - Windows 里执行
systeminfo,看输出的最后一段有“Hyper-V 要求”字样,如果显示“检测到虚拟机监控程序。将不显示 Hyper-V 所需的功能。”说明虚拟化已经是开启状态; - 如果 BIOS 也开了,那可能是 Windows 的虚拟机平台功能没启用,打开“启用或关闭 Windows 功能”,把“虚拟机平台”和“适用于 Linux 的 Windows 子系统”两个选项勾上,重启系统。
如果你跟我一样是直接在 Linux 服务器上跑 Docker Engine,这个报错基本不会出现,因为你已经在真机内核上运行,不需要额外的虚拟化层。
4.2 容器端口拉通但外部访问不通
这个问题几乎每个新手都会遇到,具体表现是:容器起来了,日志正常,宿主机上curl内网地址也通,但公网就是访问不了。
排查步骤我建议按这个顺序:
- 确认端口映射:
docker ps看 PORTS 列,要显示0.0.0.0:3306->3306/tcp才说明宿主机端口确实转发到了容器; - 确认监听地址:
ss -lntp | grep 3306,看监听地址是不是 0.0.0.0,如果监听在 127.0.0.1,那只能在本地访问; - 放行云安全组:这是最容易被忽视的一步。很多云厂商默认安全组只放行 22、80、443,3306 之类的端口全被拦住了;
- 宿主防火墙:
iptables -L -n看一下 OUTPUT/INPUT 链规则,必要时临时放行测试。
另外还有一个我踩过多次的坑:排查防火墙时用systemctl stop firewalld停掉防火墙,但 iptables 规则是 Docker 自己生成的,容器网络本身依赖 iptables 做 DNAT,如果你过度清理 iptables,反而会让 Docker 网络直接瘫痪。正确做法是你只放行你需要的外部端口,而不是把所有 iptables 规则都清掉。
4.3 Docker 磁盘占满与日志膨胀处理
说了很多部署,最后聊一个运维上的持久战问题:磁盘。
Docker 容器跑的时间长了,镜像、日志、数据卷会不断膨胀。特别是那些不设日志轮转的容器,比如 Nginx 每天产生的访问日志,几个月不清理,轻松占满几十 GB 硬盘。我自己的服务器上出现过一次磁盘 100% 导致 MySQL 直接崩溃的经历,教训太深刻了。
日常建议做三件事:
- 在
daemon.json里配日志轮转,前面已经提到,这是最根本的办法; - 定期用
docker system df查看磁盘占用分布,看看是退出的容器太多,还是悬空镜像太多; - 定期清理
docker system prune -af,这条命令会删除所有停止的容器和悬空镜像,但注意别加-v,加了会把数据卷一起删掉,我前面已经吃过这个亏了。
4.4 常见问题速查
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 容器启动后立即退出 | 启动命令/环境变量有误 | docker logs 容器名查看日志,重点看最后几行报错 |
| 远程连不上 MySQL | 端口未放行 / 认证插件不兼容 | 按 4.2 排查四步走,必要时切换为 mysql_native_password |
| Redis 从库无法同步 | 主库有密码但从库没配 masterauth | 在从库启动命令中加--masterauth 主库密码 |
| 拉镜像超时失败 | 官方源访问不稳定 | 配置 registry-mirrors,改完务必systemctl restart docker |
| 容器时间比北京时间早/晚 8 小时 | 容器时区没设置 | 环境变量加TZ=Asia/Shanghai,或挂载/etc/localtime |
| 磁盘空间被日志占满 | Docker 容器日志无限增长 | daemon.json 里配置 max-size/max-file,然后清理旧日志 |
写在最后
折腾了这么久,我个人最大的体会就是:服务器的价值在于稳定运行,而不是让你天天折腾它的系统。用 Docker 把所有服务关进容器里,主机保持干净,出问题直接删容器重建,连系统都不用重装,这种安全感和自由度是裸机部署永远给不了的。
如果你一开始觉得 compose 文件麻烦,不妨就从最简单的 MySQL 容器开始上手,跑通了再逐步加服务。等你习惯了“改配置、重启容器、看日志”这套循环,大概就能理解为什么那些老运维都爱说一句话:能容器化的,别裸跑;能编排的,别手敲命令。这套玩法现在还远没到上限,K8s、Docker Swarm 以及各种云原生工具都在等你去尝试,但先把一台干净、稳定的 Docker 服务器跑起来,绝对是值得的第一步。