Docker与Compose实战:从镜像到生产环境部署指南
2026/9/16 20:35:40 网站建设 项目流程

先交代一句:这篇文章不是那种“安装完就算完事”的教程。我写它是因为发现太多人卡在同一个地方——照着网上的命令敲完了,容器也起来了,但稍微换个环境、换个项目就一脸懵。所以这篇主要解决三件事:Docker 和 Docker Compose 到底在解决什么问题,完整的部署流程每一步在干什么,以及出问题时怎么快速定位

我自己这些年从单机部署到多服务编排,再到现在几乎每个项目都离不开 Docker,踩过的坑不算少。这篇文章里的内容,全是我亲手验证过的操作和配置,不是从文档里抄来的概念。你可以把这篇文章当成一份“抄作业指南”,照着做能跑通,做完之后你还能明白为什么要这么做。

1. 部署这件事,为什么非得用 Docker

1.1 从“在我电脑上是好的”说起

先说一个所有开发者都经历过的场景。代码本地跑得好好的,一到服务器就崩;这个服务依赖 Java 8,那个服务需要 Java 17;MySQL 版本换了一下,整个应用连不上数据库。这些问题的根源只有一个:运行环境不一致

Docker 的思路特别直接:把应用连同它需要的运行环境、依赖库、配置,一起打包成一个“集装箱”。这个集装箱在任何地方打开,里面的东西都一模一样。开发机上是这样,测试机上是这样,生产服务器上也是这样。环境差异导致的问题,从根上就被消灭了。

这不是什么高深的技术,就是一个朴素的思想——标准化。就像麦当劳的汉堡,配方固定、流程固定,在北京吃和在上海吃,味道不会有太大差别。Docker 干的就是这件事,把“怎么做汉堡”这件事标准化。

1.2 Docker Compose 解决的是什么问题

单个容器好理解,但现实中的项目很少只有一个模块。一个典型的 Web 应用,至少需要:前端 Nginx、后端应用、数据库 MySQL、缓存 Redis。如果你手动去跑四个 docker run 命令,会遇到一堆麻烦:容器之间的网络怎么打通、启动顺序怎么控制、环境变量怎么管理、数据怎么持久化。

Docker Compose 就是一个“编排工具”。你用一份 YAML 文件把所有的服务定义好,包括镜像、端口、环境变量、依赖关系、数据卷,然后一个命令全部启动。它就是那个帮你把集装箱按顺序、按规则摆放好的吊车。

更关键的是,这份 YAML 文件是文本,可以提交到 Git 里。新同事入职,拉下代码,docker compose up -d,一套环境就起来了。这就是基础设施即代码的雏形,整个部署过程变得透明、可审计、可复制。

1.3 这篇教程能带你走到哪一步

我不打算只讲理论,也不打算只丢几个命令然后说“亲测有效”。这篇教程的目标是:你从零开始,在 Windows 或 Linux 上装好 Docker,理解镜像和容器的关系,掌握常用命令,最后用 Docker Compose 把一个带 MySQL 和 Redis 的后端服务完整地跑起来。整个过程我会边操作边解释,每个步骤的作用和背后的原理都会说清楚。

学完这篇,你自己去部署 Dify、JumpServer、GitLab 这类开源项目,或者把自己的项目容器化,基本上不会再有障碍。

2. 环境准备:先把 Docker 装好

2.1 Windows 和 macOS 怎么装 Docker Desktop

Docker Desktop 是官方提供的图形化桌面工具,内置了 Docker Engine、Docker Compose、Kubernetes,装一个全都有。Windows 用户要注意,Docker Desktop 依赖 WSL 2 或 Hyper-V 虚拟化,这是大多数人安装时遇到问题的第一道坎。

安装流程分成三步。

