项目上线前一周,测试环境一切正常,到了生产服务器上,接口超时、数据库连接被拒、日志打印乱码,三个服务互相指责对方的版本不对。这种场景在很多团队里都上演过。后来我们决定把Docker真正用起来,从开发到部署的整条链路统一标准化,才把这类问题逐步根治。这篇文章就拿我们Spring Cloud微服务项目从构建到部署的完整过程来说,把Docker从安装、镜像、网络到Compose编排的核心要点拆开讲一遍。无论你是在Windows上第一次双击Docker Desktop,还是已经在Linux服务器上维护着十几个容器,这篇内容应该都有可以拿走直接用的部分。
1. 为什么我反复跟团队强调“Docker不是虚拟机”
刚接触Docker的人最容易犯的错误,就是把它理解为“一个很轻的虚拟机”。如果抱着这个认知去做微服务部署,后面每一个设计决策都会跑偏。我见过不少同事往容器里塞systemd、塞完整的Ubuntu桌面、甚至试图用容器跑KVM,最后全都撞得头破血流。
1.1 容器与虚拟机的本质区别
虚拟机是通过Hypervisor虚拟出一套完整的硬件,然后在上面运行一个完整的操作系统。每个虚拟机都有自己的内核、文件系统、网络栈,启动要几十秒甚至几分钟,占用几个GB的内存都很正常。
而Docker的容器共享宿主机内核,它只通过Linux的namespace和cgroup做隔离。容器里面看起来有个文件系统,有进程、有网络接口,但实际上所有容器都在同一个内核之上。打个比方:虚拟机是每个人租了一整套独立房子,水电气全部分开;容器是很多人住在同一栋楼里,墙壁和门是隔开的,但水管、电线、地基是共享的。
这个区别带来两个直接结论:
- 容器启动只需要创建进程和分配隔离空间,所以秒级启动、内存占用低,一台4GB的服务器跑十几个容器完全可能。
- 容器内运行的程序必须能在宿主机内核上跑。这意味着你没法在一个Linux宿主机上跑Windows容器,也没法在一个老旧的Linux内核上跑需要新内核特性的服务。
1.2 从一次“在我机器上是好的”说起
我们当时有个订单服务,本地连的是MySQL 8.0,测试环境连的是MySQL 5.7,生产环境更“经典”,用的是别人维护的MariaDB。结果某个SQL用了MySQL 8.0才支持的窗口函数,测试阶段没人发现,一到生产直接报语法错误。负责的同事第一反应是“线上环境有问题”,后来一查,三套环境的数据库版本都不一样。
这就是微服务时代的部署困境:服务数量一多,每个服务的依赖版本组合呈指数级增长。三套环境、五个服务、每个服务四五个依赖,谁也没法保证所有环境完全一致。
Docker解决这个问题的方式很朴素:把运行环境连同依赖一起打包进镜像。开发者在镜像里用MySQL 8.0,QA拉同一个镜像也是MySQL 8.0,生产环境直接run这个镜像,跑的还是MySQL 8.0。镜像就是部署的“合同”,它把运行环境从物理机和操作系统里解耦出来。
1.3 微服务部署为什么离不开Docker
微服务拆分之后,每个服务都是一个独立进程,有自己的语言运行时、依赖库、配置文件。如果用传统方式部署,得在一台服务器上装好JDK、Nginx、Redis、数据库,再手工拷贝各种Jar包、脚本、配置。服务器一多,人肉操作必然出错。
Docker给微服务带来的东西,我总结成三件事:
- 标准化的交付物:一个服务交付的不是“源码+部署文档”,而是一个镜像,镜像能跑,环境就到位。
- 进程级别的隔离:不同服务之间的依赖冲突被隔离,A服务的Netty版本不会干扰B服务的Netty版本。
- 快速编排与横向扩展:要扩容商品服务?
docker compose up -d --scale product=3,三份实例直接拉起,配合负载均衡就能分担流量。
2. 环境准备:Docker安装时最容易翻车的三个环节
很多新手第一步就被卡住了,尤其是Windows用户,安装完Docker Desktop双击图标,结果一直停在“Docker Desktop is starting...”转圈,然后弹出一句莫名其妙的话。
2.1 Virtualization support报错的完整排查链路
那句经典报错是:virtualization support not detected, docker desktop failed to start because virtualiztion support is disabled。这里我要先纠正一个拼写误区,报错里经常出现virtualiztion,这是Docker Desktop自身文案的拼写问题,很多人误以为自己下错了版本,其实不是。
这个报错的本质是:Docker Desktop需要硬件虚拟化支持,但当前系统没有开启,或者被其他软件占用了。排查链路按顺序来:
- 打开任务管理器,切到“性能”标签,看右下角“虚拟化”是否显示“已启用”。如果显示“已禁用”,需要进BIOS开启Intel VT-x或AMD-V。这一步的坑在于不同主板BIOS入口完全不同,有的在
Advanced -> CPU Configuration,有的在Security -> Virtualization,最笨也最有效的办法是把BIOS里所有带Virtualization、VT-x、SVM字样的选项全部设为Enabled。 - 如果BIOS里已经开启,任务管理器也显示“已启用”,但Docker还是报这个错,检查是否装了Hyper-V相关的其他虚拟化软件,比如旧版VirtualBox、VMware Workstation,或者安卓模拟器。它们和Windows的Hypervisor可能冲突,最典型的就是开启Hyper-V之后旧版VirtualBox没法用。
- Windows家庭版用户要注意:Docker Desktop从某个版本开始依赖WSL2,如果你的系统是旧版家庭版,需要先确认Windows版本号。
winver查看,如果是1903以前的老版本,先更新Windows再装,否则直接装Docker Desktop会各种莫名其妙。
当时我们团队有个同事的笔记本就是第三种情况,BIOS里都开了,任务管理器也显示启用,但Docker Desktop怎么都起不来。最后发现他装了某款模拟器,模拟器先占了Hyper-V的虚拟化层,把模拟器卸载重装Docker就正常了。
2.2 WSL2和Hyper-V到底选哪个
Docker Desktop在Windows上有两种后端:WSL2和Hyper-V。新版本默认用WSL2,我建议不要改。
原因很简单:
- WSL2启动快、内存占用比Hyper-V轻,而且你可以把WSL2的虚拟硬盘文件放在自定义目录,避免塞满C盘。
- WSL2和你的日常Linux开发环境是同一个内核,你在WSL里装的东西和容器共享网络栈更自然。
- Hyper-V后端隔离性更强,适合企业强管控场景,但性能开销大,启动慢,日常开发没必要。
如果你要改后端,在Docker Desktop的Settings -> General里切换。切换之后Docker会重启,原有容器不会丢,但需要注意镜像存储位置可能变化,生产环境别乱切。
2.3 Linux服务器安装与镜像加速配置
Linux上安装Docker很简单,curl -fsSL https://get.docker.com | bash可以一键装,但生产服务器我不建议这么干。更好的做法是使用发行版官方源装,以Ubuntu为例:
sudo apt update sudo apt install ca-certificates curl gnupg 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 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 sudo apt update sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin这里有个细节:官方源用download.docker.com,在某些网络环境下访问比较慢,但装的过程多等一会儿一般能完成。安装完成后执行docker info验证。如果你看到permission denied,说明当前用户不在docker组里:
sudo usermod -aG docker $USER然后退出重新登录。这个操作很多人忘了,之后每次都要sudo,非常难受。
镜像加速的问题也提一下。Docker Hub的访问速度在不同网络环境下差异很大,docker pull经常卡住。Docker Desktop用户可以在Settings -> Docker Engine里配置registry-mirrors,Linux用户则修改/etc/docker/daemon.json:
{ "registry-mirrors": ["https://你的镜像加速地址"] }改完重启Docker。这一步属于Docker常规配置,能显著提升拉取镜像的体验。
3. 镜像与容器:从MySQL 8.0部署理解Docker的核心逻辑
安装好Docker之后,你手上的“玩具”就是镜像和容器。我当时让团队所有人做的第一个练习不是Hello World,而是用Docker部署一个MySQL 8.0并让本地项目连上它。这个练习覆盖了镜像拉取、容器运行、端口映射、数据卷、环境变量五个核心概念。
3.1 镜像分层是什么,为什么Dockerfile那么快
Docker镜像不是一个大文件,而是由很多只读层叠加而成。每一层记录文件系统的一个变更,比如装了一个软件包、复制了某个配置文件。当你基于某个镜像构建新镜像时,公共层是可以重复使用的,不需要重新下载。
这就是为什么基于同一个基础镜像构建多个服务,磁盘占用不会成倍增长。也是为什么你改一行代码重新构建时,只需要重新下载/构建变更的层,其余层直接走缓存。Dockerfile里每一行指令都可能生成一个新的层,所以指令顺序讲究“变化少的往前放,变化多的往后放”。
举个最简单的例子:
FROM openjdk:17-jdk WORKDIR /app COPY target/app.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]这里COPY target/app.jar后面不能有任何执行步骤依赖先前的缓存层,否则改一次Jar包,后面所有层全部失效。如果你有大量依赖安装的步骤,建议把依赖声明文件先COPY进去,跑完安装命令,再COPY源码,这样依赖层不会因为源码变化而反复重建。
3.2 部署MySQL 8.0的完整命令拆解
先说明:这里假设你只是想快速跑一个MySQL实例给开发用,生产环境还是要考虑配置、备份和高可用。命令是:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root123 \ -e MYSQL_DATABASE=commerce \ -v mysql_data:/var/lib/mysql \ -v /etc/localtime:/etc/localtime:ro \ mysql:8.0逐项拆解:
-d:后台运行,不加这个的话,容器会挂在当前终端前台。--name mysql8:给容器起一个名字,后续docker logs mysql8、docker exec -it mysql8 bash都要用这个名字。-p 3306:3306:把宿主机的3306端口映射到容器的3306端口。宿主机端口在冒号左边,容器端口在右边。宿主机端口冲突是最常见的启动失败原因,换个宿主机端口比如-p 3307:3306就能解决。-e:注入环境变量。MySQL镜像用MYSQL_ROOT_PASSWORD设置root密码,MYSQL_DATABASE会创建一个额外数据库。-v mysql_data:/var/lib/mysql:这是把容器里的MySQL数据目录挂载到一个名为mysql_data的卷里。没有这一行,容器删了数据就会丢失。
启动后可以这样验证:
docker ps docker exec -it mysql8 mysql -uroot -proot123如果连接不上,优先看日志:
docker logs mysql8我遇到过最典型的启动失败是docker run时报Bind for 0.0.0.0:3306: port is already allocated,这就是宿主机3306端口被占用了,要么停掉占用进程,要么换端口。
3.3 数据卷:为什么容器被删,数据不能跟着没了
容器本身是临时性的。你可以随时docker rm -f mysql8把它删掉,但删掉不等于删掉数据——前提是你挂载了数据卷。
Docker的数据卷有两种主要形式:
- 命名卷:
-v mysql_data:/var/lib/mysql,Docker管理卷的存储位置,你不用关心它具体在哪,备份用docker run --rm -v mysql_data:/data alpine tar -czf /tmp/mysql_data.tar.gz -C /data .。 - 绑定挂载:
-v /home/user/mysql-data:/var/lib/mysql,直接映射宿主机目录,好处是你可以在宿主机上直接看到数据文件,适合自己管理备份策略。
我在生产环境更推荐绑定挂载,因为备份思路更直白:把宿主机目录纳入定期备份计划即可。但要注意权限,容器里的MySQL进程是以mysql用户运行的,如果宿主机目录所属用户不对,容器启动时会报权限错误。解决方法是先让目录对应用户有权限,或者用--user参数指定UID。
4. 网络不通是新手第一大敌:Docker网络模型实战
在所有Docker相关的报错里,“网络不通”应该是最让人头疼的一类。明明是同一个宿主机上的两个容器,互相ping不通;明明端口映射了,外部访问还是拒绝连接;服务A自称启动成功,服务B却说连不上数据库。这些问题的根源,大多是对Docker网络模型理解不到位。
4.1 bridge、host、none三种模式到底怎么选
先记住一句话:Docker容器默认的网络模式是bridge,所有容器都在一个虚拟的NAT网络里,容器之间默认是不能通过IP直接通信的,需要通过端口映射或者自定义网络。
bridge:默认模式,容器有自己的虚拟网卡和IP,宿主机通过端口映射把流量转发到容器。适合单机多个服务的场景。host:容器直接共享宿主机的网络命名空间,没有独立IP,端口也不用映射,直接用宿主机端口。性能好,但隔离性差,两个host模式的容器不能监听同一个端口。none:没有网络,只能本地回环,适合对网络隔离要求极高的场景,日常用不到。
我们当时在服务器上部署微服务,网关注册到Nacos的地址是内网IP,服务之间调用走的是网关转发,一直强调用bridge模式配合自定义网络,而不是host模式。原因很简单:host模式会让每个服务直接占用宿主机端口,服务数量一多,端口管理就变成灾难。
4.2 端口映射失败的完整排查链路
有一次测试反馈说商品服务的外部地址死活访问不了,页面一直转圈。这个问题的排查链路非常有代表性,我建议按顺序走:
第一步,确认容器确实在运行。docker ps -a,如果容器状态是Exited,先看日志:
docker logs product-service第二步,确认容器内部端口确实正常。进入容器测试:
docker exec -it product-service bash curl localhost:8080/actuator/health如果这里通了,说明应用本身没问题,问题出在映射或外部链路。
第三步,确认宿主机端口映射。docker ps里看PORTS列。如果是0.0.0.0:8080->8080/tcp,说明映射存在。这时用ss -tlnp | grep 8080查看端口监听情况。如果看到docker-proxy在监听,说明Docker的端口转发正常。
第四步,检查宿主机防火墙。这一步经常被忽略。如果宿主机开了ufw或firewalld,外部访问会被拦截。当时我们的问题就出在云服务器安全组只放行了80端口,没有放行8080。Docker的端口映射做得再好,流量在宿主机外面就被安全组拦掉了。
这个排查链路的顺序很重要:从内到外,先确认容器内、再确认映射、最后看外部防火墙。别一上来就问防火墙,容易漏掉前面更基础的环节。
4.3 自定义bridge网络:让服务用名字互调
默认的bridge网络里,容器之间虽然能互相访问,但IP是动态分配的,重启一次就变了。微服务之间调用如果靠IP,分分钟断连。
解决办法是创建自定义bridge网络:
docker network create commerce-net启动容器时指定网络:
docker run -d --name mysql8 --network commerce-net -p 3306:3306 mysql:8.0 docker run -d --name product-service --network commerce-net -p 8080:8080 product:latest在自定义网络里,Docker自带DNS服务,容器可以直接用名字解析到对方的IP。也就是说,product-service里配置的数据库地址可以直接写jdbc:mysql://mysql8:3306/commerce,不需要知道MySQL容器的IP到底是多少。
这里有个反直觉的地方:docker exec -it进入容器后ping mysql8是通的,但如果你用docker run临时起一个容器在默认网络里,它没法通过mysql8这个名字访问自定义网络的容器。每个网络是隔离的命名空间,容器可以同时加入多个网络,但启用时用--network指定哪个网络里进行服务发现。
5. Docker Compose:微服务编排的“总配电箱”
当你的微服务数量超过三个,一条条手敲docker run就不现实了。记不住参数、容易写错端口、环境不一致,而且每多一个容器,启动顺序就得靠人脑维护。这时候Docker Compose就该上场。
5.1 从shell脚本失控说起
我给不少团队做过咨询,见过很多“看似自动化”的部署:一个几百行的shell脚本,里面全是docker命令、sleep 10、再docker run下一个。这种脚本一开始能跑,等服务和环境多了之后,改一个端口就要在一堆命令里找,而且sleep等待完全是赌运气,数据库还没准备好,业务服务已经启动,然后疯狂重试。
Compose的思路非常直接:把整个应用的容器配置写在一个docker-compose.yml文件里,启动用docker compose up -d,停止用docker compose down,日志聚合用docker compose logs -f。文件即配置、配置即代码,可以进版本库维护。
5.2 用compose搭建MySQL与Redis主从的完整示例
先看一个比较典型的开发环境Compose文件,包含MySQL 8.0和Redis主从两个组件:
services: mysql: image: mysql:8.0 container_name: commerce-mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: commerce ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-proot123"] interval: 5s timeout: 3s retries: 10 redis-master: image: redis:7 container_name: redis-master restart: unless-stopped ports: - "6379:6379" command: ["redis-server", "--requirepass", "redis123"] redis-slave: image: redis:7 container_name: redis-slave restart: unless-stopped depends_on: - redis-master ports: - "6380:6379" command: ["redis-server", "--slaveof", "redis-master", "6379", "--requirepass", "redis123", "--masterauth", "redis123"] volumes: mysql_data:几点经验:
restart: unless-stopped:容器异常退出时会自动重启,这个配置对线上服务非常友好。除非手动stop,否则它一直保持运行。healthcheck:这是Compose里容易被低估的配置。MySQL的mysqladmin ping能真实反映数据库是否可接受连接,比单纯看进程活着靠谱得多。- Redis从库通过
--slaveof redis-master 6379指定主库,这里的redis-master直接用了服务名,Docker内置DNS会自动解析,这就是Compose网络模型的便利之处。
5.3 depends_on和healthcheck:微服务最典型的“假装连不上”
很多人写Compose时喜欢用depends_on控制启动顺序,但如果没有配合healthcheck,depends_on的语义只是“等容器启动”,而不是“等容器健康”。MySQL容器刚起来那几秒,端口可能已经监听,但内部InnoDB还没准备好接受连接,业务服务立刻去连,就会报Communications link failure。
正确做法是:数据库容器配好healthcheck,业务服务在depends_on里加condition: service_healthy:
services: product-service: image: product:latest depends_on: mysql: condition: service_healthy这样Compose会等待MySQL的healthcheck通过,再启动product-service。这个改动看似小,实际上解决了启动阶段大量“数据库连接被拒”的误报。
5.4 环境变量与多环境配置
同一个Compose文件,开发环境和生产环境的数据库密码、端口可能完全不同。我的做法是把所有可变配置抽成环境变量,新建.env文件:
MYSQL_PASSWORD=prod_over_9000 EXTERNAL_PORT=18080Compose文件里用${MYSQL_PASSWORD}引用。不同环境只需要准备不同的.env文件,然后执行:
docker compose --env-file .env.prod up -d这里有个安全细节:.env文件绝不能提交到代码仓库,里面是生产密码。我们的做法是在仓库里放.env.example,里面只有占位符,实际文件在部署服务器上手工创建。
6. 微服务部署实战:Spring Cloud项目从Jar包到整套环境
前面的基础概念都通了,这一节来说最实际的:一个Spring Cloud微服务项目,怎么用Docker把它完整部署起来。
6.1 服务拆分之后我们需要多少容器角色
以我们当时一个中型电商项目为例,拆成了这些角色:
- 注册中心/配置中心:用的Nacos,一个容器就能同时承担服务注册和配置管理。
- 网关:Spring Cloud Gateway,所有外部请求的入口。
- 业务服务:商品、订单、用户三个服务,各自独立镜像。
- 中间件:MySQL、Redis、RabbitMQ,分别容器化。
- 前端:Nginx容器,托管打包后的静态资源,并反向代理到网关。
一共七八个容器,用脚本管理已经开始吃力,所以直接上Compose统一定义整条链路。
6.2 多阶段构建:Dockerfile写法和Java服务优化
Java微服务的镜像常见问题是:打出来的镜像太大,甚至包含完整的Maven构建环境。我们采用多阶段构建,把编译和运行分开:
# 构建阶段 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests # 运行阶段 FROM eclipse-temurin:17-jre-alpine WORKDIR /app COPY --from=builder /build/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-Xms256m", "-Xmx512m", "-jar", "app.jar"]经验点:
mvn dependency:go-offline先把依赖下载好,这样后续只要源码没变,构建阶段会命中缓存,打包速度能快很多。- 运行阶段用
eclipse-temurin:17-jre-alpine而不是完整的openjdk:17,镜像体积能减掉一半以上。Alpine基础镜像比较小,但要注意某些需要native库的组件可能在Alpine上缺少libc兼容,遇到类似问题再换回标准的slim版本也行。 -Xms和-Xmx限制JVM堆内存,这是防止容器内存爆掉的关键一步。JVM默认按物理机内存比例设置堆大小,容器里如果不显式限制,很容易把整个宿主机内存吃光。
6.3 用Compose拉起注册中心、网关与业务服务
下面是一个简化的Compose片段,重点看服务注册与发现:
services: nacos: image: nacos/nacos-server:v2.3.2 container_name: commerce-nacos environment: MODE: standalone NACOS_AUTH_ENABLE: "false" ports: - "8848:8848" - "9848:9848" gateway: image: gateway:latest container_name: commerce-gateway environment: SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR: nacos:8848 SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR: nacos:8848 depends_on: - nacos ports: - "8080:8080" product-service: image: product:latest container_name: commerce-product environment: SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR: nacos:8848 SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR: nacos:8848 SPRING_DATASOURCE_URL: jdbc:mysql://mysql8:3306/commerce depends_on: - nacos - mysql这里最核心的改动是把Nacos地址配成nacos:8848而不是localhost:8848或某个具体的IP。因为网关和业务服务都在同一个Docker网络里,容器名就是主机名,互相访问靠名字就可以。
6.4 服务间调用为什么用容器名而不是IP
Spring Cloud微服务里,服务A调用服务B通常不直接写IP,而是通过注册中心拿实例地址。Nacos里注册的IP,是服务启动时自己上报的。容器启动时如果没配置,它会默认用容器的内网IP上报。这在同一个Compose网络内没问题,因为服务消费者只需要通过这个内网IP去访问,而同一个自定义网络内的容器互相都能通。
但要注意一个经典坑:如果容器使用host网络模式启动,或者Nacos和业务服务不在同一个网络里,业务服务注册给Nacos的IP可能是127.0.0.1或者一个不可达的IP。这时候消费者拿到这个地址去调用,必然失败。
解决方法是显式指定注册IP,在Spring Cloud的应用配置里设置:
spring: cloud: nacos: discovery: ip: 192.168.1.20在容器环境下更推荐的做法是保证所有需要互相调用的服务都在同一个自定义网络里,让Docker的DNS把容器名解析成内网IP。这样既不用关心容器实际拿到了什么IP,也不用在配置里显式写死宿主机IP,避免换服务器就配置失效。
7. 生产环境里我吃过的教训
前几节讲的都是“怎么把服务跑起来”,但真正的麻烦往往发生在跑了三个月之后。这一节把我在生产环境里踩过的坑集中说一遍,能救一个是一个。
7.1 容器不是虚拟机:处理后台进程和定时任务的姿势
第一次把服务容器化引入生产时,我们有点太激进了:把一个包含多个功能的“全家桶”服务整个塞进一个容器,里面既有业务接口(Java进程),又有定时任务(同一个Jar包的另一个启动方式),还有日志采集agent。结果就是容器内多进程管理混乱,Java进程挂了容器不退出,定时任务日志和业务日志混在一起,排查问题罪受。
Docker容器的设计哲学是单进程优先:一个容器最好只跑一个主进程。但实际微服务项目里,定时任务和业务进程往往是同一个Jar包的不同profile。这种情况下,更推荐的做法是把定时任务抽成独立服务,或者用控制脚本作为容器入口,你这个脚本负责启动Jar包并监控进程状态。如果真要在容器里跑多个进程,借助supervisord这类进程管理器来控制启动顺序和进程守护,不要裸奔。
7.2 资源限制一定要给,否则宿主机先挂
Docker默认不限制容器资源,也就是说一个容器可以无限吃宿主机内存和CPU。我们当时线上有个服务出现内存泄漏,Java进程把容器内所有内存吃完,然后开始消耗宿主机的内存和swap,最后整台服务器OOM,其他服务的容器被系统杀掉一大片。那次事故之后,我们把所有容器都加上了--memory和--cpus限制:
docker run -d --memory=1g --cpus=1 product:latestCompose里对应的写法:
services: product-service: image: product:latest deploy: resources: limits: memory: 1g cpus: "1.0"设置资源限制的好处不只是防止某容器拖垮宿主机,也是给每个服务划定明确的资源预算,部署之前就能估算服务器能承载多少服务。
7.3 日志、镜像清理、数据备份:更需要的运维习惯
容器越跑越多,日志和镜像占用的磁盘也会悄悄膨胀。Java服务默认往stdout打日志,Docker会捕获这些输出写入json文件。如果不加轮转,长期运行可能把磁盘写满。建议在daemon.json里全局配置日志轮转:
{ "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" } }镜像也是一个容易忽略的点。每次构建新版本,docker build都会留下悬空镜像(dangling image),久而久之占用大量磁盘。我建议部署脚本里带上清理指令:
docker image prune -f docker container prune -fprune -f只清理停止的容器和无用的悬空镜像,不影响正在运行的容器,可以放心加进常规部署流程。
数据库容器的备份比物理机裸装更需要注意:容器删了重建,数据不会丢,前提是你挂载了卷;但卷的损坏和误删同样会让数据消失。我们的备份策略是:宿主机上写一个cron脚本,每天用docker exec进入数据库容器执行mysqldump,把备份文件导到宿主机的备份目录,再同步到异地存储。容器化不改变数据备份的基本原则,只是执行方式从本机命令变成了docker exec。
7.4 微服务部署这条路,往前走还有哪些值得关注
Docker解决的是单个应用和单台机器的标准化部署问题,但微服务规模大了之后,你还会面对跨机器的调度、服务发现、滚动更新、自动扩缩容。我们现在的做法是Docker Compose负责单机编排,加上Nginx做反向代理和负载均衡,日常扩容还够用。等服务和机器数量继续涨,下一步就得认真考虑Kubernetes这类容器编排平台。那时候你会发现,现在学会的Docker镜像、网络、数据卷、资源限制这些基本功,全都能无缝迁移过去,没有白学。
如果有人问我学Docker最应该记住的一句话,我会说:把镜像当作不可变的交付物,把容器当作可丢弃的进程,剩下的问题都是配置问题。配置问题用Compose管起来,环境问题用镜像锁起来,这才是微服务部署的核心思路。
最后分享一个我们项目里的实际小技巧:每次发布新版本,不要直接在老的容器上docker exec改文件。正确流程是构建新镜像,然后重新docker compose up -d替换容器。镜像ID变了、容器重建了,但数据卷和数据不受影响。这套流程走了快一年,发布时出岔子的概率比之前人肉部署低了一个数量级。