从零掌握Docker与Compose:安装配置到实战部署全攻略 做后端这几年Docker 绝对是我用得最值的一个工具。不管你是要部署个人博客、搭建开发环境、跑数据库中间件还是给团队搞一套统一的交付流程Docker 加 Docker Compose 这套组合基本都能覆盖。今天这篇东西我不打算讲那些花里胡哨的概念就老老实实把从零开始安装 Docker、配置 Compose、写出第一个 Dockerfile 和 docker-compose.yml、最后把一个完整项目跑起来的全过程按我亲测过的顺序给你捋一遍。文章里所有命令、配置文件、踩坑点都是我在真实环境里跑过验证过的不是照抄官方文档的那种空话。不管是刚接触容器的小白还是已经用过一段时间但想系统补一补细节的开发者这篇都值得你花二十分钟读一遍。1. 为什么 Docker Compose 能成为部署标配1.1 一次构建到处运行的底层逻辑先说清楚 Docker 到底解决了什么问题。以前部署一个应用最头疼的就是环境一致性本地跑得好好的上了服务器就崩JDK 版本不对、依赖缺了、系统库不兼容各种玄学问题。Docker 的思路很简单——把应用本身和它运行所需要的完整环境一起打包成镜像然后用这个镜像去创建容器运行。镜像这东西你可以理解成一个模具容器就是模具每一次倒出来的成品。同一个镜像可以倒出无数个一模一样的容器不管底层是哪台机器、什么操作系统只要安装了 Docker跑出来的环境就完全一致。这就是所谓的一次构建到处运行。这个特性带来的直接好处是部署变成了一件极度无聊的事情。你不需要在目标机器上装数据库、配环境变量、折腾依赖只需要拉取镜像、创建容器完事。以前部署一个项目可能要写几页纸的部署文档现在一行docker run就搞定了。1.2 Compose 解决了什么痛点单机单容器用docker run没问题但实际项目很少只有一个组件。典型场景一个 Web 应用前面有 Nginx 做反向代理后端连着 MySQL 和 Redis可能还有消息队列。这种情况下你需要管理好几个容器要处理它们之间的网络通信、启动顺序、数据持久化、环境变量配置……Docker Compose 就是干这个的。它让你用一个 YAML 文件描述整个应用栈的组成包括每个服务用哪个镜像、映射哪些端口、挂载哪些数据卷、依赖哪个服务然后一条docker compose up -d命令把整栈拉起来。我举个生活化的例子docker run就像你分别去买各种家电买回来后还得自己接线、布管、调试而 Compose 就是给你一张总装图照着图把所有东西装好通电就能用。它不是一项全新的技术而是在 Docker 之上的一个编排工具让你对多容器的管理从面向命令变成面向声明。1.3 这套方案适合谁、能解决什么问题按我的经验下面几类人特别适合用这套东西独立开发者 / 站长用 Compose 把博客、数据库、反向代理一次性编排好换服务器时只需复制 compose 文件和挂载目录几分钟恢复全部服务。后端研发人员本地开发环境统一用 Docker 跑中间件项目代码放在宿主机容器只提供运行时既隔离又高效。运维 / SRE用 Compose 做应用生命周期管理配合 CI/CD 流水线极大降低发布成本和环境配置错误率。想在本地跑最新服务的人现在很多开源项目比如各类 AI 模型、监控系统、代码托管平台都提供了 Docker 部署方式拉个镜像就能跑省去大量编译安装的精力。核心价值总结下来就一个词——确定性。环境确定、步骤确定、行为确定这意味着你可以把部署这件事完全交给脚本和配置人只负责写配置文件就行。2. 环境准备三大平台安装与配置2.1 Linux 安装 Docker 和 ComposeLinux 是我最常用的部署环境以 Ubuntu 20.04 / 22.04 为例整个安装流程亲测稳定。第一步更新软件源并安装依赖sudo apt update sudo apt install -y ca-certificates curl gnupg lsb-release第二步添加 Docker 官方 GPG 密钥和软件源。这里注意国内服务器如果访问 Docker 官方源慢可以替换成清华或阿里云的镜像。# 添加 GPG 密钥 sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg # 添加软件源 echo \ deb [arch$(dpkg --print-architecture) signed-by/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null第三步安装 Docker Enginesudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin注意这里我直接装了docker-compose-plugin这个包装好后自带的命令是docker compose中间有个空格这是目前官方推荐的用法。老版本的docker-compose是一个 Python 编写的独立二进制虽然还有不少人在用但新项目建议直接上官方插件版本。装完验证一下sudo docker --version sudo docker compose version第四步把当前用户加入 docker 组省去每次敲 sudo 的麻烦sudo usermod -aG docker $USER newgrp docker这一步非常关键。我见过很多人装完 Docker 后每次执行命令都要加 sudo极其影响使用体验。加入 docker 组后重新登录终端直接docker ps就能用了。注意加入 docker 组的用户拥有等同于 root 的 Docker 权限生产环境要谨慎管理组内成员。2.2 Windows 和 macOS 安装 Docker DesktopWindows 和 macOS 上的方案比较统一就是安装 Docker Desktop。这个图形化工具集成了 Docker Engine、Compose、Kubernetes 等组件装上基本全齐了。Windows 上要注意一个前置条件必须开启系统的虚拟化功能。现在的电脑基本都是 Win10/11在 BIOS 里确认 Virtualization Technology (VT-x/AMD-V) 已开启然后控制面板里启用Windows 虚拟机监控程序平台和适用于 Linux 的 Windows 子系统功能。Docker Desktop 在 Windows 上默认基于 WSL2 后端运行相比老的 Hyper-V 方案启动速度快、内存占用低容器内网络和文件 IO 性能都好很多。安装完成后设置里可以选Use the WSL 2 based engine。macOS 分 Intel 芯片和 Apple Silicon 芯片去官网下载对应的安装包就行。Apple Silicon 上建议优先拉取linux/arm64架构的镜像能发挥原生性能有些老镜像只有 amd64 版本Docker Desktop 会自动走仿真性能会有一定折损。安装完成后Mac 上 Docker Desktop 会默认开机自启并占用一部分内存如果你本地开发环境吃紧可以在 Preference 里调低内存配额。2.3 镜像加速配置亲测有效的国内方案国内拉取 Docker Hub 官方镜像经常遇到超时、速度极慢的情况。最直接的解决方案是配置镜像加速器。这里说一个我实测有效的方式编辑/etc/docker/daemon.json{ registry-mirrors: [ https://hub-mirror.c.163.com, https://docker.mirrors.ustc.edu.cn ] }不同镜像源稳定性会有波动如果拉取慢就多备几个地址。配置完重启 Dockersudo systemctl daemon-reload sudo systemctl restart docker验证是否生效docker info | grep -A 2 Registry Mirrors我自己实际用下来加了镜像加速后常见的nginx、mysql、redis这些热门镜像基本都能达到几 MB/s 的下载速度跟裸连官方源比是天壤之别。注意如果你用的 Docker Desktop可以在设置里直接填镜像地址效果一样。加速器只影响 Docker Hub 的官方仓库不影响你自己推到私有仓库的镜像。3. 核心概念镜像、容器、数据卷、网络3.1 镜像与容器的关系很多新手对镜像和容器这两个概念分不清。我打个比方镜像是类容器是实例镜像是只读的模板容器是基于模板创建的可写运行实体。镜像从哪来两个途径从仓库拉取现成的或者用 Dockerfile 自己构建。一个镜像由多层只读层叠加而成每一层对应 Dockerfile 里的一条指令。这种分层设计的好处是共享和复用——你本地有十个基于 Ubuntu 的镜像底层那几层基础层只需要存一份大大节约磁盘空间。容器则是在镜像的只读层之上加了一层可写层。你对容器的所有修改都写在这一层里。所以容器可以随便折腾坏了删掉用同一个镜像再建一个就是全新的。3.2 数据卷容器删了数据不能丢容器的一个特性是一切皆可丢。容器被删除里面的所有写入数据跟着销毁。但我们的数据库、应用日志、用户上传的文件这些都是不能丢的。解决这个问题的方案就是数据卷Volume。数据卷的本质是把宿主机上的一个目录或一个由 Docker 管理的存储空间挂载进容器内部的指定路径。容器读写这个路径实际上是在读写宿主机目录。容器删了卷还在换一个新的容器启动时挂载同一个卷数据就回来了。创建命名卷的方式docker volume create mydata docker run -d -v mydata:/var/lib/mysql mysql:8.0另一种方式是绑定挂载直接把宿主机的具体目录映射到容器内volumes: - /home/user/app/data:/app/data两者的区别命名卷由 Docker 管理目录位置在/var/lib/docker/volumes/下你不需要关心具体路径绑定挂载则暴露了宿主机真实路径方便直接查看和备份。生产环境中数据库这类数据量大的服务我更喜欢用绑定挂载因为备份、迁移时直接拷目录就行。3.3 自定义网络容器间通信的机制Docker 会为每个容器分配虚拟 IP但直接靠 IP 通信不现实——容器重启后 IP 就变了。正确的做法是用 Docker 网络提供的 DNS 解析能力。当你创建一个自定义网络后连接到同一网络内的容器可以直接用服务名容器名互相访问。比如一个叫mysql的容器和一个叫backend的容器都连到app-network那么backend访问mysql:3306就能直接连上Docker 内置的 DNS 会自动解析成对应 IP。在 Compose 文件中每个服务名天然就是一个可解析的主机名这就让容器间通信变得跟局域网里访问主机名一样简单。理解这一点对排错很重要——很多容器间连不上的问题根源就在网络配置上。Compose 里默认会为你的项目创建一个网络所有服务都在里头。除非有特殊需求一般不用手动定义网络。4. 从 0 到 1写一个完整的 Dockerfile4.1 一个最小可用的 Dockerfile以用 Python Flask 写的一个简单 Web 服务为例。假设项目结构是app/ ├── app.py ├── requirements.txt └── Dockerfile先看最简单版本的 DockerfileFROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 5000 CMD [python, app.py]逐行拆解一下FROM指定基础镜像。python:3.11-slim是精简版比完整版小很多生产环境倾向这种。WORKDIR设定工作目录后续指令都在这个目录下执行不存在会自动创建。COPY把宿主机文件拷进镜像。RUN在构建阶段执行命令这里用来装 Python 依赖。EXPOSE声明容器内监听的端口纯粹是文档性质真正对外映射还是要-p或 compose 里的ports。CMD指定容器启动时执行的命令。构建镜像docker build -t flask-app:v1 .运行docker run -d --name my-flask -p 5000:5000 flask-app:v1这样访问宿主机的http://localhost:5000就能看到 Flask 应用了。4.2 多阶段构建镜像瘦身的关键技巧实际项目中很多应用需要先编译。比如一个 Java 应用要先用 Maven 打包一个 Node 应用要先用 npm 构建前端资源。常规做法是把构建工具和运行时依赖都装进同一个镜像导致镜像动辄几个 GB。多阶段构建就是为了解决这个问题。它在同一个 Dockerfile 里定义多个FROM每个 FROM 是独立的构建阶段前面的阶段负责编译后面的阶段只拷贝编译产物。以 Node.js 应用为例# 构建阶段 FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # 运行阶段 FROM nginx:alpine COPY --frombuilder /app/dist /usr/share/nginx/html EXPOSE 80 CMD [nginx, -g, daemon off;]COPY --frombuilder这个语法是这个方案的精髓可以从之前的构建阶段里拷贝文件。最终镜像只包含 Nginx 和构建好的静态文件没有 node_modules没有源码体积从几百 MB 压缩到几十 MB。多阶段构建还有一个附带好处构建过程中编译工具链里的潜在漏洞不会带进最终镜像安全性也更好。4.3 构建时的缓存逻辑与版本管理Docker 构建是分层的每一条指令如果内容没变会复用之前的缓存层构建速度快很多。但这也带来一个常见坑如果你把COPY . .写在前面那么源码一有任何改动后面所有层都会失效包括安装依赖那层导致每次构建都要重新下载依赖。正确策略是把依赖变化的频率和指令顺序结合起来。先拷贝依赖清单文件安装依赖再拷贝源码。依赖文件相对稳定只要不改依赖就始终命中缓存。这段经验写出来就是三行代码的位置差异但构建效率差了不止一个量级。版本管理上给镜像打标签别用默认的latest。我自己的规则是测试环境打v1.2.3-canary正式环境打v1.2.3同时打一个stable标签指向最新的稳定版本。这样回滚时有明确的版本可依据不会出现昨天还能跑今天 build 一下就挂了的尴尬。5. 编排神器docker-compose.yml 实战5.1 写一个最小的 Compose 文件Compose 文件是 YAML 格式后缀名是.yml或.yaml。一个最小的示例version: 3.8 services: web: build: . ports: - 5000:5000 volumes: - .:/app environment: - FLASK_ENVdevelopment一个完整的 Compose 文件由几个顶级字段组成version现在是注释性质但保留无妨、services、networks、volumes。services下每个 key 代表一个服务这个服务名同时也是容器在网络里可以被其他服务访问的主机名。启动命令docker compose up -d查看状态docker compose ps停止但不删除容器docker compose stop彻底停止并删除容器、网络保留数据卷docker compose downdown后面加-v参数会连数据卷一并删除这个命令慎用数据没了就真的没了。5.2 常用指令详解开发中最常打交道的 Compose 指令就这几个我展开说一下我的使用心得。ports的格式是宿主机端口:容器端口也可以在冒号后追加协议ports: - 8080:80 - 443:8443/tcp注意如果只需要容器间互相访问、不需要暴露到宿主机就不应该写ports。所有 Compose 网络内的服务已经可以通过服务名访问了暴露端口的唯一意义是让外部能访问。volumes可以挂载命名卷也可以挂载宿主机路径volumes: - mysql-data:/var/lib/mysql - ./config:/etc/app/config volumes: mysql-data:depends_on用来控制启动顺序。但它只保证服务启动的先后不代表依赖的服务已经 ready。比如数据库容器起来了但 MySQL 初始化还没完成你的应用去连就可能失败。解决这个问题的办法是让应用带重试机制或者在入口脚本里轮询端口就绪状态。environment设置环境变量可以写固定值也可以用${VAR}从宿主机的环境变量或.env文件里读取。.env文件要加进.gitignore不能把密码提交到仓库。restart策略是生产环境的必需品常用值有no、always、unless-stopped、on-failure。services: app: restart: unless-stoppedunless-stopped是我目前的主力配置只要不是手动 stop容器挂了、机器重启了都会自动拉起来非常适合后台服务。5.3 完整编排实例MySQL Redis 后端应用来一个我实际跑过的完整配置一个后端服务连带 MySQL 和 Redisversion: 3.8 services: mysql: image: mysql:8.0 container_name: myapp-mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: myapp MYSQL_USER: myapp MYSQL_PASSWORD: ${MYSQL_PASSWORD} ports: - 13306:3306 volumes: - mysql-data:/var/lib/mysql command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci healthcheck: test: [CMD, mysqladmin, ping, -h, localhost] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine container_name: myapp-redis restart: unless-stopped volumes: - redis-data:/data healthcheck: test: [CMD, redis-cli, ping] interval: 10s timeout: 3s retries: 5 backend: build: ./backend container_name: myapp-backend restart: unless-stopped depends_on: mysql: condition: service_healthy redis: condition: service_healthy environment: DB_HOST: mysql DB_PORT: 3306 REDIS_HOST: redis REDIS_PORT: 6379 ports: - 8080:8080 volumes: mysql-data: redis-data:这个配置有几个细节值得注意第一depends_on配合condition: service_healthy能真正等到 MySQL 和 Redis 就绪后再启动后端。这是官方新版本推荐的做法比单纯depends_on靠谱得多。第二MySQL 端口映射成13306:3306宿主机上不用冲突 3306 端口。容器间通信走mysql:3306外部运维走13306互不干扰。第三command里加了 utf8mb4 字符集配置避免中文乱码问题。第四所有密码放在.env文件里MYSQL_ROOT_PASSWORDchange_me_root MYSQL_PASSWORDchange_me_app启动只需要两步docker compose up -d docker compose ps日志查看docker compose logs -f backend这套东西我用了很久稳定可靠属于那种配一次迁无数次的典型方案。换服务器时把整个目录compose 文件 .env 数据卷目录带走目标机器上执行docker compose up -d完事。6. 日常运维常用命令与效率技巧6.1 高频命令速查表运维 Docker 不需要背太多命令高频的翻来覆去就这几个# 查看所有容器-a 包括已停止的 docker ps -a # 进入容器执行命令 docker exec -it 容器名 bash # 查看容器日志-f 实时跟踪 docker logs -f 容器名 # 查看所有镜像 docker images # 删除不再使用的容器 docker container prune # 删除悬空镜像 docker image prune # 一键清理所有未使用资源 docker system prune -adocker system prune -a这个命令会把所有没在使用的镜像、容器、网络全部清掉执行前务必确认没有需要保留的东西。我吃过亏想着反正只是清理结果辛辛苦苦拉下来的一堆镜像全没了流量白花时间白费。6.2 进入容器排查问题的正确姿势容器是精简环境很多常用工具vim、curl、ping可能都没装。调试时我一般分两层第一层直接看日志docker logs --tail 100 -f myapp-backend第二层进入容器看进程和网络docker exec -it myapp-backend /bin/sh注意有些镜像基于 Alpine里面只有/bin/sh没有 bash。进去后没有curl、vi用apk add现装也行但生产容器不建议做持久性的改动——容器本来就该是用完即弃的。如果容器处于 Exited 状态可以先启动再进入或者直接看容器日志定位崩溃原因。一个非常常见的排查路径docker ps -a看状态 →docker logs 容器名看错误 → 根据报错调整配置或代码。6.3 数据备份与恢复的实用经验容器本身不用备份要备份的是数据卷。MySQL 备份用官方客户端进容器里执行导出最直接docker exec myapp-mysql sh -c exec mysqldump --all-databases -uroot -p$MYSQL_ROOT_PASSWORD backup.sql恢复时把备份文件传到宿主机再执行cat backup.sql | docker exec -i myapp-mysql sh -c exec mysql -uroot -p$MYSQL_ROOT_PASSWORD这里强调一点管道重定向到docker exec -i时一定要加-iinteractive参数否则标准输入无法传入容器数据就传不进去。我见过不止一个人在这一步卡壳输了一下午命令发现备份文件根本没用。对 Compose 管理的数据卷稳妥的办法是直接备份挂载目录。比如上面例子里 MySQL 的数据在名为mysql-data的命名卷里可以先在宿主机上打包目录sudo tar czf mysql-backup.tar.gz /var/lib/docker/volumes/目录/mysql-data/_data恢复时把文件解压回同一目录然后重启容器即可。7. 踩坑记录常见问题与排查方案7.1 Docker Desktop 启动失败虚拟化相关Windows 上最经典的报错是Docker Desktop failed to start because virtualization support wasnt enabled或者Virtualization support not detected问题根源绝大多数是 BIOS 里没开虚拟化或者 Windows 的虚拟化相关功能没启用。我的排查流程打开任务管理器 → 性能 → CPU看虚拟化一栏是否显示已启用。如果显示已禁用进 BIOS 开启 VT-x 或 AMD-V。控制面板 → 程序和功能 → 启用或关闭 Windows 功能勾选适用于 Linux 的 Windows 子系统和Windows 虚拟机监控程序平台。确认 WSL2 已安装wsl --status如果版本是 1执行wsl --set-version 发行版名 2。改完 BIOS 或系统功能后需要重启电脑然后再启动 Docker Desktop基本都能解决。7.2 容器网络通信失败现象后端容器里访问mysql:3306超时但访问 IP 能通。原因多半是网络没对。两个容器必须处于同一个 Docker 网络才能用服务名解析。排查方法docker network ls docker inspect myapp-backend | grep -A 10 Networks如果服务不在同一网络用docker network connect手动把容器加进去或者在 Compose 文件里确认networks配置一致。还有一个小概率原因是容器名变了——Compose 默认容器名会带上项目名前缀直接写死container_name能避免这类混乱。7.3 容器日志撑爆磁盘这个坑我栽过不止一次。默认情况下 Docker 不限制容器日志文件大小某些疯狂打印日志的应用几天就能写满整个磁盘。解决方案是在/etc/docker/daemon.json里配置全局日志轮转{ log-driver: json-file, log-opts: { max-size: 10m, max-file: 3 } }重启 Docker 生效。已经运行中的容器不受影响要重建才会应用新配置。7.4 容器内时区不对容器默认 UTC 时区如果你的应用要显示本地时间就会发现差了 8 个小时。改用挂载时区文件的方式最省事services: app: volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro或者用环境变量environment: - TZAsia/Shanghai不是所有基础镜像都支持TZ环境变量两个方式结合使用更保险。7.5 容器启动后立刻退出容器一启动就退出的原因通常只有一个前台进程结束。要明白 Docker 容器必须有一个前台运行的进程如果进程退出了容器就跟着退出。比如你写CMD [nginx, -g, daemon off;]里面的daemon off就是让 Nginx 在前台跑。但如果你启动的是一个长期运行的脚本脚本执行完就退出了容器也就结束了。解决办法是在启动命令上确保进程保持在前台或者用tail -f /dev/null这类把主进程挂住。另一个常见原因是进程因错误退出。查docker logs是最快的定位手段别去猜先看日志。7.6 端口被占用docker run或docker compose up时报port is already allocated说明宿主机端口被别的进程或别的容器占了。排查占用端口的进程sudo lsof -i :8080或者是另一个容器占了用docker ps找出来先停掉。最省事的方法就是改个宿主机端口映射比如把 8080 改成 18080。7.7 磁盘空间告急Docker 长时间使用后镜像、构建缓存、悬空卷会占用大量磁盘空间。我一般养成的习惯是每个月做一次docker system df看空间占用然后有针对性清理。构建频繁的开发机可以定期执行docker builder prune清掉无用的构建缓存。8. 关于安全与生产环境的几条忠告讲完实操最后说几个我在生产环境里踩过坑后总结出来的安全习惯。不要在镜像里写死密码。无论是 Dockerfile 里的ENV还是 Compose 文件里的environment都不应该把真实密码直接写进去。用.env文件管理并确保它不在 git 仓库里更严格的场景配合 Docker Secrets 或外部密钥管理服务。别用 root 跑应用。很多基础镜像默认就是 root 用户但这不符合最小权限原则。Dockerfile里创建普通用户再切过去RUN useradd -m -u 1000 appuser USER appuser注意MySQL 这类容器内部有自己独立的管理机制不需要你手动改用户但自己写应用镜像时务必加上这一层。限制资源使用。Compose 文件里可以给服务设置 CPU 和内存限额防止某个容器把整台机器打挂deploy: resources: limits: cpus: 0.5 memory: 512M及时更新镜像和 Docker 版本。容器技术迭代快老版本多少存在一些已修复的漏洞。这不是让你天天追新但至少别停在两年前的老版本上裸奔。镜像都是分层存储每一层都可能藏内容。生产环境使用的镜像要确认来源可信自己构建的镜像要保证基础镜像版本明确可追溯。拉镜像前多看一眼FROM用的是官方仓库还是个人仓库区别很大。根据我个人的体会Docker 和 Compose 最迷人的地方不是它本身有多复杂而是它能把部署这件原本充满不确定性的事情变成一条明确、可复现的路径。把环境、步骤、配置全部代码化之后换机器、扩容、回滚都变成了几分钟的小事——这种踏实感用过的都懂。最后再分享一个小技巧把常用的 Compose 模板分类存档比如nodejsmysql、nginxphpfpm、redissentinel下次搭环境时直接复制粘贴改一改效率翻倍。毕竟部署这件事最大的浪费不是机器不够而是反复折腾那些早该被模板化的琐碎细节。