第一步,确认虚拟化已开启。打开任务管理器,切到“性能”标签页,看“虚拟化”那一项是不是“已启用”。如果显示未启用,需要进 BIOS/UEFI 开启 Intel VT-x 或 AMD-V。不同主板的设置入口不太一样,但基本都在 Advanced 或 Configuration 菜单下,找找 Virtualization Technology、SVM Mode 这类选项就行。

提示:开启虚拟化后一定要断电重启,只按重启键有时候不生效。这个细节很多人忽略,结果装完还是报错,以为电脑不支持。

第二步,安装 WSL 2。打开 PowerShell(管理员模式),执行:

wsl --install

装完重启电脑,然后执行wsl --set-default-version 2把默认版本设为 WSL 2。这一步是有原因的:WSL 2 比 WSL 1 有更完整的 Linux 内核,Docker 跑在上面的文件读写性能要好得多。

第三步,去 Docker 官网下载 Docker Desktop 安装包,双击按提示装就行。装完启动,等右下角鲸鱼图标稳定,不再转圈,就说明引擎已经起来了。

macOS 用户更简单,下载 Docker Desktop for Mac,装完打开,输入一次密码授权安装 Docker 的辅助工具,就完事了。如果有 Apple Silicon 芯片,记得选对应 ARM 版本的安装包。

2.2 Linux 服务器怎么装 Docker Engine

服务器上一般不用 Docker Desktop,直接装 Docker Engine 加 Compose 插件就行。以 Ubuntu 为例,官方推荐用 apt 仓库安装,而不是用 curl 脚本一键装,原因是方便后续升级。

先卸载可能存在的旧版本:

sudo apt-get remove docker docker-engine docker.io containerd runc

然后安装依赖并添加官方 GPG 密钥和仓库:

sudo apt-get update sudo apt-get install ca-certificates curl gnupg lsb-release 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 引擎和 Compose 插件:

sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

装完先别急着用,执行一下验证:

sudo docker run hello-world

能打印出 Hello from Docker! 就说明整个链路是通的。

注意:默认情况下 docker 命令需要 root 权限。把当前用户加入 docker 组,可以免 sudo 使用,但这也等于把这个用户提升到接近 root 的权限。单机开发环境这么做没问题,生产服务器要慎重评估。

sudo usermod -aG docker $USER

执行完要重新登录一次才能生效。

2.3 虚拟化相关的常见报错和解决办法

先说 Windows 上最典型的报错:Docker Desktop failed to start because virtualisation support wasn't detected(或者信息里带 virtualization support not detected)。

这个报错 90% 是虚拟化没开或者没开对。排查思路按这个顺序来:

第一,确认 BIOS 里的虚拟化开了。不同品牌的主板叫法不一样,Intel 平台叫 Intel Virtualization Technology,AMD 平台叫 SVM Mode,还有的板子叫 Virtualization,开了就行。

第二,确认 WSL 2 是真装好了。在 PowerShell 里执行:

wsl --status

如果显示“默认版本:2”,基本就正常。如果显示 WSL 1,执行wsl --set-default-version 2wsl --shutdown重启 WSL。

第三,检查 Windows 功能里 Hyper-V 和“适用于 Linux 的 Windows 子系统”是否启用。控制面板 → 程序 → 启用或关闭 Windows 功能,把这两项勾上,重启电脑。

再有一种情况是 Windows 系统版本太旧。WSL 2 要求 Windows 10 版本 2004 以上,或者 Windows 11。老版本系统的处理办法不一样,但建议直接升级系统,别折腾兼容方案。

Linux 服务器上如果遇到虚拟化相关报错,多数是在虚拟机里再套虚拟机。这种情况要用 docker 的“嵌套虚拟化”模式,比较麻烦,更推荐直接换一台物理机或者用云服务器。

3. Docker 核心概念:镜像、容器、数据卷、网络

3.1 镜像和容器的关系,用面向对象来理解

“镜像”这个词很形象,但也容易让人误解。你可以把镜像理解成一个只读的模板,里面装好了操作系统的基础层、运行环境、应用代码、配置文件。它自己不运行,只是静静地躺在那里。

