Docker与Compose生产级部署实战:从安装、编排到踩坑全指南
都是在生产环境里跑过、被坑过以后反复验证过的结论。1. 为什么Docker和Compose几乎总是成对出现先把这个基础问题说透。很多人第一次接触Docker的时候容易把Docker和Docker Compose搞混其实它们解决的是两个不同层面的问题但配合起来才是一个完整的工作流。Docker本身是容器运行时负责拉取镜像、启停容器、管理网络和数据卷。你可以在命令行用docker run启动一个容器比如跑一个MySQLdocker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDyourpassword \ -v mysql_data:/var/lib/mysql \ mysql:8.0那如果我们的项目需要同时跑MySQL、Redis、后端应用、前端Nginx呢四个容器每个都要写一大串参数每次都要一个个启动、一个个停止容器之间还要互相通信手动管理多容器会出现几个很现实的问题启动顺序靠记某个服务起晚了就连不上数据库端口冲突排查起来很麻烦容器之间的网络配置要么用默认bridge手动互联要么用--link这种已经被官方标记为过时的功能换个环境部署同样的命令要重新敲一遍环境差异导致各种莫名奇妙的问题Docker Compose就是来解决这个问题的。它用一个docker-compose.yml文件把这些容器的配置、依赖关系、网络、数据卷都声明出来然后用一条docker compose up -d把整个服务栈拉起来。做个简单的类比Docker是一间独立的集装箱房间Docker Compose是一整栋楼的设计图加管理员。设计图规定了每个房间的位置、装修标准、水电怎么接管理员负责按图纸施工和后续维护。没有图纸也能造房子但一旦房间多了图纸和管理员是刚需。这也是为什么完整的Docker部署教程几乎必然要同时讲Docker和Compose。只装Docker不装Compose遇到稍微复杂一点的场景就得靠脚本硬扛只学Compose不懂Docker的基础命令和原理出了问题完全不知道怎么排查。两个一起用才能覆盖从单机单容器到多服务编排的完整链路。2. Linux服务器上安装Docker引擎核心步骤与镜像加速安装Docker之前先想清楚一个问题你的部署目地是生产环境还是个人测试这个问题直接决定了你要不要加很多额外的配置。对于生产环境我通常建议在干净的Linux服务器上安装Docker Engine而不是Docker Desktop因为桌面版额外的虚拟机层在服务器上没有任何意义反而增加资源消耗和故障点。以下以Ubuntu 22.04 LTS为例这也是目前云服务器最常用的版本之一。先更新系统并安装依赖sudo apt update sudo apt install ca-certificates curl gnupg lsb-release然后添加Docker官方的GPG key和软件源。这一步很多教程简化掉直接跳过但我建议老老实实加上因为用官方源装出来的版本和apt自带的老旧版本差别很大后者经常不带Compose插件也不满足新特性的要求。sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod ar /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引擎和Compose插件。这里记住一个重点新版Docker Engine安装包会同时提供docker-compose-plugin装完以后系统里同时有docker和docker compose命令这里的docker compose是插件形式而不是老的独立二进制docker-compose两者有区别后面会专门展开讲。sudo apt update sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin安装完成后启动服务并设置开机自启sudo systemctl enable docker --now sudo systemctl status docker如果每一步都正常最后用docker version验证一下。这个命令会同时显示Client和Server版本信息如果只看到Client没有Server说明Docker daemon没起来这时候先看sudo systemctl status docker的日志再排查。2.1 非root用户执行docker命令的正确姿势这是几乎所有新手都会遇到的一个门槛装完Docker以后普通用户执行docker ps会报permission denied while trying to connect to the Docker daemon socket。原因是Docker的socket文件默认属于root用户和docker组普通用户没有权限访问。解决办法是把当前用户加入docker组然后重新登录或者重启会话sudo usermod -aG docker $USER newgrp docker注意一点加入docker组相当于把root权限间接交给了这个用户因为docker组内的用户可以随意操作宿主机文件系统。所以不要随便把业务账号加进docker组生产环境建议用专门的部署账号或者坚持用带sudo的方式执行。2.2 配置国内可用的镜像加速器这是亲测有效的关键前置条件。默认情况下Docker从Docker Hub拉镜像网络状况不稳定的时候一个几百MB的镜像能拉到怀疑人生甚至直接超时。解决办法是配置registry mirror让Docker从更快的镜像源拉取。编辑/etc/docker/daemon.json没有就新建{ registry-mirrors: [ https://docker.m.daocloud.io, https://dockerproxy.com, https://docker.nju.edu.cn ] }然后重启Dockersudo systemctl daemon-reload sudo systemctl restart docker配置好以后用docker info查看Registry Mirrors那一段是否生效。如果拉取还慢可以试试其他镜像站但我不建议把太多镜像源一股脑都填进去两三个够用的就行。填多了反而可能出现源切换的额外耗时。另外注意镜像加速器只影响从Docker Hub拉取的镜像从其他私有仓库或者第三方平台拉镜像不走这个配置。CentOS、Debian等其他发行版的安装步骤基本类似套路就是装依赖、加源、装docker-ce、启动服务。区别主要在包管理器命令和软件源路径核心配置daemon.json和用户组设置是通用的。3. Windows和macOS的场景Docker Desktop还是纯命令行本地开发和测试环境用Windows或者macOS跟服务器上的安装方式则完全不同。很多人下载Docker Desktop以后被各种报错劝退其实不是软件不行而是没搞清楚它的运行机制。Docker Desktop的底层是自己管理的一个轻量级虚拟机在Windows上是基于WSL 2或者Hyper-V在macOS上是基于Apple Virtualization框架或者老一点的HyperKit。所有容器实际都跑在这个Linux虚拟机里Docker Desktop只是它的图形化管理壳。明白了这个基本原理很多问题就有了排查方向。3.1 安装Docker Desktop之前先检查虚拟化最常见的问题之一就是启动时报错Virtualization support not detected。原因很直接BIOS里的虚拟化功能没开启。进BIOS查一下CPU虚拟化设置Intel平台叫VT-x或者Intel Virtualization TechnologyAMD平台叫SVM Mode把它改成Enabled保存重启大部分问题就解决了。Windows上还有第二道开关控制面板—程序—启用或关闭Windows功能确认以下两项至少有一项是开的Hyper-V适用于Linux的Windows子系统WSL虚拟机平台推荐最新的Windows 11直接开WSL 2然后安装Docker Desktop时选择使用WSL 2 backend。这样比Hyper-V更轻量集成的Linux内核由wsl命令统一管理日常使用也更顺滑。装完Docker Desktop以后它在系统托盘常驻所有Docker相关命令在PowerShell、CMD或者Windows Terminal里都能直接用。Docker Desktop内置了docker compose插件所以compose相关的命令在本地一句额外的东西都不用装。3.2 Docker Desktop的镜像源配置本地开发同样要配置镜像加速不然一样会被拉镜像卡住。打开Docker Desktop的Settings切到Docker Engine这个Tab能看到一段JSON配置在registry-mirrors字段里填入跟服务器版一样的镜像源即可{ registry-mirrors: [ https://docker.m.daocloud.io, https://docker.nju.edu.cn ] }改完点Apply RestartDocker Desktop会自动重启并生效。3.3 挂载本地文件目录做数据卷时踩的坑在Windows上用Docker Desktop挂载本地目录做数据卷比如把MySQL的数据文件挂载到D:\mysql-data看起来配置没问题但容器跑起来以后写文件失败日志报Permission denied或者Cant create/write to file。根因在于Windows文件系统映射到Linux虚拟机里的权限模型不一致Docker Desktop会弹窗让你授权共享驱动器。在Settings - Resources - File Sharing里把D盘加进去或者直接把所有盘符授权这问题就没了。另外数据卷目录尽量不用中文路径和带空格路径Windows路径的兼容性本来就敏感别给自己找额外的麻烦。4. Compose的安装方式与版本差异如果用的是Docker Desktop或者新版Docker Engine加docker-compose-plugindocker compose子命令直接可用。但如果是历史遗留环境、离线安装、或者某台服务器上的Docker版本比较老就得单独处理Compose。先说清楚两个容易混淆的东西docker-compose独立的Python二进制文件老式写法通常安装到/usr/local/bin/docker-composedocker composeDocker官方插件集成在CLI插件目录里新式写法功能上两者差别不大但新项目我强烈建议用后者因为它是官方持续维护的也更符合Docker命令行生态的演进方向。Compose文件格式现在已经演进到v2/v3新版docker compose对现代语法的支持也更好。4.1 独立二进制安装方式确实需要单独安装docker-compose二进制时官方推荐的方式是去GitHub Releases页面下载对应的二进制文件sudo curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose sudo chmod x /usr/local/bin/docker-compose验证一下版本docker-compose version如果网络环境访问不了GitHub可以从镜像站下载或者找一台已经装好Compose的机器把二进制拷贝过去。独立二进制安装完成以后要确认/usr/local/bin在PATH里不然执行命令会提示找不到。4.2 Compose文件格式的版本匹配问题Compose文件开头的version字段例如version: 3.8在旧版必须写新版官方已经不推荐写了。我实测下来这个字段不写也不影响运行新版Compose会直接按最新格式解析。但有一个场景要注意假设你的生产服务器上Docker Compose还是老版本比如1.25那种而你的docker-compose.yml里用了depends_on的condition新语法、healthcheck、init这些新特性老版本根本解析不了直接报错。所以拿到别人的Compose文件先看本地的Compose版本再决定要不要调整语法。这里给一个参考Compose v1.27以上才支持完整的depends_on.conditionv2.0以上才比较完整地支持healthcheck链式依赖。5. 第一个生产级编排实践MySQL 8.0 Redis主从纸上谈兵说多了没用下面用一个比较典型的场景把整个Compose编排流程串起来部署一套MySQL 8.0带数据持久化加一个Redis主从架构主库写、从库读支持故障手动切换的那种基础形态最后再挂一个最简单的应用容器来验证服务间网络互通。这个组合基本覆盖了日常项目90%的编排需求。先看目录结构deploy/ ├── docker-compose.yml ├── .env └── mysql/ └── conf/ └── my.cnf.env文件存放环境变量目的是把密码这类敏感信息跟Compose文件分离也方便不同环境用不同配置MYSQL_ROOT_PASSWORDstrong_root_password MYSQL_DATABASEapp_db MYSQL_USERapp_user MYSQL_PASSWORDapp_password REDIS_REQUIREPASSredis_pass_123然后是docker-compose.ymlservices: mysql: image: mysql:8.0 container_name: mysql8 restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: ${MYSQL_DATABASE} MYSQL_USER: ${MYSQL_USER} MYSQL_PASSWORD: ${MYSQL_PASSWORD} TZ: Asia/Shanghai command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci - --default-time-zone8:00 ports: - 3306:3306 volumes: - mysql_data:/var/lib/mysql - ./mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -uroot, -p${MYSQL_ROOT_PASSWORD}] interval: 10s timeout: 5s retries: 5 start_period: 30s redis-master: image: redis:7.2 container_name: redis-master restart: unless-stopped command: [redis-server, --appendonly, yes, --requirepass, ${REDIS_REQUIREPASS}] ports: - 6379:6379 volumes: - redis_master_data:/data redis-replica: image: redis:7.2 container_name: redis-replica restart: unless-stopped depends_on: - redis-master command: [redis-server, --appendonly, yes, --replicaof, redis-master, 6379, --masterauth, ${REDIS_REQUIREPASS}, --requirepass, ${REDIS_REQUIREPASS}] ports: - 6380:6379 volumes: - redis_replica_data:/data app: image: nginx:alpine container_name: demo-app restart: unless-stopped depends_on: mysql: condition: service_healthy redis-master: condition: service_started ports: - 8080:80 volumes: mysql_data: redis_master_data: redis_replica_data:运行cd deploy docker compose up -d查看运行状态docker compose ps看到healthy状态以后进数据库验证一下docker exec -it mysql8 mysql -uapp_user -papp_password app_db进MySQL以后执行SELECT 1;能正常返回就说明数据库通了。5.1 为什么每个关键配置都要这样写数据卷挂载是唯一能保证容器重建后数据还在的方式。很多人图省事直接把数据库目录挂到宿主机的普通文件夹但那样权限问题、IO性能问题接踵而来。用命名卷named volume让Docker自己管理存储位置既能保持数据持久化又规避了跨平台权限问题我强烈建议数据库这类有持久化需求的服务都用命名卷。restart: unless-stopped是企业部署的默认选择。它跟always的区别在于当容器被用户手动docker compose stop停掉以后unless-stopped不会在Docker重启时把它拉起来适合明确要停的服务而always会在Docker daemon启动时不管之前是什么状态都拉起。生产环境我用unless-stopped多一点因为运维有手工维护的余地。depends_on配合healthcheck才能实现真正的按序启动。早期的Compose只保证服务启动顺序但不保证服务内部真正就绪。MySQL可能容器起来了但还在初始化目录这时候应用去连接它必然失败。加了healthcheck以后depends_on的condition: service_healthy会让Compose一直等MySQL通过健康检查才启动下一个服务。这是生产级编排和玩具级编排的分水岭。Redis主从的replicaof参数里主机名直接写Compose服务名redis-master。这是Compose内部网络DNS带来的便利服务之间不用记IP用服务名就能互相解析。同样的道理应用容器里连MySQL就直接用mysql:3306连Redis从库就用redis-replica:6379。关于账号密码Compose会自动读取同目录的.env文件把变量插进docker-compose.yml。这样docker-compose.yml本身可以提交到Git仓库而.env留在服务器本地或者用密钥管理工具单独分发避免了密码被提交进仓库的风险。5.2 生产环境还可以补几刀上面这个配置跑起来已经能应付大多数演示环境了但要上生产我一般还会加这么几件事在MySQL的my.cnf里显式限定bind-address 0.0.0.0否则容器内MySQL默认只监听localhost外部工具连不上Redis如果只需要内网访问从库端口可以不映射到宿主机让它在Compose网络内部通信就好给Compose项目设置独立的网络别名避免多个项目之间的容器因为服务名冲突而串网加一层oom_score_adj或者deploy.resources.limits限制资源使用防止某个容器异常吃满内存拖垮宿主机比如docker compose.yml里可以加资源限制deploy: resources: limits: cpus: 0.50 memory: 512M注意deploy.resources在非Swarm模式下新版本Compose直接支持但老版本可能提示忽略这个语法要跟Compose版本匹配别在旧版本上白费力气。6. 日常运维中真正用得上的命令清单装好、部署好只是开始日常维护才是大头。下面这份清单是我在各种项目里反复用到的命令每条都标了使用场景和注意事项。命令使用场景注意事项和坑docker compose ps查看服务栈内所有容器状态加-a可以看到已停止的容器docker compose logs -f service跟踪某个服务的实时日志不指定服务名会输出所有容器日志噪音大docker compose exec service cmd进入正在运行的容器执行命令加-u root可切换用户不要用docker exec替代前者会更准确定位服务docker compose config校验Compose文件并展开变量改动.yml以后先跑一遍能发现大部分语法错误docker compose down停止服务并移除网络加-v时连数据卷一起删不可恢复执行前想清楚docker compose restart service单独重启某个服务不改代码不重建适合配置了环境变量的服务docker compose build --no-cache重新构建镜像不用缓存层构建Dockerfile改过基础依赖时用docker system df查看镜像、容器、数据卷占用空间定期看一下避免磁盘被撑爆docker system prune -af清理所有停止容器和无用镜像高危操作生产环境先确认没有需要保留的离线镜像docker stats实时查看所有容器CPU/内存占用排查性能问题或者内存泄漏特别好用其中docker compose config被很多人忽略它其实是最早能暴露问题的一步。写错一个缩进、变量没定义直接运行up会浪费时间先用config展开检查最划算。还要提一个习惯不要在生产环境用latest标签锁定镜像的具体版本号或者用commit SHA。latest标签随上游更新而漂移哪天你执行了docker compose pull容器悄悄跑上新版本行为变化可能毫无预兆。锁版本配合docker compose的声明式配置才能做到这个部署是可复现的。7. 亲测踩过的坑和对应解法这部分才是标题里亲测有效四个字的分量所在。下面这些坑我每一个都在真实环境里遇到过先说现象和排查链路再给最终解法你可以按同样的思路去排查自己的问题。7.1 Permission denied while trying to connect to the Docker daemon socket现象普通用户执行docker ps报错sudo docker ps正常。排查链路第一反应看docker version是否正常确认Client和Server都在接着看/var/run/docker.sock的文件权限然后看当前用户是否属于docker组。解法sudo usermod -aG docker $USER重新登录。忘了重新登录是这个坑的另一个常见原因newgrp docker可以临时刷新当前会话而不需要注销。7.2 Windows上Docker Desktop持续报Virtualization support not detected现象Docker Desktop启动几秒后报错日志显示虚拟化检测失败。排查链路先任务管理器确认虚拟化是否已启用若显示已启用去控制面板检查Hyper-V和虚拟机平台是否打开如果这两项都正常问题出在BIOS里的VT-x被关闭了。解法重启机器进BIOS把Virtualization Technology改为Enabled开机再验证。笔记本用户特别注意有些电脑在电池模式下默认关闭虚拟机开关插上电源重启一次可能就好了这个情况比较隐蔽遇到就多试一次。7.3 拉取镜像超时或者速度极慢现象docker pull跑很久最后提示net/http: request canceled。排查链路先确认是不是首次拉取大镜像比如MySQL这种体积的网络慢会导致一个几百MB的镜像拉十几分钟然后检查daemon.json里registry-mirrors是否生效如果配置了多个镜像源逐一套出来测试哪个真正可达。解法配置镜像加速器见2.2节或者临时从国内云厂商的镜像仓库拉取改tag以后再用。另外拉镜像的时候不要开太多并行任务带宽有限的情况下并行拉取反而会更慢。7.4 容器起来了但应用连不上MySQL现象docker compose up -d全部显示Up应用日志报Access denied for user或者Cant connect to MySQL server on mysql。排查链路先在宿主机上验证MySQL端口通不通telnet 127.0.0.1 3306注意Windows下要先开telnet服务再进MySQL容器看进程状态docker exec -it mysql8 mysql -uroot -p如果是连接被拒重点看MySQL的bind配置和用户授权如果是密码错误确认一下.env里的变量是否被正确读取docker compose config看展开后的实际值。解法MySQL容器里的授权规则要照顾到连接来源Compose网络里连接MySQL的主机名是服务名要给对应的用户授权mysql -h localhost和mysql -h mysql都能通过。配置示例CREATE USER app_user% IDENTIFIED BY app_password; GRANT ALL PRIVILEGES ON app_db.* TO app_user%; FLUSH PRIVILEGES;7.5 容器启动后时区不对日志时间差8小时现象容器日志时间跟北京时间差8小时MySQL里存储的时间字段也是错的。排查链路容器默认使用UTC时区宿主机和容器各走各的时钟。看docker exec container date的输出即可验证。解法Compose配置里给每个服务加TZ: Asia/Shanghai环境变量MySQL这类应用还需要在command或者配置文件里设置--default-time-zone8:00。如果是Debian系的基础镜像有些还要装tzdata包否则TZ环境变量不生效。Java应用的话除了容器时区JVM默认时区也要一并处理。7.6 Redis主从配置好了但从库连不上主库现象redis-replica日志一直报MASTER - REPLICA sync started但是没有MASTER - REPLICA sync finished或者直接报NOAUTH Authentication required。排查链路先确认主库是否设置了密码再看从库的--masterauth有没有配上如果主库配置了bind 127.0.0.1从库在别的容器里根本连不上主库的地址最后看Compose网络里能否用服务名解析到主库。解法主库的redis-server命令不要加--bind 127.0.0.1如果担心安全用Compose的expose而不是ports对外只暴露给同网络内的服务从库的--masterauth必须和主库的--requirepass一致。日志里看到Full resync就说明从库已经开始正常同步了。8. 一套部署配置拷到另一台机器就能跑才叫真有效写到这我停一下想聊点个人体会。标题里亲测有效四个字看上去是我这套配置跑通了的意思但我的理解更深一层有效的标准是任何一个人拿到你的Compose文件在一台干净的机器上能复现出同样的结果。我最开始接触Docker的时候也是照着网上的教程一步步装装完发现各种细节对不上比如系统版本不同、镜像源失效、新旧工具混用。后来整理出自己的部署模板固定流程装引擎、配置加速器、套Compose模板、验证健康检查、跑日志、锁版本号、记录变更。这套流程完整走一遍以后部署的时间成本直线下降。日常工作中我还在用这么一个小技巧把一套完整的生产Compose项目docker-compose.yml加.env.example加README放进Git仓库管理。.env这类含敏感信息的文件用.gitignore排除掉只提交.env.example作为模板。换服务器的时候git clone下来把.env.example复制一份为.env填好密码docker compose up -d整个服务栈就起来了整个迁移过程半小时内绝对能搞定。我在实际使用中发现这套从零搭建到一键复现的工作流比记住任何一条具体命令都更有价值也是我坚持推荐Compose的根本原因。