说真的,Docker 相关的教程我在网上看过无数篇,也照着部署过不少服务,踩过的坑基本能绕地球一圈。这套 “Docker + Docker Compose 部署” 的流程,是我在真实项目里反复使用、亲测有效的一套方案,不是单纯的文档搬运。
这篇内容适合这几类人看:刚接触 Docker 想搭一套自己的服务环境的同学、在 Windows 上被 Docker Desktop 各种报错折磨到头秃的朋友、以及想把 MySQL、Redis、GitLab 这类常用服务快速跑起来但不想被安装文档淹没的开发者。我会把所有前期准备、核心概念、实操步骤和排坑经验都放在一起,尽量让你照着做就能成功。
1. 环境准备:Docker 装不好,后面全是坑
1.1 Windows 用户:别跳过 WSL2 这一步
很多人装 Docker Desktop 失败,十有八九是卡在虚拟化或者 WSL2 上。Docker Desktop 在 Windows 上现在默认依赖 WSL2 后端,这是它运行容器的底层支撑。网上搜到的 “virtualization support not detected” 或者 “Docker Desktop failed to start because...” 这类报错,基本都是 WSL2 没启用,或者 BIOS 里的虚拟化开关没打开。
装之前先在 PowerShell(管理员)里跑两条命令:
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /restart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /restart跑完重启,再去 BIOS 里确认虚拟化(Intel VT-x / AMD-V)已经开启。然后下载 WSL2 内核更新包安装,最后执行:
wsl --set-default-version 2这时候再装 Docker Desktop,基本不会再报虚拟化相关的错误。顺手说过一句踩过的坑:Windows 上装完 Docker Desktop 后,第一次启动如果提示需要更新 WSL 内核,不要点“忽略”,老老实实去下载安装,不然之后拉镜像很容易莫名其妙失败。
1.2 Ubuntu / CentOS 服务器:官方脚本不是唯一选择
如果是 Linux 服务器,很多人喜欢直接执行 Docker 官方脚本一行安装:
curl -fsSL https://get.docker.com | bash这个脚本确实快,但有个问题:它会把 Docker 的 apt 源指向国外服务器,在国内网络环境下经常出现下载超时。我的习惯是先配置好国内的软件源,再装。
以 Ubuntu 22.04 为例,先装依赖:
sudo apt update sudo apt install -y ca-certificates curl gnupg lsb-release添加官方 GPG key 和仓库,然后把源地址替换成可用的镜像源,最后:
sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin注意这里直接安装了docker-compose-plugin,这是 Docker Compose 的官方插件版本,装完系统里就有docker compose命令(注意是docker compose,中间带空格,不是旧版的docker-compose)。CentOS 那边类似,用 yum 装docker-ce docker-ce-cli containerd.io,然后单独装 compose 插件包,步骤差不太多。
装完先验证一下:
sudo systemctl enable --now docker docker version docker compose version如果两条命令都有输出,环境基本就绪了。还有一步很重要:把当前用户加进 docker 组,免掉每次敲 sudo:
sudo usermod -aG docker $USER加完重新登录终端才生效。
1.3 镜像加速配不好,部署失败一大半
Docker 装好了,第一件事不是急着跑容器,而是配置镜像加速。纯新手最容易在这栽跟头:明明docker pull nginx:latest输进去了,结果卡在 “Get https://registry-1.docker.io/v2/” 或者连 pull 到一半就断了重试,心态直接崩掉。
Docker 的镜像仓库默认在国外,网络经常不稳定,所以需要给 Docker 配置镜像加速地址。做法是在/etc/docker/daemon.json(Windows 上是 Docker Desktop 的 Settings -> Docker Engine)里写:
{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://dockerproxy.com" ] }改完重启 Docker:
sudo systemctl restart docker配好之后docker pull的速度和成功率都会好很多。这个文件不止配镜像加速,后面很多全局参数都在这里设置,比如日志大小限制,后面会提到。注意,不同加速源的有效期和稳定性不一样,最好挑一个备选,一个不行就换另一个,有备无患。
2. Docker Compose 到底解决什么问题
2.1 为什么不用 docker run 硬扛
有人觉得:我直接用docker run不也能跑容器吗?为什么要学 Compose?
拿一个最简单的场景说:你要部署 MySQL,用一段 docker run 命令确实能跑起来:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -v mysql-data:/var/lib/mysql \ mysql:8.0一条命令可以接受,但如果你要部署 Redis、RabbitMQ、Nginx 三个服务呢?每个服务都得找对应的启动参数,端口、密码、数据卷、网络统统要写对,这些命令还特别长,一旦写错就得删掉重来。更糟糕的是,这些容器默认都在各自的网络里,想互相通信还得手动docker network create去对接。
Compose 做的事情,就是把这一堆手动操作写进一个compose.yaml文件里,用声明式的方式定义“我这个项目需要哪些容器、各自怎么配置、怎么互相通信”。之后只需要一个命令docker compose up -d,所有容器一次性启动,干净利落。
我遇到过一个自带十多个服务的开源项目,官方文档给了五页部署步骤,实际上就是一张 compose 文件的事。这也解释了为什么现在大量开源项目(Dify、GitLab、RabbitMQ 等)都会提供现成的 compose.yaml,拉下来直接就能跑。
2.2 compose.yaml 核心字段是哪些
Compose 文件的语法不复杂,但有些字段如果不理解,后面排错会很痛苦。
services 是顶层核心字段,下面每个子项代表一个容器服务。image 指定镜像,ports 做端口映射,environment 传环境变量,volumes 挂载数据卷,networks 指定网络。下面这个例子是单服务的极简 compose:
services: web: image: nginx:1.27-alpine container_name: my-web ports: - "8080:80" volumes: - ./html:/usr/share/nginx/html restart: unless-stopped几个值得深入理解的点:
ports的"8080:80"表示宿主机 8080 端口映射到容器内 80 端口。容器 80 端口一般不能被外部直接访问(容器网络隔离),所以必须映射。但注意映射也意味着宿主机端口被占用,同一个端口不能被两个容器同时映射。volumes是数据持久化的关键。容器是无状态的,只要容器被删,容器内部写的数据全会丢。把宿主机的目录或具名卷挂载到容器内目录,数据就能留宿主机上。restart: unless-stopped让容器在意外退出时自动重启,生产环境几乎是标配,不然服务器一重启服务就全部挂掉,很头大。
2.3 版本语义和兼容性
用 Compose 时要注意版本概念。Docker Compose 现在分两代:旧版使用 Python 写的docker-compose(带横杠),新版是 Docker 官方用 Go 重写的docker compose插件(带空格)。新版兼容旧的docker-compose.yml文件格式,但命令参数略有不同。
如果照着网上老教程用docker-compose up -d,而系统只装了新插件,会提示 “docker-compose: command not found”。现在最省事的方案:直接用docker compose,语法上区别不大,只是注意搜索引擎里的教程有“时代差异”,老教程的命令要手动转换一下。
version字段在新版里已经标记为废弃,compose.yaml 文件顶部不用写version: '3.8'这类内容,写了也不影响,但没必要。默认就是最新格式。
3. 三个高频场景的 Compose 实战
3.1 MySQL 8.0:持久化和时区一个都不能少
MySQL 是容器化部署时最容易踩坑的数据库之一。最经典的错误:容器跑了很久,数据都存进去了,突然容器被删或者重建,所有数据全没了。原因就是没用数据卷挂载。所以 MySQL 的 compose 里,volumes必须配置。
我常用的 MySQL 8.0 配置:
services: mysql8: image: mysql:8.0 container_name: mysql8 restart: unless-stopped environment: TZ: Asia/Shanghai MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: app_db MYSQL_USER: app_user MYSQL_PASSWORD: app_pass command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --default-authentication-plugin=mysql_native_password ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql - ./mysql/conf:/etc/mysql/conf.d - ./mysql/init:/docker-entrypoint-initdb.d healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot123456"] interval: 10s timeout: 5s retries: 5 volumes: mysql-data:细节拆解:
TZ: Asia/Shanghai解决容器内时间比宿主机慢 8 小时的问题。不用 TZ 环境变量的话,也可以挂载/etc/localtime:/etc/localtime:ro,两个方案选一个。建议优先用环境变量,兼容性更好。command里的参数是 MySQL 服务端的启动参数。utf8mb4字符集必须显式声明,否则默认的 latin1 会让你写进去的中文全部变成乱码。mysql_native_password是为了兼容老客户端,新版 MySQL 默认用 caching_sha2_password,个别老版本客户端连不上,这个参数可以按需添加。./mysql/init:/docker-entrypoint-initdb.d这个目录会自动执行里面的 SQL 脚本。比如把建库建表语句放进去,容器首次启动时自动完成初始化,非常方便。但注意只有首次启动且数据目录为空时会执行,数据卷已经有内容就不会再执行了。healthcheck是健康检查,配合后面的depends_on很有用,下面会详细说。
启动命令:
docker compose up -d docker compose ps看到 STATUS 显示 healthy,说明 MySQL 已经就绪,可以用客户端连接了。
3.2 Redis 日常和生产环境部署
Redis 在 Compose 里最常见的是单节点和主从两种玩法。先说日常开发用的单节点:
services: redis: image: redis:7-alpine container_name: redis-dev restart: unless-stopped ports: - "6379:6379" command: redis-server --requirepass redispass --appendonly yes volumes: - redis-data:/data volumes: redis-data:--appendonly yes开启 AOF 持久化,数据安全性更好。开发环境这样够了,但如果做生产级部署,就得考虑主从复制甚至哨兵。
主从结构我实际部署过一套,两个 Redis 实例,一个主一个从,配置思路如下:
services: redis-master: image: redis:7-alpine container_name: redis-master restart: unless-stopped ports: - "6379:6379" command: redis-server --requirepass masterpass --appendonly yes redis-slave: image: redis:7-alpine container_name: redis-slave restart: unless-stopped depends_on: - redis-master ports: - "6380:6379" command: redis-server --slaveof redis-master 6379 --masterauth masterpass --requirepass slavepass --appendonly yes volumes: - redis-slave-data:/data volumes: redis-slave-data:注意 slave 里使用了服务名redis-master作为主机地址,这是 Compose 默认网络内的 DNS 解析能力。容器之间直接通过服务名互相访问,不需要关心具体 IP 是多少,这也是 Compose 相比裸 docker run 的另一个便捷之处。
Redis 7 之后的趋势是使用 Redis Stack(带 JSON、Search 等模块)或者 valkey 之类的替代分支。不过本教程目标是用稳定方案,redis:7-alpine 镜像体积小、内存占用低,已经能满足绝大多数场景。
3.3 GitLab 和 RabbitMQ:重服务的资源规划
GitLab 是容器化部署里出了名的“吃内存大户”,默认配置下 8G 内存都可能不够用。但好消息是 Compose 里可以限制资源。
GitLab 的官方推荐是通过环境变量覆盖配置。我的实践配置如下:
services: gitlab: image: gitlab/gitlab-ce:latest container_name: gitlab restart: unless-stopped hostname: gitlab.example.com environment: GITLAB_OMNIBUS_CONFIG: | external_url 'http://gitlab.example.com' unicorn['worker_processes'] = 2 puma['worker_processes'] = 0 sidekiq['max_concurrency'] = 5 prometheus_monitoring['enable'] = false grafana['enable'] = false ports: - "80:80" - "443:443" - "2222:22" volumes: - gitlab-config:/etc/gitlab - gitlab-logs:/var/log/gitlab - gitlab-data:/var/opt/gitlab shm_size: '256m' volumes: gitlab-config: gitlab-logs: gitlab-data:这里有几个关键点:
GITLAB_OMNIBUS_CONFIG是 GitLab 容器用来生成配置的环境变量,不在这里写配置,后面每次修改都要进容器改/etc/gitlab/gitlab.rb再重启,麻烦。- 关闭了 Prometheus 和 Grafana,因为单机部署用不上,能省一大截内存。
2222:22映射 SSH 端口,外部连接用 2222,不然和宿主机 SSH 的 22 端口冲突。shm_size设大一点,GitLab 对 /dev/shm 要求很高,默认 64M 容易触发 “SIGBUS” 报错。
RabbitMQ 部署相对简单,主要是注意管理插件:
services: rabbitmq: image: rabbitmq:3.13-management container_name: rabbitmq restart: unless-stopped environment: RABBITMQ_DEFAULT_USER: admin RABBITMQ_DEFAULT_PASS: admin123 ports: - "5672:5672" - "15672:15672" volumes: - rabbitmq-data:/var/lib/rabbitmq volumes: rabbitmq-data:rabbitmq:management前缀的镜像自带 Web 管理界面,15672 就是管理后台的端口。5672 是 AMQP 协议端口,给客户端连接用。
4. 生产环境部署的进阶配置
4.1 网络规划:端口冲突与内网隔离
初学者最容易犯的错,就是把所有端口一律暴露到宿主机。比如部署一个由 Nginx、后端 API、MySQL 组成的应用,看到 MySQL 3306 就映射"3306:3306",看到 Redis 6379 就映射"6379:6379"。这样做的风险在于,数据库和缓存直接暴露在外网,等于把家门钥匙挂在门口。
生产环境的思路是分层:外部能访问的只有入口服务(比如 Nginx 的 80/443),MySQL、Redis 这些内部组件只在内网互相访问,不映射端口。Compose 文件天然支持这种网络隔离:
services: nginx: image: nginx:1.27-alpine ports: - "80:80" networks: - frontend - backend app: image: myapp:latest networks: - backend mysql: image: mysql:8.0 networks: - backend # 不写 ports,外部无法访问 networks: frontend: backend:这样app和mysql都在 backend 网络内,通过服务名互相访问;只有nginx同时连了 frontend 和 backend,它既能接收外部流量,又能通过http://app:8080这样的地址访问后端服务。而 MySQL 因为没写 ports,宿主机外网根本访问不到,大大降低了风险。
4.2 数据安全:备份、恢复与升级
容器化部署的一大优势是方便重建,但也有一个陷阱:数据全靠数据卷存着,一旦数据卷误删,神仙难救。所以备份必须纳入日常运维计划。
用 Docker 自带的命令备份具名卷,一条命令搞定:
docker run --rm -v mysql-data:/data -v $(pwd):/backup alpine tar czf /backup/mysql-data-$(date +%F).tar.gz -C /data .这个命令的意思是:临时用 alpine 镜像起一个容器,把mysql-data卷挂载到/data,把当前目录挂载到/backup,然后在容器里把/data打包成 tar.gz。--rm保证运行完自动删除临时容器。
恢复稍微复杂点,得先把新卷建出来再解压:
docker volume create mysql-data-restore docker run --rm -v mysql-data-restore:/data -v $(pwd):/backup alpine tar xzf /backup/mysql-data-2025-01-01.tar.gz -C /data然后在 compose 文件里把mysql-data-restore临时替换成实际卷名重启即可。生产环境强烈建议写个 cron 脚本定时执行备份任务,别等到数据没了再后悔。
升级容器镜像也需要一点技巧。不要直接在旧容器上改,而是:
docker compose pull docker compose up -dpull只会拉新镜像,不会动正在运行的容器;up -d时 Compose 发现镜像有更新,会先创建新容器再切换流量,旧容器自动停掉删除。不过数据库类服务升级前一定要先备份,MySQL 大版本升级尤其要谨慎,跨版本数据迁移不是简单换镜像就能搞定的。
4.3 日志与健康检查:别等崩溃才发现问题
容器内应用打印的日志如果不加限制,会无限膨胀,最后把磁盘塞满。Docker 默认的 json-file 日志驱动不做轮转,这是我踩过最痛的坑之一:某天发现磁盘 100%,排查半天才发现是 Nginx 容器积累了上百 GB 的访问日志。
全局限制日志大小,在/etc/docker/daemon.json里加:
{ "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "5" } }这样单个日志文件最大 50MB,最多保留 5 个文件,超过自动清理。改完重启 Docker 后新建的容器才生效,已经运行的容器需要重建。
健康检查也是生产环境的重要一环。前面 MySQL 配置里写过了healthcheck,这里说说它的用法:Compose 里可以用depends_on控制容器启动顺序,但它只管“先启动”,不管“可用不可用”。比如应用依赖数据库,如果数据库还没初始化完就被应用连,应用就会报错崩溃。
正确姿势是配合condition: service_healthy:
services: app: image: myapp:latest depends_on: mysql: condition: service_healthy这样 Compose 会等 MySQL 通过健康检查后才启动 app。这个功能在高版本 Compose 里是开箱即用的,别再用sleep 10这种笨办法了。
5. 常见问题排查与实践感悟
5.1 高频报错速查表
| 报错信息 | 可能原因 | 解决方案 |
|---|---|---|
Cannot connect to the Docker daemon at unix:///var/run/docker.sock | 当前用户不在 docker 组,或 Docker 服务没启动 | sudo usermod -aG docker $USER后重新登录;sudo systemctl start docker |
port is already allocated | 宿主机端口被占用 | sudo lsof -i:3306查看占用进程,换端口或停掉冲突进程 |
Get https://registry-1.docker.io/v2/: net/http: request canceled | 镜像拉取被网络阻断 | 配置镜像加速,重试docker pull |
no matching manifest for linux/arm64 in the manifest list entries | 镜像不支持当前 CPU 架构 | 换 multi-arch 镜像,或改用 Raspberry Pi 专用镜像 |
Virtualization support not detected | BIOS 未开启虚拟化或 WSL2 未启用 | 进入 BIOS 开启 VT-x/AMD-V,启用 Windows 功能,安装 WSL2 内核 |
OCI runtime exec failed: exec failed: unable to start container process | 容器内命令路径不对 | 检查 healthcheck 或 exec 里的命令是否为容器内存在的绝对路径 |
docker compose命令找不到 | 没有安装 compose 插件 | Ubuntu 安装docker-compose-plugin,或用官方二进制安装 |
有一类比较隐蔽的问题:compose.yaml 语法没问题,但环境变量没传进去。我遇到过 MySQL 密码一直是默认值的问题,排查半天发现是.env文件里的变量名写错,Compose 并不会提示这种错误,只会默默用空值。建议启动后先进容器验证:
docker compose exec mysql env docker compose exec redis redis-cli ping容器内环境变量对不对,跑一下就知道了。
5.2 一些值得记住的小技巧
最后分享几个实际项目里攒下来的小技巧。
第一,.dockerignore文件一定要写。构建 Docker 镜像时,Docker 会把构建目录里的所有文件发给守护进程。如果不排除 node_modules、.git、dist 这类大目录,构建会慢得想砸电脑,更严重的是可能把敏感信息也打进去。在项目根目录建一个:
node_modules .git .gitignore *.log .env dist第二,.env文件管理敏感信息。compose.yaml 里可以直接写MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD},然后在同一目录的.env文件里写具体的值。这样 compose.yaml 可以进代码库,.env塞进.gitignore,密码就不会泄露。
第三,先用docker compose config做静态校验。这个命令会渲染出最终生效的完整配置,任何语法错误、变量缺失都会在这里暴露出来。我习惯在up之前必跑一下这个命令,能省掉一半的启动失败。
第四,清理环境时别直接docker volume prune -f。这个命令会删除所有未被使用的数据卷,如果你有不再运行的容器但数据卷里还存着重要数据,一起删掉就追悔莫及了。稳妥做法是手动确认再删,或者先列出卷列表看清楚再处理。
5.3 个人实操中的几个体会
这套 Docker + Compose 方案我前后用了两三年,从最早单个服务手敲 docker run,到后来一个中等规模项目用 Compose 管理十几个服务,整体的感受是学习曲线其实比想象中平缓,真正的门槛不在命令,而在理解容器和宿主机之间的边界在哪里。
边界理解到位了,很多问题自己就能推出来。比如容器为什么重启后数据没了?因为数据写在容器可写层里,而可写层跟着容器生命周期走。容器为什么时不时连不上数据库?因为数据库容器的启动完成时间和外部认为的“容器已启动”不是一回事。这些逻辑想通之后,Compose 里的每个字段为什么这么写,就有了答案。
如果你现在正准备把一个新服务容器化,我的建议是别一上来就追求高可用、负载均衡、Kubernetes 那套。先用一个服务、一个 compose 文件把基础链路跑通,数据卷、健康检查、日志轮转这些基本功练扎实,再逐步往上加东西。这套基础的部署方案已经能覆盖绝大多数中小项目的需求,而且后期迁移到 Docker Swarm 或 Kubernetes 时,之前写的 compose 配置也能平滑过渡,不会白费功夫。
最后再分享一个小技巧:每套部署写成独立目录,里面放 compose.yaml、.env、README.md 和必要的配置目录。这样即使三个月后回来看,也能快速回忆起来当时怎么部署的,新机器上一拉一跑就能重建整个环境。这个习惯救过我不少次,也推荐你试试。