“容器”则是镜像的一个运行实例。镜像启动了,就变成容器。容器有完整的运行环境,有独立的文件系统,但它和宿主机共享内核。你可以在容器里装软件、写文件、跑进程,但容器一删除,这些改动就都没了。

用面向对象的思路来说,镜像是类,容器是对象。类定义了属性和方法,对象才真正占用内存、执行逻辑。你可以从同一个镜像启动无数个容器,它们之间互不干扰。

举一个实际场景:同一个 Java 后端镜像,A 容器用 8080 端口连接 MySQL,B 容器用 8081 端口,两个容器在同一台服务器上同时跑,互相看不到对方内部的文件变化。这就实现了隔离

3.2 镜像从哪来:Docker Hub 和 Dockerfile

Docker 镜像的获取方式有两种:从注册中心拉取,或者自己构建。

Docker Hub 是默认的公共镜像仓库,里面有海量的官方镜像,比如 nginx、mysql、redis、ubuntu、openjdk。拉取命令非常简单:

docker pull nginx:latest docker pull mysql:8.0 docker pull redis:7.2

注意镜像名后面的标签(tag),它代表版本。生产环境一定要指定具体版本,不要用 latest。原因很直白:latest 不是固定的,你两个月后重新拉取,得到的东西可能和当初完全不一样,这会给排查问题带来极大的不确定性。

自己构建镜像要写 Dockerfile。这就是一封“写给 Docker 的安装说明书”,告诉它要基于哪个基础镜像、拷贝哪些文件、执行什么命令、暴露哪个端口。比如用 openjdk 部署一个 Spring Boot 项目,Dockerfile 可以长这样:

FROM openjdk:17-jdk-slim WORKDIR /app COPY target/app.jar . EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]

构建命令是:

docker build -t myapp:1.0.0 .

这里-t指定镜像名和标签,最后的.指定构建上下文(当前目录)。构建好的镜像可以推到私有仓库或 Docker Hub,供其他机器拉取。

技巧:基础镜像尽量选带-slim或者-alpine后缀的精简版本。同一个功能的镜像,slim 版可能只有完整版的三分之一大小,拉取更快,占用磁盘更少,被攻击面也更小。

3.3 数据卷:容器删了,数据不能跟着没

前面提到容器删除后改动全没了。这对无状态的应用(比如 Nginx 提供静态页面)没影响,但对 MySQL、Redis、文件上传这类有状态的服务,简直就是灾难。

解决办法是数据卷(Volume)。它的本质是把宿主机上的一个目录“挂载”到容器内部。容器往挂载目录里写东西,实际上写到了宿主机;容器删了,宿主机的目录还在,新容器挂载同一个目录,数据就还在。

Docker 的-v参数有两种用法:命名卷和绑定挂载。

命名卷由 Docker 管理,数据存在 Docker 的专属目录里。命令像这样:

docker run -v mydata:/var/lib/mysql mysql:8.0

绑定挂载则直接把宿主机的一个具体路径挂进去:

docker run -v /home/user/data:/var/lib/mysql mysql:8.0

生产环境推荐使用绑定挂载,因为数据就在宿主机上一个明确的路径里,备份、迁移都直观方便。数据卷这一点在 Compose 文件里也是重头戏,后面实战部分会具体演示。

3.4 网络模式:容器之间怎么互相访问

Docker 容器的网络默认是桥接模式。每个容器内部有一个独立的 IP,宿主机可以通过映射出来的端口访问容器,容器之间可以通过容器名互相访问。

端口映射是-p参数,格式是“宿主机端口:容器端口”。比如-p 8080:80,意思是访问宿主机的 8080 端口,流量会转到容器内的 80 端口。这个映射关系在 Compose 文件里同样会用到。

