前段时间好几个朋友问我,Docker到底该怎么入门。市面上的教程要么太零散,要么只甩一堆命令不讲原因,照着敲完还是一头雾水。我自己从Windows上折腾Docker Desktop开始,到用Docker Compose一键拉起MySQL、Redis、RabbitMQ,再到给微服务项目写编排文件,中间踩过的坑确实不少。这篇教程把完整的Docker + Docker Compose部署流程串起来,所有命令都在Windows和Linux上亲测跑通,按安装、概念、命令、编排、实战、排查这条线走下来,目的就是让你照着敲一遍就能把常用服务部署起来。
内容适合两类人:一是刚接触容器的小白,需要有人告诉你哪些步骤容易翻车;二是已经用了几天Docker,但对Compose配置、数据卷、生产环境参数还不太清楚的同学。这篇不写虚的,全是实际能落地的操作和参数,文末还附了常见问题的排查思路,遇到报错直接翻到对应章节就行。
1. 环境准备:Docker安装全流程与Windows高频报错
1.1 Windows上安装Docker Desktop:先把虚拟化检查一遍
Windows上安装Docker Desktop其实不难,难的是装完点开直接报错。最常见的错误长这样:
“Docker Desktop failed to start because virtualisation support wasn't detected.”
看到这个先别急着卸载重装,按顺序排查:
- 确认CPU虚拟化已经开启。打开任务管理器 -> 性能 -> CPU,看右下角“虚拟化”是否显示“已启用”。如果显示“已禁用”,需要进BIOS把Intel VT-x(AMD机器是SVM Mode)打开,这个选项一般在Advanced或CPU Configuration里。
- 开启WSL2和虚拟机平台。以管理员身份打开PowerShell,执行下面两条命令,然后重启电脑:
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart- 确认WSL2内核是新的。执行wsl --update或者wsl --set-default-version 2。如果机器里的WSL内核太老,Docker Desktop连不上WSL后端,照样启动失败。
- 装完Docker Desktop后,如果提示需要注销或重启,就按它说的做,这一步千万别跳。
还有另一个高频报错:
“Failed to connect to the Docker API at npipe:////./pipe/docker-desktop...”
这个通常是Docker引擎没起来。处理方式:右下角鲸鱼图标右键选Restart;不行就退出Docker Desktop,打开任务管理器结束掉所有Docker相关进程再重新打开;再不行就在PowerShell里执行wsl --shutdown,等几秒重新启动Docker Desktop。绝大多数情况走完这三步就恢复了。
提示:Windows上装Docker Desktop,建议选择使用WSL2后端的版本,因为Hyper-V后端在部分老系统上问题更多。另外要明确一点,Docker Desktop更适合本地开发调试,生产环境还是用Linux服务器上的Docker引擎更稳。
1.2 Ubuntu上安装Docker:官方源流程
Linux服务器上安装Docker,我推荐用Docker官方apt源,别直接用系统自带的docker.io,那个版本太老,Compose插件也没法方便地一起装。
以下命令在Ubuntu 20.04、22.04、24.04上都实测过:
# 安装依赖 sudo apt update sudo apt install -y ca-certificates curl gnupg lsb-release # 添加Docker官方GPG密钥 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 a+r /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引擎、CLI和Compose插件 sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin装完分别验证三个命令:
sudo docker --version sudo docker compose version sudo systemctl enable docker sudo systemctl start docker注意Compose这里的命令是docker compose,中间是空格,这是新版Compose v2插件的用法。很多老教程里写的是docker-compose(带横杠),那是Python版独立程序,两者理念一样但命令名和安装方式不同,千万别混用。
1.3 处理docker权限错误:别再每次加sudo
用非root用户执行docker ps,如果出现“permission denied”,是因为当前用户不在docker组里。解决办法:
sudo usermod -aG docker $USER newgrp docker然后重新执行docker ps验证。这里多说一句:加入docker组的用户等同于拥有root权限,个人开发机这么干没问题,但生产服务器上给哪些账号加docker组,最好走正规审批流程,别图省事一把梭。
2. 核心概念:镜像、容器、数据卷、网络是怎么配合的
2.1 镜像和容器的关系:模板与实例
Docker里最基础的一对概念就是镜像和容器。我习惯把这两者类比成“类与对象”:镜像是模板,容器是模板跑起来的实例。同一个镜像,你可以同时启动多个容器,彼此互不影响。比如mysql:8.0这个镜像,可以拉起一个测试库,再拉起一个正式库,只要端口、数据目录、配置分清楚就行。
镜像本身是分层存储的。每一条RUN、COPY指令都会生成一层,所以构建过镜像的机器再次构建时会走缓存,速度很快。反过来,如果你把COPY文件放在Dockerfile很靠前的位置,文件一改动,后面所有层的缓存会全部失效,这就是为什么写Dockerfile时要把改动频繁的操作放后面。
2.2 数据卷与端口映射:数据不能丢,外面要能访问
容器默认是隔离的,直接导致两个问题:容器删了,里面的数据就没了;容器重启后,写入的文件也可能丢。解决办法是挂载数据卷。数据卷有两种常用方式:
- 命名卷:比如Compose文件里
volumes: mysql-data,数据由Docker统一管理,适合数据库这类场景。 - 绑定挂载:直接把宿主机目录映射进容器,比如
./mysql/conf:/etc/mysql/conf.d,特点是你直接能看到和修改文件,适合配置文件、代码目录。
端口映射则是把容器的端口暴露到宿主机。命令里-p 3306:3306,左边是宿主机端口,右边是容器端口。为什么右边不能随便改?因为容器里的MySQL默认监听3306,你映射宿主机3307,外面就连3307,容器里依然是3306。
注意:一条常用但容易踩坑的命令——
docker run --rm。加上这个参数,容器退出后会自动删除,临时调试非常方便,但如果你是想长期运行的服务,千万别加,否则容器一停就没了。
2.3 网络模型:容器间通信别用IP,用服务名
容器每次重启IP都可能变,所以容器之间互相访问时,不要写死IP,而应该通过服务名或容器名解析。单容器用docker run不指定网络时,会加入默认的bridge网。
Docker Compose的方便之处在于,同一个Compose项目下的所有service会自动加入同一个自定义网络,service名就是hostname。比如编排文件里定义了一个叫mysql的service,应用容器里连数据库就可以写host: mysql,端口3306,这个“mysql”就是Docker内置DNS解析出来的。
这一步理解透了,后面部署微服务、连中间件就顺了。很多人在单容器阶段觉得Docker没什么用,其实就是因为没体会到网络编排带来的便利。
3. Docker常用命令:命令用熟了才谈得上部署
3.1 镜像命令:拉取、查看、删除、打Tag
镜像相关的命令,日常用的就是这几个:
# 拉取镜像,不写tag默认latest docker pull nginx:1.25 # 查看本地镜像 docker images # 给镜像打tag,方便推到私有仓库 docker tag nginx:1.25 registry.example.com/nginx:1.25 # 删除镜像,注意没有容器在用它才能删 docker rmi nginx:1.25 # 清理悬空镜像,也就是出现<none>的中间产物 docker image prune说到latest,我以前图省事经常pull latest,后来被坑过一次:某天重新部署,镜像拉下来直接版本大升级,行为全变了。生产环境一定把tag写死,比如mysql:8.0、redis:7.2,不要用mysql:latest。
3.2 容器命令:run、ps、logs、exec、cp
容器操作是日常用得最多的:
# 启动一个nginx容器,挂载目录并映射端口 docker run -d --name web -p 8080:80 -v /opt/html:/usr/share/nginx/html nginx:1.25 # 查看运行中的容器 docker ps # 查看所有容器,包括已退出的 docker ps -a # 查看日志,-f表示跟随输出 docker logs -f web # 进入容器内部,推荐用exec而不是attach docker exec -it web bash # 把宿主机文件拷进容器 docker cp app.jar web:/app/ # 强制删除容器 docker rm -f web这里专门提一下:docker run -d是后台执行,-it通常用于交互,两者一般不同时用。想进容器调试,用docker exec -it,别用docker attach,attach会把容器主进程的输出刷你一脸,用过一次的人基本都不想再用。
3.3 启动参数里藏着三个容易忽略的细节
docker run参数很多,但有三个我建议每次都检查:
--restart unless-stopped:容器异常退出会自动重启,服务器重启也会自动拉起来。没有这个参数,机器一重启服务全挂。--name:给容器起个有意义的名字,看日志和删除时非常方便,不然会冒出一串随机字符。--network:多个容器要互相访问就放到同一个自定义网络,先docker network create建一个,再启动容器时指定进去。
4. Docker Compose:多容器部署的正确姿势
4.1 为什么需要Compose,什么时候别硬用命令
单容器用docker run就够了,但真实项目从来不止一个容器。一个典型的后端服务至少包含应用、MySQL、Redis,再加上队列和前端静态页面,如果每个都用docker run手敲,命令会越来越长,参数也不统一,同事接手看半天也理不清。
Compose的价值就是把“一组容器的定义”写成一份声明式配置文件,docker compose up -d一条命令全起,docker compose down一键全停。配置纳入git管理,换台机器clone下来直接部署,这才是可复现的部署方式。Dify、GitLab这些开源项目自带的docker-compose.yml,本质上也是这么一回事。
4.2 Compose安装方式与版本确认
Ubuntu官方docker-ce源里已经带了docker-compose-plugin插件,装完就是docker compose命令。验证一下:
docker compose version如果服务器上只有旧版docker-compose独立二进制,也可以手动装:
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose docker-compose --version两种方式二选一,我更建议用插件方式,命令更统一。看资料时注意区分:新版是docker compose,旧版是docker-compose,看到老教程里的横杠命令别直接复制。
4.3 compose.yml的核心字段逐个拆解
一份最小的Compose文件长这样:
version: "3.8" services: web: image: nginx:1.25 container_name: web ports: - "8080:80" volumes: - ./html:/usr/share/nginx/html restart: unless-stopped逐个字段说明:
- version:声明Compose文件格式版本,Compose v2已经不怎么强制了,但写“3.8”能兼容更广泛的工具链。
- services:定义所有容器,每个service一个名字。
- image:用哪个镜像。
- build:如果要用Dockerfile本地构建,就指定构建上下文。
- container_name:容器名,不写则Compose按“项目名-服务名-序号”自动生成。
- ports:端口映射,字符串形式要加引号,不然YAML里
8080:80可能被解析成数字。 - volumes:挂载。
- environment:环境变量,敏感信息建议改用env_file。
- depends_on:声明服务启动顺序。
- restart:重启策略。
- networks:指定加入哪个网络。
运行几个关键命令:
# 校验compose文件语法,强烈建议每次改完先跑一遍 docker compose config # 后台启动 docker compose up -d # 查看状态 docker compose ps # 看日志 docker compose logs -f web # 停止并删除容器(网络会保留,数据卷不会删) docker compose down # 连数据卷一起删,慎用 docker compose down -v注意:
docker compose down不会删除命名卷,但加上-v会把volumes里定义的数据也一并清掉。我自己有一次执行down -v,本意是清理测试环境,结果把本地开发数据库也删了。教训就是:手别快,先看清里面有没有业务数据。
5. 实战部署:用Compose一键拉起常用服务与微服务
5.1 MySQL 8.0:字符集、时区与数据持久化
先给一份我在开发和测试环境都在用的MySQL Compose配置:
services: mysql: image: mysql:8.0 container_name: mysql8 restart: unless-stopped ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: "Root@123456" MYSQL_DATABASE: demo TZ: Asia/Shanghai command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --lower_case_table_names=1 - --default-time-zone=+08:00 volumes: - ./mysql/data:/var/lib/mysql - ./mysql/conf:/etc/mysql/conf.d healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p$$MYSQL_ROOT_PASSWORD"] interval: 10s timeout: 5s retries: 5几个容易踩的点:
- 字符集不提前指定,容器里默认是latin1,插入中文直接乱码。所以command参数里charset必须显式指定utf8mb4。
lower_case_table_names=1是让表名不区分大小写。如果你在Windows上开发、Linux服务器上部署,这个参数建议两边统一,不然表和索引名大小写不一致,会排错老半天。注意MySQL 8.0里这个参数在初始化时才能改,所以第一次启动就要带上。- 数据目录挂载到宿主机
./mysql/data,容器删除、重建,数据都还在。 - 健康检查里写的是
$$MYSQL_ROOT_PASSWORD,两个美元符号是Compose的转义写法,如果只写一个$,YAML环境变量解析时会变成空值。 - root密码直接放Compose文件里只适合测试环境,生产环境至少要用env_file,配合.gitignore不要把env文件提交到git。
5.2 Redis:开启AOF和主从,别让人踩了生产环境的坑
Redis的单容器配置很简单:
services: redis: image: redis:7.2 container_name: redis restart: unless-stopped ports: - "6379:6379" command: redis-server --appendonly yes --requirepass Redis@123 --bind 0.0.0.0 volumes: - ./redis/data:/data这里三个点:
appendonly yes:开启AOF持久化。Redis容器默认配置是不持久化的,机器重启等于数据全部清零。不是所有业务都能接受这个,所以一定要提前开启。requirepass:设置密码。注意设了密码之后,主从模式下从库还要配masterauth,否则从库连不上主库。bind 0.0.0.0:让容器外能访问。生产环境别把6379直接对公网开放,尽量只让内网或同一Compose网络里的应用访问,需要对外就加防火墙策略。
主从配置也不难,一个master,一个replica。replica服务的command加一行:
redis-server --slaveof redis-master 6379 --masterauth Redis@123 --appendonly yes --requirepass Redis@123服务名redis-master要能解析,同一个Compose网络下直接把service名当hostname用就行。如果还要做故障自动切换,就得引入哨兵或者直接上Redis Cluster,那又是另一个话题了。
5.3 RabbitMQ:默认账号的坑与可视化面板
RabbitMQ的Compose配置我常用这个:
services: rabbitmq: image: rabbitmq:3-management container_name: rabbitmq restart: unless-stopped ports: - "5672:5672" - "15672:15672" environment: RABBITMQ_DEFAULT_USER: admin RABBITMQ_DEFAULT_PASS: Admin@123 volumes: - ./rabbitmq/data:/var/lib/rabbitmq选带-management后缀的镜像,自带Web管理面板。这里最大的坑是:RabbitMQ默认的guest账号只能在localhost登录,你用宿主机IP去访问15672,会一直提示登录失败。解决办法就是像上面一样,启动时通过RABBITMQ_DEFAULT_USER和RABBITMQ_DEFAULT_PASS创建自己的管理员账号。
5672是amqp协议端口,应用连消息队列用的;15672是管理界面端口。端口映射出去之前想清楚:管理界面暴露到公网等于把密码爆破面也暴露了,测试环境还好,生产环境要收敛。
5.4 微服务项目的Dockerfile与Compose编排
针对Java微服务,我习惯用多阶段构建,既能保证构建环境完整,又不让最终镜像带上maven和源码。以Spring Boot项目为例:
# 第一阶段:构建 FROM maven:3.8-openjdk-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests # 第二阶段:运行 FROM openjdk:17-jdk-slim WORKDIR /app COPY --from=builder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]然后Compose里这样引用:
services: order-service: build: ./order-service container_name: order-service restart: unless-stopped ports: - "8080:8080" environment: SPRING_PROFILES_ACTIVE: dev depends_on: - mysql - redis - rabbitmq关于depends_on,这里必须说清楚:它只保证服务启动顺序,不保证依赖服务已经可以接受连接。实际使用中,应用连不上数据库大概率会自己重试,但更稳妥的做法是给mysql、redis配healthcheck,然后在order-service里设置condition: service_healthy,这样的编排才算真正“健康”的编排。
如果你要部署的不是自研微服务,而是Dify这类开源项目,读完这篇以后再去看它自带的docker-compose.yml,会发现已经能看懂80%的配置了,剩下20%无非是env文件、外部卷和自定义网络这些我们已经聊过的东西。
6. 常见问题与排查技巧实录
6.1 容器启动后秒退:先看exit code
容器一上来就退,这在刚接触Docker时几乎必遇。正确姿势是:
# 看容器状态和退出码 docker ps -a # 看完整日志 docker logs --tail 200 <容器名>常见的几种情况:
- 端口被占用:日志里出现“bind: address already in use”,把宿主机对应端口空出来就行。
- 配置不对:比如MySQL初始化参数写错,容器直接退出,看日志就能明白。
- 命令问题:自己写的Dockerfile,最后一条命令执行完,前台进程退出,容器就跟着退。所以启动命令要写成前台驻留形式,Redis是
redis-server,Nginx是nginx -g 'daemon off;',Java应用就正常执行java -jar。
6.2 端口被占用:address already in use
这个错误我碰到过好多次,尤其是3306、6379这类常用端口,宿主机上可能已经有一个原生MySQL或Redis占用了。
排查命令:
sudo lsof -i:3306 # 或者 sudo ss -lntp | grep 3306处理方式有三种:停掉宿主机原有服务;把Docker映射端口改成3307、6380这种不冲突的端口;或者反过来保留Docker服务,给宿主机原生服务换端口。对我个人来说,能不用原生服务就尽量不用,Docker环境隔离得太好,卸载重装也不脏系统。
6.3 容器时间和宿主机不一致:时区问题
容器默认时区是UTC,你看到日志里时间少了8个小时,大概率就是时区没设置。解决方案是启动时加环境变量TZ:
environment: TZ: Asia/Shanghai写代码读取数据库时间时也留个心眼,MySQL的时区配置、Java应用里JDBC连接串的serverTimezone参数,三处时区不一致就会出现“怎么差了8小时”这种疑难杂症。排查思路:先date查看容器时间,再查应用日志,最后看数据库当前时间,哪一环偏了就改哪一环。
6.4 磁盘被镜像和日志占满:清理与预防
Docker用久了,磁盘被撑爆是早晚的事。先体检:
docker system df看到一堆悬空镜像、构建缓存、无用网络之后,执行:
# 删除所有停止的容器、悬空镜像、无用网络、构建缓存 docker system prune -af # 单独清理没有被任何容器使用的数据卷,慎用! docker volume prune注意:
docker system prune -af不会删数据卷,但docker volume prune会把没有被容器引用的数据卷全部删干净。如果里面有你想留的数据,提前确认再执行。
预防方案:Compose里给日志加尺寸限制,每个服务都加这段:
logging: driver: json-file options: max-size: "10m" max-file: "3"这个配置很容易被忽略,不加的话容器日志会无限增长。我亲眼见过一台机器被一个日积月累的容器日志塞满,清理起来特别狼狈。
6.5 快速定位挂载、环境变量和IP问题
想把容器的问题一次看明白,最直接的是docker inspect:
# 查看容器的完整配置、挂载、环境变量、网络IP docker inspect <容器名>如果容器起不来,又想验证Compose写得对不对,先docker compose config输出解析后的最终配置,很多手滑写错的地方一眼就能看出来。
最后把这一章的排查思路浓缩成一张速查表,贴到笔记里随手能翻:
| 现象 | 排查方向 | 处理建议 |
|---|---|---|
| 容器秒退 | 退出码与日志 | docker ps -a + docker logs --tail 200 |
| 端口冲突 | 宿主机端口占用 | lsof -i:端口,换映射端口或停占用的服务 |
| 数据丢失 | 未挂载数据卷 | 持久化卷或绑定挂载 |
| 容器内文件不符合预期 | 挂载未生效 | docker inspect 查看Mounts |
| 服务间无法通信 | 网络不一致 | 同Compose网络,用service名访问 |
| 磁盘爆满 | 镜像、日志、构建缓存 | docker system df + prune |
最后的一点实践建议
教程写到这里,基本把Docker和Docker Compose从安装到实战的流程走完了。最后说几个我自己的使用习惯,算是一个补充。
第一,生产环境所有镜像tag都固定,绝对不用latest。第二,每次写Compose文件前先想清楚数据卷规划,哪些要持久化、哪些可以随容器销毁,数据库和消息队列这类有状态服务一定要持久化。第三,修改Compose文件后先docker compose config校验一遍再up,能省掉很多低级错误。第四,如果机器配置一般,给每个服务加上资源限制,避免一个容器把宿主机内存吃满。
这几点听着琐碎,但大多数容器翻车现场都跟它们有关。你把这套流程跑熟之后,不管是部署中间件、开源项目还是自己的微服务,思路都是相通的——网上任何带docker-compose.yml的项目,你都能一眼看明白它干了什么,哪里要改心里也有数。