这个标题其实拆开看只有四个字:“云原生、Docker、使用、构建”,但组合在一起就是一套完整的现代应用交付思路。我在一线做开发和运维这么多年,最有感触的一件事就是:很多人把Docker当成一个“打包工具”来学,装个容器、跑个镜像就觉得完事了,真正到生产环境一上线,网络不通、镜像太大、启动巨慢、数据丢失,各种问题全冒出来。其实Docker真正的价值不在“容器”本身,而在于它把构建、分发、部署、运行这整条链路统一成了一致的工作流。这就是云原生落地的第一步,也是绝大多数团队从传统部署走向云原生的第一个台阶。
这篇文章我就围绕“云原生下的Docker部署、使用与构建”这条主线,把我这些年实操中验证过的方案、踩过的坑、还有那些文档里不会写明白的细节一起梳理出来。内容覆盖从基础概念到镜像构建优化,再到典型应用部署,最后是问题排查,适合正在入门容器化、准备把项目迁移到Docker、或者已经在用但总觉得“差点意思”的开发者阅读。我尽量用大白话讲清楚每一个“为什么”,而不只是告诉你“怎么敲命令”。
1. 内容整体设计与思路拆解
1.1 为什么说Docker是云原生的“地基”
云原生这个概念被各种厂商讲了太多年,听上去很玄,什么微服务、Service Mesh、不可变基础设施、声明式API,一套一套的。但剥开这些术语,落到一个开发者的日常工作上,云原生说白了就是:你的应用从开发机到生产环境,走的是一条标准化、可复现、自动化的流程。代码在本地能跑,到服务器上也必须能跑;今天部署的版本是什么,明天回滚的也必须是同一个东西。而Docker恰恰是这个链条上最底层、也最关键的一环。
为什么这么说?因为Docker把应用和它的运行环境——操作系统库、运行时、依赖、配置文件——一起打包成一个镜像。镜像就是应用的“快照”,里面不仅有自己的代码,还有代码赖以运行的一切。这就解决了传统部署里最难缠的问题:环境不一致。我见过太多这样的场景:开发说“我本地跑得好好的”,运维说“服务器上就是报错”,最后排查半天,是JDK小版本不一样、缺了个系统库、或者某个配置文件被改了。用Docker之后,这些问题被镜像固化了,开发交付的不再是一堆代码加一份部署文档,而是一个可以直接运行的标准化制品。
更重要的一点是,Docker给了你**“一次构建,处处运行”**的能力。这个能力支撑起了云原生里的两个关键实践:一个是不可变基础设施,容器一旦启动就不会在运行中被修改,出了问题直接杀掉换新的,而不是SSH进去修修补补;另一个是弹性伸缩,Kubernetes这类编排系统之所以能灵活地扩缩容,前提是每个实例都能快速、一致地启动,而镜像机制保证了这一点。所以,学云原生如果跳过Docker,就像盖楼不打地基,后面学再多调度、编排都是空中楼阁。
1.2 方案选型:虚拟机、容器与裸机该怎么选
聊Docker之前,必须先搞清楚一个问题:容器和虚拟机到底啥区别?很多人第一次接触Docker时会困惑,这玩意儿和VMware、VirtualBox有啥不一样?不都是虚拟出一个环境吗?
核心区别在虚拟化层次。虚拟机是硬件级虚拟化,Hypervisor模拟出一台完整的“假电脑”,有自己的CPU、内存、硬盘,里面装一个完整的操作系统(Guest OS),所以一台物理机只能跑有限个虚拟机,每个虚拟机光操作系统就要占几个GB的内存。容器是操作系统级虚拟化,它不虚拟硬件,而是共享宿主机的内核,只是通过namespace做资源隔离、通过cgroup做资源限制,容器里面跑的进程直接使用宿主机内核。
打个比方:虚拟机是酒店,每个房间都自带独立的卫生间、厨房、电视,什么都是独立一套,隔离性强但浪费大;容器是公寓,厨房和卫生间公用,每个房间只摆了一张床和自己的私人物品(进程、文件系统、网络),节省得多。这个差异带来的直接好处是容器启动极快,秒级甚至毫秒级,而虚拟机通常是分钟级;一台物理机可以跑几十上百个容器,密度高出很多。
那什么时候用虚拟机,什么时候用容器?我的经验是:需要强隔离、要跑异构操作系统、或者有内核定制需求时用虚拟机,比如安全要求极高的多租户场景、Windows和Linux混跑的场景。常规应用部署、微服务架构、CI/CD流水线,用容器。还有一点要注意,Docker Desktop在Mac和Windows上其实也是跑在一个轻量级Linux虚拟机里的,因为Docker本身只能跑在Linux内核上——这一点后面排查问题时要记住,很多Windows/Mac上的“诡异”问题,本质上都是在和这层虚拟机较劲。
1.3 工作流拆解:构建、分发、部署、运行
我习惯把Docker的完整工作流拆成四个环节:构建(Build)、分发(Ship)、部署(Run)、运维(Operate)。这套思路源自Docker官方的“Build, Ship, Run”理念,但实际生产里运维环节同样重要。
构建环节的核心是编写Dockerfile,把应用连同依赖一起打成一个镜像。这里面学问最大,直接决定了镜像的体积、构建速度、安全性。分发环节是把镜像推到镜像仓库,就像把代码推到GitHub,只是这里的制品是镜像。仓库可以是公有的Docker Hub,也可以是私有部署的Harbor、Nexus。部署环节是把镜像变成运行中的容器,这时代需要考虑网络、存储、资源限制、环境变量注入等问题。运维环节则是容器的生命周期管理、日志查看、监控、故障恢复。
下载安装镜像、启动容器、查看日志,这些是最基础的操作。但真正到生产环境,你会发现自己需要的是:构建优化(多阶段构建、层缓存)、基础镜像选型(slim版本、alpine版本、distroless)、安全扫描(trivy、grype)、镜像签名、运行时加固。这篇文章我就按这个工作流展开,把每个环节的关键点和经验讲透。
2. 核心基础设施:Docker的安装、镜像构建与仓库使用
2.1 安装Docker的三大平台实操要点
Docker安装本身不复杂,但三个平台踩的坑完全不一样,我分开说。
Linux(以Ubuntu/Debian系为例):最怕的是从apt源里装到老版本的docker.io。正确做法是使用Docker官方apt源:
sudo apt update sudo apt install -y 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 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 $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin装完后最容易被忽略的是把当前用户加入docker组,否则普通用户每次都要加sudo:
sudo usermod -aG docker $USER newgrp dockerWindows:最主流的方式是装Docker Desktop,但它有个硬前提——需要开启Windows的虚拟化功能(Hyper-V或WSL2)。我遇到最多的报错是“Docker Desktop failed to start because virtualisation support wasn't detected”,这通常意味着BIOS里VT-x/AMD-V没打开,或者Windows的“虚拟机平台”和“适用于Linux的Windows子系统”功能没启用。解决路径是:先进BIOS开启虚拟化,然后在“控制面板-程序-启用或关闭Windows功能”里勾选“虚拟机平台”和“适用于Linux的Windows子系统”,重启后安装Docker Desktop,设置里选择使用WSL2后端。另外,Docker Desktop也可以配置镜像源加速,但境内源的稳定性波动很大,我建议优先考虑通过自建镜像仓库或使用Docker官方源,避免依赖第三方加速器带来的不确定性。
macOS:同样用Docker Desktop,Apple Silicon的机器建议选择适配arm64的镜像,很多x86镜像虽然能靠Rosetta模拟运行,但兼容性和性能都差一些。如果你是在老款Intel Mac上跑,倒是问题不大。
提示:无论哪个平台,装完Docker之后先跑一个hello-world验证。如果拉取镜像总是超时,优先检查网络环境和镜像源配置,不要急着反复重装。
2.2 Dockerfile构建的四个核心优化点
镜像构建是Docker使用里最值得花功夫的部分。一个写得好的Dockerfile和写得烂的,构建时间可能差3倍,镜像体积差5倍以上。我先讲四个核心优化点,都是我实测下来收益最大的。
第一,依赖安装要利用构建缓存。Docker构建镜像时,每一行指令如果命中了缓存,就不会重新执行。所以Dockerfile里把变更频率最低的指令放最前面。比如Java项目,应该先拷贝pom.xml、先下载依赖,再拷贝源代码;Python项目,先拷贝requirements.txt、先pip install,再拷贝业务代码。这样你改一次代码,底层依赖层全部走缓存,构建秒级完成。
第二,用多阶段构建瘦身。这是最实用的一招,能把动辄1GB多的镜像砍到200MB以内。原理很简单:第一个阶段用完整的基础镜像(比如node:20、maven:3.9)做编译,第二个阶段只把编译产物拷贝到最精简的运行镜像里。以Java Spring Boot为例:
# 构建阶段 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests # 运行阶段 FROM eclipse-temurin:17-jre WORKDIR /app COPY --from=builder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]这段代码的意思是,编译环境里你爱装什么装什么,运行阶段只要一个JRE就能跑jar包。最终镜像体积小了,被攻击面也小了,因为里面根本没有编译器和构建工具。
第三,正确设置时区和字符集。很多人在生产环境遇到时间差8小时、中文乱码的问题,根因往往是基础镜像的时区和locale设置。Dockerfile里加两行就能解决:
ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone第四,容器内不要跑多个进程。Docker的设计哲学是一个容器一个主进程,前台进程负责接管信号、输出日志、决定容器的生死。如果你用systemd、supervisord去启动多个服务,容器的生命周期管理和日志收集都会变复杂。真要跑多服务,优先考虑拆成多个容器,用Compose或编排系统把它们串起来。
2.3 镜像仓库选择与docker compose的落地姿势
镜像构建好了,接下来就是分发。个人开发用Docker Hub就够了,企业环境我建议用Harbor或Nexus自建私有仓库。选型上,Harbor功能全,有RBAC、漏洞扫描、镜像签名、复制功能,适合做生产级制品管理;Nexus更通用,除了容器镜像还能管Maven、npm、PyPI等制品,适合已有Nexus使用习惯的团队。推送镜像的命令是:
docker tag myapp:latest registry.example.com/myapp:v1.0.0 docker push registry.example.com/myapp:v1.0.0这里有个坑:默认情况下docker push只走HTTPS,如果你自建仓库用的是HTTP协议,需要在Docker daemon配置里添加insecure-registries,否则会报证书错误。
至于编排方式,docker compose是单机多容器的利器。它可以让你用一份YAML文件定义多个服务、网络、卷、端口映射。下面是典型的Web应用+MySQL+Redis架构:
services: app: build: . ports: - "8080:8080" environment: SPRING_PROFILES_ACTIVE: prod DB_HOST: mysql REDIS_HOST: redis depends_on: mysql: condition: service_healthy redis: condition: service_healthy restart: unless-stopped mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: myapp volumes: - mysql-data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 5s timeout: 3s retries: 10 redis: image: redis:7-alpine volumes: - redis-data:/data volumes: mysql-data: redis-data:注意depends_on里我用的是condition: service_healthy,而不是简单的服务启动顺序。原因是容器“启动”不等于服务“就绪”——MySQL进程起来了不代表能接受连接,用healthcheck等健康检查通过后再启动应用,才是可靠的依赖管理方式。
3. 典型应用部署实战:从数据库到大模型
3.1 数据库容器化:MySQL与Redis的部署与数据安全
数据库容器化在推进过程中,我听到最多的顾虑是:“数据库上容器数据丢了怎么办?”我的回答是:丢数据不是因为容器化,而是因为没用对卷和持久化机制。正确部署MySQL 8.0,核心就三件事:数据目录挂载到宿主机、设置合理的密码策略、初始化时创建业务数据库。
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=你的强密码 \ -e MYSQL_DATABASE=yourdb \ -e TZ=Asia/Shanghai \ -v mysql-data:/var/lib/mysql \ -v /path/to/conf:/etc/mysql/conf.d \ --restart=unless-stopped \ mysql:8.0关键点解释一下:-v mysql-data:/var/lib/mysql是命名卷,数据实际存储在Docker管理的宿主机目录下,即使容器删了重建,数据还在。-v /path/to/conf:/etc/mysql/conf.d可以挂载自定义配置,比如设置max_connections=500、character-set-server=utf8mb4。还有一个高级选项是--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci,直接解决中文和表情包乱码问题。MySQL 8.0要注意的是默认身份认证插件是caching_sha2_password,老客户端连不上时需要在配置里指定mysql_native_password,或者升级客户端。
Redis的容器化更简单,但有个细节很容易忽略:如果不设置密码,Redis容器暴露到公网端口会被爆破,然后就被感染挖矿木马。这我已经见过不止一次了。即便是内网,我也建议至少设置--requirepass:
docker run -d \ --name redis7 \ -p 6379:6379 \ -v redis-data:/data \ -v /path/to/redis.conf:/etc/redis/redis.conf \ --restart=unless-stopped \ redis:7-alpine redis-server /etc/redis/redis.confRedis的持久化默认是RDB快照,如果你对数据丢失容忍度低,需要开启AOF,在redis.conf里设置appendonly yes。另外,给Redis内存设置上限maxmemory 512mb和淘汰策略maxmemory-policy allkeys-lru也是必做项,否则内存打爆宿主机是分分钟的事。
3.2 Spring Boot与前后端分离项目的容器化部署
Java项目的容器化,我以Spring Boot为例。现代Spring Boot项目一般用Maven或Gradle构建,最终产物是一个可执行jar包。传统的做法是先在本机或CI服务器上跑mvn package,再把jar包拷贝进基础镜像——这么做没问题,但如果你能保证构建环境一致,用多阶段构建一步到位更香,Dockerfile我在前面已经写过了。
构建完之后,启动方式有几个细节:
docker run -d \ --name springboot-app \ -p 8080:8080 \ -e SPRING_PROFILES_ACTIVE=prod \ -e JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC" \ --network myapp-network \ myapp:1.0.0这里引入JAVA_OPTS环境变量时,Dockerfile的ENTRYPOINT要配合使用shell形式,否则不生效。比如:
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]再说一下前端项目。现在主流的前端打包方式是用Node.js做构建,产出静态文件,最后由Nginx提供服务。以React/Vue项目为例:
FROM node:20-alpine AS builder WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . RUN npm run build FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80这个模式用到了前端构建里最常说的“不参与构建的东西不拷进镜像”,比如node_modules永远不要写进Dockerfile上下文。项目里的.dockerignore文件至少要忽略node_modules、dist、.git这些目录,否则构建上下文会巨大无比,COPY速度慢,缓存也会失效。
3.3 RK3588与Jetson Orin上的YOLOv8部署
标题的热搜词里出现了“RK3588部署yolov8”和“jetson orin部署deepseek本地部署”,这些都是边缘侧AI部署的常见需求。边缘设备部署和普通服务器的Docker部署有显著差异:一个是CPU架构,一个是GPU/NPU的驱动透传。
RK3588是瑞芯微的ARM平台SoC,带NPU算力。部署YOLOv8时,直接用标准x86镜像肯定跑不了,你需要针对arm64构建镜像,或者使用rknn-toolkit2把模型转成RKNN格式,再用rknn-toolkit-lite2在容器里加载推理。这里的关键是,RK3588的NPU驱动(rknpu)需要在宿主机安装,同时容器要用--privileged或设备映射的方式访问/dev/rknpu节点。如果普通容器跑不起来NPU加速版,排查首先要看是不是设备没有映射进去。
Jetson Orin是NVIDIA的嵌入式平台,同样跑arm64架构,部署YOLOv8时会依赖CUDA、TensorRT。推荐使用NVIDIA官方的nvcr.io/nvidia/l4t-*系列镜像作为基础镜像,这些镜像自带L4T(Linux for Tegra)的驱动环境。但有个坑:Jetson的JetPack版本和镜像的L4T版本必须严格对应,否则容器起来会报CUDA库找不到或内核模块不兼容的错误。启动时通常需要加--runtime=nvidia和一系列环境变量,比如--gpus all,而Jetson上是否支持这个参数取决于你用的containerd版本。
这个领域的容器化部署,我的建议是关注一个原则——异构计算设备不适合在Dockerfile里“装驱动”,GPU/NPU驱动必须在宿主机预装,容器只负责携带应用和推理框架。这是由内核驱动透传的机制决定的,强行在容器里装驱动很容易把环境搞成一团乱麻。
3.4 大模型本地部署:DeepSeek、MinerU等模型的实践
近一年本地大模型部署非常热,热搜词里的“deepseek本地部署”、“mineru本地部署”、“本地部署大模型让个人电脑智能化”都是这个趋势。容器化部署大模型的优势很明显:依赖隔离、一键迁移、多版本共存。
以DeepSeek这类开源大模型为例,主流部署方式是结合Ollama或者vLLM。Ollama的Docker部署非常简单:
docker run -d \ --name ollama \ --gpus all \ -v ollama-models:/root/.ollama \ -p 11434:11434 \ ollama/ollama然后进入容器拉取并运行模型:
docker exec -it ollama ollama run deepseek-r1:7b但有个非常关键的性能考量:CPU推理和GPU推理的速度差距是数量级的。7B级别的模型在CPU上跑可能每秒只能输出几个token,在GPU上能跑到几十甚至上百token每秒。所以如果要在个人PC上跑大模型,显卡显存至少8GB起步,推荐16GB以上。Mac用户有Apple Silicon的话,M系列芯片的统一内存也能跑,但要注意Ollama在Mac上更多是以原生应用方式运行,而不是Docker——这里面的原因是容器化会引入额外的性能损耗,对大模型这种计算密集型任务不友好。
另一个要提的是MinerU这类文档解析工具,本地部署主要是为了处理敏感文档、不把数据传到云端。这种工具的Docker部署通常也更适合直接用官方提供的镜像和compose文件。需要关注的是模型文件下载链路是否顺畅,很多模型权重托管在国外站点,部署时拉取模型可能会反复失败。这时候我的经验是用专门的模型下载工具,或者换个镜像源,而不是反复重试同一个超时的URL。
提示:类似Ollama这类工具,如果宿主机温度或内存压力持续偏高,先排查是不是同时跑了多个大模型容器。模型默认会被加载进内存,不用的模型要主动卸载,否则内存爆炸是必然的。
4. Docker使用中的常见问题与排查技巧实录
4.1 网络不通与端口映射问题排查思路
Docker网络问题是我在日常支持里遇到最多的一类。典型症状是:容器起来了,但宿主机访问不到;或者容器之间互相ping不通;又或者在容器里访问外网超时。
第一类:宿主机访问不到容器。先确认端口映射有没有暴露:docker ps,看PORTS列是否有0.0.0.0:8080->8080/tcp。如果没有,说明run的时候忘了加-p。如果端口映射存在但访问不通,先docker logs看容器里服务是不是真的监听了8080端口。有时服务默认监听的是127.0.0.1而不是0.0.0.0,那宿主机通过端口映射一样访问不了。MySQL、Spring Boot都可能遇到这种问题,排查命令是进容器执行netstat -tlnp看监听地址。
第二类:容器间互通问题。容器默认使用bridge网络,容器之间通过IP可以互通,但IP在容器重建后会变。正确的做法是使用自定义网络(docker network create mynet),这样容器之间可以通过容器名直接访问。自定义网络还提供了内置DNS解析,比如compose文件里,服务名就是主机名。我见过一个常见的坑是:写了compose,两个服务都能启动,但应用报UnknownHostException: mysql,排查下来是容器不在同一个网络里,Compose创建的默认网络没有把手工docker run的容器加进去。
第三类:容器访问外网超时。这通常是DNS问题。Docker默认的DNS是127.0.0.11(内置的DNS转发器),如果宿主机本身DNS有问题,容器里解析域名就会失败。临时解决可以在run时加--dns 8.8.8.8,但生产环境建议从宿主机DNS根源排查。另外,如果宿主机有防火墙或安全组策略,记得放行对应端口。
4.2 镜像构建失败与启动崩溃的定位技巧
镜像构建失败,最常见的报错有两类。一类是网络拉取依赖失败,比如npm ERR! network、maven package超时,这大概率不是Docker问题,而是构建环境的网络到npm、Maven中央仓库链路不稳定。解决方案是在基础镜像里配置镜像源,比如npm设置registry、Maven配置settings.xml里的mirror。另一类是构建缓存导致的“陈旧依赖”,比如Java项目改了pom.xml但maven依赖缓存没失效,或者前端项目锁定了旧版本依赖。我的经验是,遇到诡异问题时,先单独对某一层做docker build --no-cache测试,确认不是缓存问题,再逐层排查代码问题。
容器启动崩溃(比如启动后立即退出),排查分三步。第一步:docker logs 容器名看日志,这一步能解决90%的问题。第二步:如果日志没有输出就退出了,大概率是主进程没有保持前台运行——比如起了个后台进程,PID 1结束了,容器自然退出。第三步:如果容器反复重启,看是不是healthcheck失败导致Compose的重试机制在起作用,或者restart: unless-stopped和进程崩溃形成循环。我曾经排查过一个问题:容器每隔几十秒重启一次,日志全是“OutOfMemoryError”,Java堆设太大,容器内存限制太小,JVM根本没法正常启动。后来把JAVA_OPTS里的Xmx调整到容器limit的70%,问题立刻消失。
4.3 Docker Desktop与WSL2的Windows兼容问题
Windows上使用Docker Desktop,热搜词里好几个都是在问这个。我讲几个高频问题和对应的处理方式。
最经典的是“Docker Desktop failed to start because virtualisation support wasn't detected”这个报错。这意味着Docker Desktop检测不到系统的虚拟化能力。可能性依次是:BIOS里CPU虚拟化没开、Windows的Hyper-V或虚拟机平台功能未启用、WSL2未安装。处理顺序建议是:先进BIOS开启VT-x/AMD-V,然后Windows以管理员身份运行:
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart wsl --set-default-version 2重启后安装Docker Desktop。如果还是失败,再看一下是否有第三方杀毒软件或安全软件干扰了Hyper-V的启动。Windows上Docker Desktop还有一个迷惑点:容器里看到的CPU、内存和Mac一样,都是虚拟机的资源,而不是你物理机的全部资源。默认Docker Desktop只分一部分CPU和内存给VM,你需要在Settings-Resources里手动调高,否则部署Spring Boot或大模型时经常出现“明明电脑32G内存,容器里只有4G”的尴尬。
4.4 数据持久化与备份恢复的保命经验
最后这块是很多人忽略但真的能保命的环节:容器数据持久化。我有个原则:任何容器都可以随时删除重建,但数据必须留在卷或挂载目录里。如果你发现某个容器删了以后数据全没了,那说明当初没挂卷,这是Docker使用里最危险的失误。
数据备份的常规操作是对卷做备份。比如MySQL容器:
docker exec mysql8 sh -c 'exec mysqldump --all-databases -uroot -p"密码"' > /backup/all.sql定时任务可以用crontab做,每天凌晨备份一次,保留最近7天或30天的备份文件。如果集群环境更复杂,可以结合xtrabackup做物理备份。Redis的话,直接备份appendonly.aof或者dump.rdb文件即可。
恢复时要注意,MySQL导入大SQL文件会非常慢,建议在目标容器启动前就把备份文件挂载进去,或者用docker exec -i mysql8 mysql -uroot -p密码 < backup.sql的方式导入。数据无价,这一点怎么强调都不为过。我自己就因为早期“图省事”不挂卷,把一套测试环境的Prometheus数据删了个干干净净,从那以后所有有状态服务一律先挂卷再启动。这件事也成了我后来写任何部署文档都会先写存储的原因。
5. 工具链延伸:从Compose到Kubernetes的演进路径
5.1 从Docker CLI到docker compose的过渡
很多初学者会觉得docker compose是另一个工具,但其实它不是替代CLI,而是把CLI里的参数从命令行转移到了声明式配置文件。用CLI部署一个服务,参数全写在命令行里,一旦服务多了,记不清某个容器当初怎么起的;而用compose,一条docker compose up -d就能把一组服务全部拉起来,所有配置都在YAML里,可读、可控、可版本化。
我从CLI迁移到compose的经验是:不要一开始就追求复杂的compose模板,先把单个服务的run命令翻译成compose配置,跑通了再叠加网络、依赖、健康检查。比如Redis单机部署,compose文件就这么点内容:
services: redis: image: redis:7-alpine container_name: redis7 ports: - "6379:6379" volumes: - redis-data:/data command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru volumes: redis-data:这套配置可以提交到Git、可以做代码评审、可以用CI/CD自动部署。这就是基础设施即代码的思想,也是云原生的核心实践之一。等你习惯了用compose描述“应用由哪些组件构成”之后,再去看Kubernetes的YAML,会发现底层逻辑是相通的——都是声明期望状态,由系统去调和实现。
5.2 Docker与Kubernetes的边界:什么时候需要进阶
标题里没有直接提Kubernetes,但云原生方向绕不开这个话题。我的建议是分阶段演进:单机环境、一个Docker主机上跑几个容器,用compose就够了;当你的服务需要多副本、需要自动扩容、需要滚动更新不中断、需要自愈,或者有多台服务器要统一管理,才开始考虑Kubernetes。
用Kubernetes之后,Docker的角色会发生变化——你不再直接使用docker run命令,而是用kubectl管理Pod,Pod里的容器由Kubelet调用容器运行时(containerd、CRI-O等)拉起。但镜像构建和分发的工作流完全保留:你依然需要写Dockerfile、构建镜像、推到仓库,然后Kubernetes从仓库拉取镜像创建Pod。所以,Docker的知识一点都不会白学,它只是从“直接使用”变成了“底层支撑”。
这个阶段需要补充的知识也很明确:Pod和Deployment的差异(Pod是实例,Deployment管理副本和滚动更新)、Service和Ingress(四层和七层负载均衡)、ConfigMap和Secret(配置和密钥管理)、PersistentVolume(与docker volume对应的持久化抽象)、HPA(自动扩缩容)。建议的学习路径是:先把每门技术用明白,再去做抽象——先在单机上用compose部署一套真实应用,比如Grafana+Prometheus+MySQL+Spring Boot,再把同样的应用迁移到Kubernetes,体会编排系统带来的能力差异。我只在直接用单节点Docker部署已经无法满足需求时,才把它搬到Kubernetes上,这个阶段的判断不能反过来。
5.3 常见问题速查表
整理一个我的高频排查速查表,方便遇到问题时快速定位:
| 症状 | 大概率原因 | 首选排查命令/操作 |
|---|---|---|
| 容器启动后立刻退出 | 主进程未前台运行 | docker logs 容器名 |
| 宿主机访问不了容器端口 | 端口映射缺失或服务监听127.0.0.1 | docker ps,netstat -tlnp |
| 容器间访问不到服务名 | 不在同一自定义网络 | docker network inspect |
| 镜像构建慢 | 基础镜像层缓存失效、依赖源慢 | 调整Dockerfile指令顺序、配置镜像源 |
| 时区差8小时 | 基础镜像默认UTC | ENV TZ=Asia/Shanghai |
| 中文/表情符号乱码 | 字符集不是utf8mb4 | MySQL配置character-set-server=utf8mb4 |
| JVM OOM崩溃 | 容器内存限制小于JVM堆配置 | 调整-Xmx为容器limit的70% |
| 日志找不到 | 容器内日志输出到文件而非stdout | 修改应用配置,输出到stdout |
| 镜像体积巨大 | 未用多阶段构建 | 重构Dockerfile,运行阶段只拷贝产物 |
| 数据丢失 | 未挂载卷 | --volumes-from或-v指定目录,不可恢复时寻求备份 |
5.4 日志与监控的容器化管理
最后聊一个容易被新手忽略、但生产环境必做的事情:容器日志和监控。Docker容器默认把日志输出到stdout和stderr,docker logs命令能查看,但容器一旦重建、日志文件轮转之后,历史日志就不好查了。生产环境建议配置Docker的json-file日志驱动,并且限制单个日志文件大小,避免磁盘被撑爆:
{ "log-driver": "json-file", "log-opts": { "max-size": "20m", "max-file": "5" } }在/etc/docker/daemon.json里配置完后重启Docker生效。更完善的方案是把日志收集到集中式平台,比如用Filebeat或Fluentd把容器日志转发到Elasticsearch或Loki,再用Kibana或Grafana展示。监控方面,cAdvisor可以采集容器级CPU、内存、网络、磁盘指标,配合Prometheus存储、Grafana展示,这是一套我用了很久的标准组合。部署方式也很贴合本篇文章的主题——用docker compose一键拉起:
services: prometheus: image: prom/prometheus ports: - "9090:9090" volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prom-data:/prometheus grafana: image: grafana/grafana ports: - "3000:3000" environment: - GF_SECURITY_ADMIN_PASSWORD=admin volumes: - grafana-data:/var/lib/grafana cadvisor: image: gcr.io/cadvisor/cadvisor:latest ports: - "8080:8080" volumes: - /:/rootfs:ro - /var/run:/var/run:ro - /sys:/sys:ro - /var/lib/docker/:/var/lib/docker:ro - /dev/disk/:/dev/disk:ro这套系统搭建起来之后,你会对“容器是怎么被管理起来的”有直观认知。监控不是为了好看,而是为了在故障发生时能快速定位“是容器本身的问题还是宿主机资源的问题”,这种能力在多人协作、业务复杂的环境里极其重要。
我个人在实际操作中的体会是,Docker入门容易,但真正用得顺手、用得安全,拼的是细节的积累。我见过有人把生产环境数据库跑在容器里不挂卷的,见过有人把镜像搞得比操作系统还大的,也见过有人为了图省事把所有容器都加了privileged最后被入侵的。每一个坑的背后都是对设计哲学的不理解:镜像要小、进程要单一、数据要持久、权限要最小。你把这几条原则刻在脑子里,再去看任何Docker相关的项目,思路都会清明很多。
最后再分享一个我一直在用的小技巧:每写完一份Dockerfile或者compose文件,都用docker compose config验证一遍语法,这个命令会解析并输出最终的配置,很多缩进错误和参数拼写错误都能当场暴露。另外建议给自己做一个“部署清单”,包含卷、端口、环境变量、健康检查、数据备份五项,每次上线前逐项核对。我的经验是,这些看似琐碎的习惯,到最后比任何高深的技术都更能保护你的生产环境稳定运行。