容器之间访问,最好的方式是通过 Compose 创建的自定义网络。在同一个 Compose 网络里的服务,可以直接用服务名当作主机名访问。比如后端应用要连 MySQL,数据库地址写成mysql:3306而不是localhost:3306,因为对容器来说,它自己的 localhost 就是自己,不是别的容器。

我遇到过不少新手在这一步卡住:应用能启动,但报“连接数据库超时”,怎么排查都发现不了问题。十有八九就是数据库地址写成了 localhost,而实际上应该写成服务名。

4. Docker 常用命令速查:这些命令够用 90% 的场景

4.1 镜像管理三件套

镜像管理的核心操作就三个:看、拉、删。

# 查看本机所有镜像 docker images # 拉取指定镜像 docker pull nginx:1.24 # 删除镜像 docker rmi nginx:1.24

说一个常见现象:docker images里出现一堆<none>标签的镜像。这是悬空镜像,通常是因为重新构建同名镜像后,旧版本没有清理。可以用下面的命令一键清掉:

docker image prune -f

这个命令会删掉所有<none>的悬空镜像,不影响正在使用的。我习惯在每次重新构建镜像后执行一次,磁盘空间能省不少。

4.2 容器生命周期操作

容器的操作比镜像多一些,但规律性很强。以下是我每天都会用到的一套命令:

# 运行一个容器,-d 后台运行,--name 起名字,-p 映射端口 docker run -d --name mynginx -p 8080:80 nginx:1.24 # 查看正在运行的容器 docker ps # 查看所有容器(包括停止的) docker ps -a # 停止容器 docker stop mynginx # 启动已停止的容器 docker start mynginx # 重启容器 docker restart mynginx # 进入容器内部,用 bash 交互 docker exec -it mynginx bash # 查看容器日志,-f 是持续跟踪 docker logs -f mynginx # 删除容器,-f 强制删除正在运行的容器 docker rm -f mynginx

这里面docker exec -it是排查问题的利器。容器起不来了,或者起来了但报错,进去看一眼日志、检查一下配置,比盲猜高效得多。有些精简镜像里没有 bash,只有 sh,可以把 bash 换成 sh。

提示:容器被删了但镜像还在,数据卷还在,但是容器内的文件没了。所以一定要把需要保留的数据挂载到宿主机,养成从第一天就做好数据持久化的习惯。

4.3 日志、资源、系统信息

部署完项目,最常用的可能就是看日志和看资源占用。

# 动态查看实时日志 docker logs -f <容器名> # 查看最近 100 行日志 docker logs --tail 100 <容器名> # 查看容器的资源占用(CPU、内存、网络) docker stats # 查看容器详细信息(包括 IP、网络、挂载) docker inspect <容器名> # 查看 Docker 系统占用的磁盘空间 docker system df

docker system df这个命令容易被忽略,但它能直接告诉你镜像、容器、数据卷、构建缓存分别占了多少空间。磁盘满了不知道删什么的时候,先跑这个。

4.4 一键清理:让服务器“重获新生”

有一个命令我强烈建议你在掌握之前不要随便用,但掌握之后会爱不释手:

docker system prune -a

它会删掉所有未被使用的镜像、停止的容器、未用的网络,以及构建缓存。执行完,Docker 的磁盘占用通常能掉一大半。

注意-a参数的含义是“删除所有未被容器引用的镜像”,意味着你本地如果有一些旧版本镜像,也会被一起清掉。在测试环境没问题,生产环境删之前确认一下没有特殊需求。

5. 实战:用 Docker Compose 部署一套完整的 MySQL + Redis + Spring Boot 应用

5.1 写一个基础的 docker-compose.yml

Compose 文件的默认名字是docker-compose.yml,新版也支持compose.yaml。我个人习惯用docker-compose.yml,兼容性最好。一个最小可用的组成结构如下:

version: "3.8" services: mysql: image: mysql:8.0 container_name: myapp-mysql restart: always environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: myapp ports: - "3306:3306" volumes: - ./data/mysql:/var/lib/mysql redis: image: redis:7.2 container_name: myapp-redis restart: always ports: - "6379:6379" volumes: - ./data/redis:/data app: image: myapp:1.0.0 container_name: myapp-backend restart: always ports: - "8080:8080" depends_on: - mysql - redis environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/myapp?useUnicode=true&characterEncoding=utf8 SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: root123456 SPRING_REDIS_HOST: redis SPRING_REDIS_PORT: 6379

几个核心点解释一下。

restart: always的意思是容器意外退出后会自动拉起。服务器重启后,Docker 服务会自动把标记了always的容器启动。这是生产环境必备配置,否则一次宿主机重启,你的所有依赖服务全得手动起。

depends_on只是控制启动顺序,不保证依赖服务“已可用”。MySQL 容器起来了,不代表 MySQL 已经可以接受连接了。所以如果你的应用启动得太快导致连不上数据库,通常的解法是在应用代码里加一个连接重试机制。

environment这一节是把环境变量传给容器。Spring Boot 应用会读取SPRING_DATASOURCE_URL这类标准环境变量,自动覆盖 application.yml 里的配置。所以说容器化部署根本不需要改动代码里的配置,只要环境变量传对就行。

启动只需要一条命令:

docker compose up -d

-d是后台运行。执行完docker compose ps可以看所有服务的状态。

5.2 MySQL 生产环境部署的关键参数

如果只是本地测试,上面的 MySQL 配置够用了。但要放到生产环境,至少还要注意几件事。

第一,MySQL 8.0 默认的认证插件是caching_sha2_password,一些老客户端连接会报错。在 Compose 里可以加一条命令参数来兼容:

mysql: image: mysql:8.0 command: - --default-authentication-plugin=mysql_native_password

第二,时区问题。MySQL 默认时区是 UTC,如果你的应用和数据库交互涉及时间字段,务必在连接串里加上serverTimezone=Asia/Shanghai,否则查询出来的时间会差 8 个小时。

第三,字符集问题。 在 MySQL 8.0 中默认已经是 utf8mb4,不需要额外配置。但你的应用在建表时如果没有显式指定字符集,最好在连接串里加上characterEncoding=utf8

第四,生产环境不要用 root 用户跑业务。应该单独创建一个应用用户,只授权业务库的权限。虽然这在 Compose 里配置起来稍麻烦,但安全边际会高很多。

5.3 Redis 的持久化配置

Redis 默认是没有持久化的,数据全在内存里,重启就没了。生产环境几乎都要开启 AOF 或 RDB 持久化。

在 Compose 里挂载配置文件的写法是:

redis: image: redis:7.2 container_name: myapp-redis restart: always command: redis-server /usr/local/etc/redis/redis.conf ports: - "6379:6379" volumes: - ./config/redis/redis.conf:/usr/local/etc/redis/redis.conf - ./data/redis:/data

redis.conf 里把appendonly yes打开,数据写入会追加到文件里,就算容器重启,数据也能恢复。这也是为什么要在宿主机上挂一个./data/redis目录,AOF 文件就写在那里。

经验:Redis 的数据目录权限容易出问题。如果容器日志报“Can't open the append-only file: Permission denied”,多半是宿主机的挂载目录权限不对。在宿主机执行chown -R 999:999 ./data/redis通常能解决,因为 Redis 容器内默认以 uid 999 运行。

5.4 一个命令启停整个项目

Compose 最有价值的地方在于,整组服务可以用一条命令管理。

# 后台启动所有服务 docker compose up -d # 查看所有服务状态 docker compose ps # 查看所有服务日志 docker compose logs -f # 停止所有服务(容器不会被删除) docker compose stop # 停止并删除容器、网络 docker compose down # 停止并删除容器、网络,同时删除数据卷 docker compose down -v

docker compose down -v里的-v一定要慎用。它会把数据卷也删了。如果有一次你执行过这个命令,并且数据没有备份,MySQL 里的数据就彻底没了。每次执行这个命令之前,都必须确认数据已经同步或备份完。

5.5 部署后的验证步骤

部署完不能直接收工,至少要把以下四步走一遍,才算真正完成:

第一步,看所有容器的状态。

docker compose ps

正常情况下所有服务应该都是Uprunning。如果有Restarting,说明容器在崩溃重启循环,马上往下看日志。

第二步,看应用日志里有没有异常。

docker compose logs app

重点是看应用是否成功连上 MySQL 和 Redis。如果有连接失败,先确认应用里的数据库地址是不是写成了mysql、Redis 地址是不是redis

第三步,从宿主机访问一次接口。

curl http://localhost:8080/api/health

这一步验证端口映射是否生效,网络链路是否打通。

第四步,重启一次整个容器组,确认数据还在。

docker compose restart

重启后再查一次数据库数据是否还在。如果数据丢了,说明数据卷挂载没生效,这是继续使用之前必须修复的大坑。

6. 实战进阶:给后端服务构建镜像

6.1 用 IDEA 构建 Docker 镜像

我在实际项目里经常遇到开发人员问:Java 项目的 Docker 镜像到底哪里来?其实最简单的流程就是本地打 jar 包,然后通过 Dockerfile 构建镜像。如果你用的是 IntelliJ IDEA,装一个 Docker 插件,在 Dockerfile 上右键就能直接构建镜像。就前端而言,Node 项目也有类似的流程:本地 build 出静态文件,再放到 Nginx 镜像里跑。

先看一个更精细的 Java Dockerfile:

FROM maven:3.8-openjdk-17 AS build WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests FROM openjdk:17-jdk-slim WORKDIR /app COPY --from=build /build/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]

这里用了多阶段构建。第一阶段用 Maven 镜像把源码编译成 jar 包,第二阶段只拿 jar 包做一个精简运行镜像。这样做的好处是,最终镜像里只有运行时需要的 Java 环境,没有几百兆的 Maven 依赖,镜像体积从 700MB 直接变成 300MB 不到。

注意:多阶段构建第一阶段里的RUN mvn dependency:go-offline是把依赖先下载好形成一个缓存层。以后只要不改 pom.xml,重新构建时这层会被 Docker 缓存直接命中,速度飞快。

第一行FROM maven:3.8-openjdk-17 AS buildAS build给这个阶段起了一个名字,后面的COPY --from=build就是从那个阶段拷贝产物过来,这种写法算是多阶段构建的标准范式。

构建命令是:

docker build -t myapp:1.0.0 .

如果你不是 Java 项目,是 Node 前端项目,Dockerfile 长这个样子:

FROM node:18-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build FROM nginx:alpine COPY --from=build /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]

这个思路是一样的:构建阶段的产物只有 dist 目录,运行时只交给 Nginx 去做静态托管。

6.2 镜像构建提速的小技巧

第一次构建比较慢是正常的,但第二次如果还是很慢,就要优化构建策略了。有一条隐藏的规则是:Dockerfile 的每一行指令都会生成一个缓存层,只有这一行没变,后续才会继续走缓存

所以你要把“变化最少”的指令写在前面,“变化最多”的放在后面。比如 pom.xml 一般很少变,先 COPY 它,再RUN mvn dependency:go-offline,这样源码变动不会导致依赖重新下载。把COPY . .放在最后面,每次源码变了,最多只是这一层开始重建,前面的缓存全部命中。

还有一个我经常用的参数--cache-from,在某些场景可以基于远程缓存构建。但对于大多数项目,把指令顺序写对就已经能获得很好的缓存命中率了。

6.3 构建时最常见的诡异问题

我自己构建镜像时踩过一个大坑:某些依赖下载特别慢,甚至超时。解决办法是给构建过程配镜像加速器。如果你的服务器在国内,建议把 Docker Hub 的官方源换成国内镜像源。

配置位置在/etc/docker/daemon.json

{ "registry-mirrors": ["https://docker.m.daocloud.io"] }

改完重启 Docker:

sudo systemctl restart docker

之后docker pull的速度会快很多。这个配置同样适用于 Compose 拉取镜像的过程。Docker Desktop 则在 Settings → Docker Engine 里改同样的 JSON。

提示:如果docker build过程中网络一直不稳定,可以给 buildx 配置 HTTP 代理,或者直接在 Dockerfile 里用国内镜像的软件源。具体做法是RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list再加上&& apt-get update。这样至少基础的 apt 操作不会卡。

7. 生产环境:十个细节决定部署成败

7.1 安全设置:Container 权限最小化

容器并不是一个完全安全的“沙箱”。一个容器如果以 root 用户运行,一旦被攻破,攻击者可以直接操作宿主机的很多资源。所以生产环境建议给容器设置只读文件系统、禁止权限提升、非 root 用户运行。Compose 文件里对应写法是这样:

app: image: myapp:1.0.0 security_opt: - no-new-privileges:true read_only: true tmpfs: - /tmp user: "1000:1000"

read_only: true把容器的文件系统设为只读,任何写入都会被拒绝,但tmpfs: /tmp开了一个内存临时目录供运行时使用。这样就算容器被入侵,攻击者很难留下持久化的文件。

7.2 日志管理:别让日志撑爆磁盘

Docker 日志有一个很明显的特点:它会把容器写往 stdout/stderr 的全部内容存到本地文件里,而且默认是没有大小限制的。一个频繁打印日志的服务,几天时间日志文件就能撑爆磁盘,这绝不是危言耸听。

在 Compose 文件里给日志加上轮转配置:

app: image: myapp:1.0.0 logging: driver: json-file options: max-size: "10m" max-file: "3"

这样单个日志文件最大 10MB,最多保留 3 个文件。满了就丢弃最旧的日志。代价是日志只能保留比较短的时间,长期审计日志需要单独接入 ELK 或 Loki 这类集中日志平台。但起码系统不会被日志撑爆。

7.3 健康检查:容器真的“健康”吗

restart: always只能保证容器进程不退出,但如果容器内部的应用已经假死(比如线程池耗尽、连接池打满),容器本身还活着,Docker 也不会重启它。这时候就用上健康检查了。

在 Compose 里配置:

app: image: myapp:1.0.0 healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"] interval: 30s timeout: 5s retries: 3 start_period: 40s

start_period是给应用预留的启动时间。刚启动的 40 秒内的健康检查失败不计入重试次数,这个参数能避免应用启动慢被误杀。

如果你的镜像里没有 curl(精简版镜像很常见),可以考虑用wget,或者改用 Java 的角度来检查。总之健康检查的地址一定要是一个轻量接口,不要做复杂逻辑,否则检查本身就会拖垮服务。

7.4 资源限制:防止容器把宿主机吃光

默认情况下,Docker 容器能使用宿主机所有内存和 CPU。如果一个容器出现内存泄漏,整个宿主机可能因此崩溃。生产环境必须限制资源:

app: image: myapp:1.0.0 deploy: resources: limits: cpus: "1.0" memory: 1g reservations: cpus: "0.25" memory: 512M

limits是硬上限,超过会触发 OOM 被杀或者 CPU 被限制。reservations是预留的最小资源。用 Docker Compose 时,deploy.resources在非 Swarm 模式下也能生效,实测没问题。

7.5 数据备份:这是最后一条防线

不管做了多少高可用,备份永远是必须的。容器化的数据库备份其实就是备份宿主机的挂载目录。

MySQL 的简单备份,可以直接用docker exec执行 mysqldump:

docker exec myapp-mysql mysqldump -uroot -proot123456 myapp > backup_$(date +%F).sql

把它写进 crontab 每天执行一次,再配一个异地同步或者上传到对象存储,数据库的安全感就有了。

8. 常见问题排查:这些坑你一定躲不开

8.1 权限问题:Got permission denied

在 Linux 上执行docker psGot permission denied while trying to connect to the Docker daemon socket。原因基本上只有一个:当前用户不在 docker 组里。

sudo usermod -aG docker $USER

然后重新登录,让组权限生效。如果你是在脚本里用 docker 命令,记得脚本里也用sudo或者确保执行用户已经加入了 docker 组。

8.2 端口占用:Address already in use

docker run -p 3306:3306的时候报bind: address already in use,说明宿主机的 3306 端口已经被某个进程占用了。最可能的情况是宿主机自己也装了一个 MySQL,占用了同一个端口。

两种解决办法:一是停掉宿主机的服务,把端口让出来;二是改 Comose 里的端口映射,比如-p "3307:3306",宿主机用 3307,容器里依然是 3306。推荐改端口映射,服务之间互相不干扰。

排查占用的命令:

# Linux sudo lsof -i :3306 # Windows(管理员 PowerShell) netstat -ano | findstr :3306

8.3 容器一直重启:Restarting

docker ps里显示状态是Restarting,说明容器一直在重启。先看日志:

docker logs <容器名>

常见原因有几类:启动命令写错、环境变量缺失、配置文件路径不对、依赖服务没就绪。如果是应用连不上数据库,日志里通常有Connection refusedCommunications link failure。这时把depends_on改得更严格,或者在应用代码里加等待重试逻辑。

我给一个偷懒但实用的办法:在 Compose 里给应用环境变量加SPRING_DATASOURCE_HIKARI_INITIALIZATION_FAIL_TIMEOUT: 60000这类参数,让连接池的初始化失败超时时间长一点,这样数据库还在启动的过程中,应用就能熬到数据库就绪,不用频繁重启。

8.4 Docker Desktop 连不上引擎

Windows 上 Docker Desktop 启动后报failed to connect to the docker api at npipe:////./pipe/dockerdesktoplinuxen。这种情况通常是 docker 引擎没真正起来。解决方法:

先去看 Docker Desktop 的主界面,确认引擎图标是不是绿色的。如果一直在转圈,点右上角的齿轮 → Restart,重新启动。

还不行的话,在 PowerShell 里执行:

wsl --shutdown

然后重新启动 Docker Desktop。这一步是彻底重置 WSL 里的 Docker 虚拟机,很多诡异问题都能解决。

如果还是不行,检查 Windows 功能里 WSL 和虚拟机平台是否都启用了。很多时候系统更新后,这些功能会被重置。

8.5 磁盘空间占满:No space left on device

容器日志、镜像、数据卷、构建缓存,任何一个都可能撑爆磁盘。先看占用:

docker system df

然后用docker system prune -a清一遍脏数据。如果还不够,检查一下日志目录,确认 Compose 里的日志轮转配置是否生效。

一个多年来值得养成的习惯是:每周跑一次docker system df,就像每周检查邮箱一样,很多小问题在变严重之前就被发现。

9. 最后说点个人经验

我先说结论:Docker Compose 是我目前见过的“性价比”最高的部署方案。它不完美,但在绝大多数中小型项目中,它能把从前需要一周才能搞定的环境搭建压缩到半天以内,而且整个过程完全可复制、可回滚。

踩过这么多次坑后,我最大的体会是:用 Docker 不是在解决“代码跑不起来”的问题,而是在解决“环境不可控”的问题。当你开始把部署当成“写配置 + 维护配置”的过程,而不是“敲命令 + 祈祷成功”的过程时,你才算真正用好了 Docker。

给你一个特别实用的建议:每部署一个新项目,把docker-compose.yml、Dockerfile、部署步骤提交到一个专门的仓库里。下次再遇到类似场景,直接复制改改就能用。我自己的经验是,这个“部署配置仓库”比任何文档都可靠。因为代码会过时,文档会失准,但一份真实跑通的配置,永远不会骗你。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询