1. 为什么现在还要认真聊Docker
1.1 从一个真实场景说起
前阵子帮一个朋友处理他团队里的部署问题。他们做的是一个内部用的数据看板系统,前端用某个主流框架,后端是Python写的接口服务,数据库用的PostgreSQL。开发阶段一切正常,等到要往服务器上部署的时候,问题就来了:开发同学本地是macOS,测试服务器是Ubuntu 20.04,生产环境又是另一个版本的Linux。同一份代码,在三个环境里跑出了三种不同的结果——本地好好的,测试环境缺个系统库,生产环境又因为Python版本差异导致某个依赖装不上。
这种场景我相信做过部署的人都遇到过。问题的根源不在于代码写得对不对,而在于运行环境本身没有被标准化。代码是标准化的,但代码所依赖的操作系统、系统库、运行时版本、环境变量、配置文件路径,这些东西全都是"手工打造"的,每换一个环境就要重新来一遍。
Docker要解决的核心问题就是这个。它把应用和它所需要的运行环境一起打包成一个标准化的单元,这个单元在任何支持Docker的机器上都能以完全相同的方式运行。你可以把它理解成给应用做了一个"环境快照",连同操作系统层面的依赖一起冻结进去。
1.2 Docker到底是什么,用一句话讲清楚
如果只用一句话概括:Docker是一个轻量级的容器化平台,它让应用的打包、分发和运行变得标准化。
这里有几个关键词需要拆开说。
容器化——容器不是虚拟机。虚拟机是在物理硬件上模拟出一整套操作系统,每个虚拟机里跑一个完整的OS内核,重量级、启动慢、资源占用高。容器则是共享宿主机的操作系统内核,只在上面做进程级别的隔离。一个容器本质上就是宿主机上的一个特殊进程,只不过这个进程看到的文件系统、网络、进程空间都是被隔离过的。所以容器启动是秒级的,资源开销也小得多。
标准化——Docker定义了一套镜像格式(OCI标准),任何符合这个格式的镜像,在任何安装了Docker的机器上都能跑。这就好比集装箱:不管里面装的是什么货物,港口、货轮、卡车都只需要按照统一的标准来搬运,不需要关心里面装的是衣服还是电子产品。
打包、分发、运行——这三个动作对应Docker的三个核心概念:镜像(Image)是打包的产物,仓库(Registry)是分发的媒介,容器(Container)是运行的实例。这三个概念贯穿Docker使用的全过程,后面会反复提到。
1.3 谁适合看这篇内容
这篇内容面向的是那些"听说过Docker但一直没真正用起来"的人,或者"用过几个命令但不太清楚背后原理"的人。具体来说:
- 后端开发同学,需要把自己的服务部署到服务器上,但被环境问题反复折磨的;
- 运维或DevOps方向的同学,需要管理多个服务的部署和编排;
- 个人开发者,想在自己的服务器上跑几个自建服务(比如博客、网盘、监控工具),希望部署过程干净可控的;
- 学生或者转行的朋友,想系统了解容器技术但被网上碎片化教程搞晕的。
如果你已经能熟练写Dockerfile、玩转Docker Compose、对镜像分层和网络模式了如指掌,那这篇内容对你来说可能偏基础。但即便如此,里面关于实操细节和踩坑经验的部分,或许还是能给你一些参考。
接下来的内容会按照"概念理解→环境安装→镜像操作→容器管理→应用部署实战→问题排查"这条线来展开,每一步都会给出可以直接复制执行的命令和配置,同时解释清楚每个操作背后的逻辑。
2. 核心概念拆解:镜像、容器、仓库到底是什么关系
2.1 镜像:一个只读的模板
镜像(Image)是Docker世界里的"模板"。你可以把它理解成一个压缩包,里面包含了运行某个应用所需的一切:一个精简的操作系统文件系统、运行时环境(比如Python、Node.js、Java)、应用代码本身、以及一些配置信息。
但镜像和普通压缩包有个关键区别:镜像是分层的。
一个镜像由多个只读层(Layer)叠加而成。每一层代表一次文件系统变更。比如你从一个基础镜像(比如某个Linux发行版的最小系统)开始,装了一个Python运行时,这就产生了一层;然后复制了你的应用代码进去,又产生了一层;最后设置了一个启动命令,这算是一层元数据。
分层的意义在于复用和缓存。如果你有十个应用都基于同一个Python基础镜像,那这个基础镜像在磁盘上只存一份,十个应用共享它。构建镜像的时候,如果某一层没有变化,Docker会直接使用缓存,不需要重新构建。这就是为什么Docker构建镜像的速度通常比虚拟机制作模板快得多。
查看镜像分层的命令很简单:
docker image history <镜像名>这个命令会列出镜像的每一层,包括每层的大小和创建命令。实际用的时候你会发现,有些层很大(比如装系统库的那层),有些层很小(比如复制几个配置文件)。理解分层对于优化镜像体积非常关键,后面会专门讲。
2.2 容器:镜像的运行实例
容器(Container)是镜像跑起来之后的状态。如果镜像是一个可执行程序的"安装包",那容器就是这个程序运行时的"进程"。
容器和镜像的关系,类似于面向对象编程里"类"和"实例"的关系。一个镜像可以启动多个容器,每个容器都是独立的,互相不影响。你可以基于同一个Nginx镜像启动三个容器,分别监听不同的端口,提供不同的网站内容。
容器在镜像的只读层之上,加了一个可写的容器层。所有对容器的修改(比如写日志、改配置文件)都发生在这个可写层里。当容器被删除时,这个可写层也随之消失。这就是为什么容器里的数据默认是不持久的——要持久化数据,必须用数据卷(Volume)或者绑定挂载(Bind Mount),这个后面会详细讲。
容器的生命周期有几个关键状态:
| 状态 | 含义 | 对应操作 |
|---|---|---|
| Created | 容器已创建但未启动 | docker create |
| Running | 容器正在运行 | docker start/docker run |
| Paused | 容器被暂停 | docker pause |
| Stopped | 容器已停止 | docker stop |
| Exited | 容器运行结束退出 | 进程自然结束或docker stop |
实际使用中最常见的是Running和Exited两个状态。一个容易混淆的点是:docker stop和docker kill的区别。stop会先给容器里的主进程发送SIGTERM信号,等它优雅退出(默认等10秒),超时才发SIGKILL强制杀掉;kill则直接发SIGKILL,不给进程清理的机会。生产环境里应该优先用stop,除非进程已经卡死。
2.3 仓库:镜像的分发中心
仓库(Registry)是存放和分发镜像的地方。最常用的是Docker Hub,上面有大量官方和社区维护的镜像。但出于网络和安全的考虑,很多团队会搭建自己的私有仓库。
仓库里存放镜像的格式是<仓库地址>/<命名空间>/<镜像名>:<标签>。比如docker.io/library/nginx:latest表示Docker Hub官方库里的Nginx镜像,标签是latest。如果省略仓库地址,默认就是Docker Hub;如果省略标签,默认是latest。
这里有个实操中很容易踩的坑:latest标签并不等于"最新稳定版"。它只是一个默认标签,很多镜像的latest可能指向一个不稳定的开发版本。生产环境应该始终使用明确的版本标签,比如nginx:1.25.3而不是nginx:latest。这样能保证每次拉取到的镜像是一致的,不会因为上游更新导致行为变化。
镜像的拉取和推送命令:
# 从仓库拉取镜像 docker pull nginx:1.25.3 # 给本地镜像打标签(准备推送到自己的仓库) docker tag nginx:1.25.3 myregistry.example.com/myteam/nginx:1.25.3 # 推送到仓库 docker push myregistry.example.com/myteam/nginx:1.25.32.4 三个概念之间的关系梳理
把这三个概念串起来看:你从仓库拉取(或自己构建)一个镜像,基于这个镜像启动一个或多个容器,容器运行产生数据,数据通过数据卷持久化到宿主机上。镜像本身是只读的、不可变的;容器是可变的、有生命周期的;仓库是镜像的存储和分发中心。
理解这个关系模型之后,后面所有的操作都会变得有章可循。比如当你遇到"容器删了数据就没了"的问题时,自然就会想到应该用数据卷;当你遇到"每次构建镜像都很慢"的问题时,自然就会想到利用分层缓存。
3. 安装Docker:不同系统的实操方案
3.1 Linux上的安装(以Ubuntu为例)
Linux是Docker的原生平台,安装最直接。但不建议直接用apt install docker.io这种方式装,因为发行版仓库里的Docker版本往往比较旧。推荐用Docker官方提供的安装脚本或仓库。
先卸载可能存在的旧版本:
sudo apt-get remove docker docker-engine docker.io containerd runc然后安装依赖并添加官方仓库:
sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg lsb-release # 添加Docker的官方GPG密钥 sudo mkdir -p /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 > /dev/null安装Docker引擎:
sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin安装完成后验证:
sudo docker run hello-world如果看到"Hello from Docker!"的输出,说明安装成功。
这里有个实操建议:把当前用户加入docker组,这样就不用每次敲docker命令都加sudo了。
sudo usermod -aG docker $USER # 需要重新登录才能生效但要注意,加入docker组等同于给了这个用户root权限(因为容器可以挂载宿主机任意目录),所以在多用户的生产服务器上要谨慎。个人开发机或者单用户服务器上问题不大。
3.2 国内环境下的镜像加速配置
在国内网络环境下,直接从Docker Hub拉取镜像可能会很慢甚至超时。配置镜像加速器是必须的一步。
编辑或创建/etc/docker/daemon.json:
{ "registry-mirrors": [ "https://mirror.example1.com", "https://mirror.example2.com" ], "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" } }这里我顺便加了日志配置,限制每个容器的日志文件最大100MB,最多保留3个。这个配置非常重要,因为默认情况下Docker容器的日志会无限增长,跑久了能把磁盘撑满。我见过好几次服务器磁盘告警,最后查出来是某个容器的日志文件涨到了几十GB。
改完配置后重启Docker:
sudo systemctl daemon-reload sudo systemctl restart docker验证加速器是否生效:
docker info | grep -A 5 "Registry Mirrors"3.3 安装后的基础配置检查
安装完成后,建议做几项基础检查,确保环境是健康的。
检查Docker版本和存储驱动:
docker version docker info | grep "Storage Driver"存储驱动推荐用overlay2,这是目前最稳定、性能最好的选择。如果显示的是vfs或者devicemapper,建议检查一下系统内核版本和文件系统类型。
检查Docker服务状态:
sudo systemctl status docker设置开机自启:
sudo systemctl enable docker还有一个容易被忽略的点:Docker的数据目录默认在/var/lib/docker。如果系统盘空间不大,建议把数据目录迁移到更大的数据盘上。方法是修改/etc/docker/daemon.json,加上"data-root": "/data/docker",然后把原目录的数据迁移过去,再重启Docker。
3.4 非Linux环境下的说明
在macOS和Windows上,Docker实际上是运行在一个轻量级虚拟机里的(因为这两个系统的内核和Linux不同,无法直接运行Linux容器)。官方提供了Docker Desktop这个工具来管理这个虚拟机,安装过程比较简单,下载安装包一路下一步即可。
需要注意的是,Docker Desktop在资源占用上比Linux原生环境要高一些,因为多了一层虚拟机。在macOS上,文件挂载的性能也不如Linux原生环境,如果项目涉及大量文件读写(比如大型代码仓库的挂载),可能会感觉到明显的性能差异。
对于Windows用户,如果用的是较新版本的Windows,建议启用WSL2后端,性能比传统的Hyper-V后端好很多。安装Docker Desktop时会自动检测并提示。
4. 镜像操作:从拉取到构建的完整流程
4.1 镜像的获取与查看
获取镜像有两种方式:从仓库拉取,或者自己构建。先看拉取。
# 拉取指定版本的镜像 docker pull nginx:1.25.3 # 拉取所有标签(不推荐,很占空间) docker pull --all-tags nginx拉取完成后,查看本地镜像列表:
docker images输出会显示仓库名、标签、镜像ID、创建时间和大小。这里有个细节:镜像ID是SHA256哈希值的前12位,它是镜像内容的唯一标识。即使两个镜像的仓库名和标签不同,只要内容一样,它们的镜像ID就是相同的。
查看镜像的详细信息:
docker inspect nginx:1.25.3这个命令返回一个JSON格式的详细信息,包括镜像的架构、操作系统、环境变量、启动命令、挂载点、网络配置等。信息量很大,实际使用中通常配合grep或jq来提取需要的字段。比如查看镜像的默认环境变量:
docker inspect nginx:1.25.3 | jq '.[0].Config.Env'4.2 编写Dockerfile的核心要点
自己构建镜像需要写Dockerfile。Dockerfile是一个文本文件,里面是一系列构建指令。每条指令都会在镜像里产生一层。
一个典型的Python应用Dockerfile:
# 指定基础镜像,明确版本号 FROM python:3.11-slim # 设置工作目录 WORKDIR /app # 先复制依赖文件,再安装依赖 # 这样只要依赖不变,这一层就能复用缓存 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 再复制应用代码 COPY . . # 设置环境变量 ENV PYTHONUNBUFFERED=1 # 声明容器运行时监听的端口(仅作文档用途) EXPOSE 8000 # 指定启动命令 CMD ["python", "app.py"]这个Dockerfile里有几个关键设计点值得展开说。
为什么先复制requirements.txt再复制代码?这是为了利用Docker的分层缓存机制。Docker在构建镜像时,如果某一层的指令和上下文没有变化,就会直接使用缓存。依赖安装通常很耗时,而代码改动很频繁。把依赖安装放在代码复制之前,只要requirements.txt没变,依赖安装这一层就能一直用缓存,构建速度会快很多。如果反过来先复制全部代码再装依赖,那每次改一行代码都会导致依赖重新安装。
为什么用--no-cache-dir?pip默认会把下载的包缓存到本地,这在容器里没有意义(因为容器是一次性的),只会白白增加镜像体积。加上这个参数可以省掉这部分空间。
为什么用python:3.11-slim而不是python:3.11?完整的python:3.11镜像基于Debian完整版,包含了很多编译工具和文档,体积可能有900MB以上。slim版本去掉了这些非必要内容,体积通常在150MB左右。如果应用不需要编译C扩展,用slim版本就够了。还有更小的alpine版本,但alpine用的是musl libc而不是glibc,某些Python包在alpine上安装会出问题,需要额外处理,新手不建议一上来就用alpine。
4.3 构建镜像与优化体积
构建命令:
docker build -t myapp:1.0.0 .-t指定镜像名和标签,最后的.表示构建上下文是当前目录。构建上下文的意思是:Docker会把当前目录下的所有文件发送给Docker守护进程,然后根据Dockerfile的指令来构建。所以如果当前目录下有大文件(比如数据集、日志),构建会变得很慢。解决办法是写一个.dockerignore文件,排除不需要的文件:
.git __pycache__ *.pyc *.log data/ node_modules/这个文件和.gitignore的语法一样。养成写.dockerignore的习惯,能显著加快构建速度。
关于镜像体积优化,除了选对基础镜像和清理缓存之外,还有一个重要技巧:合并RUN指令。比如:
# 不推荐:产生三层,中间层的文件虽然被删了但还占空间 RUN apt-get update RUN apt-get install -y curl RUN rm -rf /var/lib/apt/lists/* # 推荐:一层完成,清理操作在同一层内生效 RUN apt-get update && \ apt-get install -y curl && \ rm -rf /var/lib/apt/lists/*原理是:镜像的每一层都是只读的,你在后一层删除前一层的文件,只是标记了"这个文件被删了",但前一层的文件数据仍然存在于镜像中。只有把安装和清理放在同一层里,才能真正减小体积。
4.4 镜像的标签管理与清理
给镜像打标签:
docker tag myapp:1.0.0 myregistry.example.com/myteam/myapp:1.0.0 docker tag myapp:1.0.0 myregistry.example.com/myteam/myapp:latest一个镜像可以有多个标签,它们指向同一个镜像ID。推送时通常同时推版本标签和latest标签,方便使用。
清理无用镜像:
# 删除指定镜像 docker rmi myapp:0.9.0 # 删除所有未被使用的镜像(悬空镜像) docker image prune # 删除所有未被容器使用的镜像 docker image prune -adocker image prune -a要慎用,它会删掉所有没有被运行中容器使用的镜像。如果你有一些停止状态的容器依赖某个镜像,那个镜像不会被删;但如果你删了容器,镜像就可能被清理掉,下次要用又得重新拉取。
查看镜像占用空间:
docker system df这个命令会显示镜像、容器、数据卷、构建缓存各自占用的空间,以及可回收的空间。磁盘紧张的时候,这个命令能帮你快速定位空间去哪了。
5. 容器管理:日常操作与数据持久化
5.1 容器的启动、停止与进入
启动一个容器最常用的命令是docker run:
docker run -d \ --name my-nginx \ -p 8080:80 \ -v /data/nginx/html:/usr/share/nginx/html:ro \ --restart unless-stopped \ nginx:1.25.3这条命令拆开看:
-d:后台运行(detach模式),不加这个参数容器会在前台运行,占住终端;--name my-nginx:给容器起个名字,方便后续操作。不指定的话Docker会随机生成一个名字;-p 8080:80:端口映射,把宿主机的8080端口映射到容器的80端口。格式是宿主机端口:容器端口;-v /data/nginx/html:/usr/share/nginx/html:ro:挂载宿主机目录到容器内,:ro表示只读;--restart unless-stopped:重启策略,表示除非手动停止,否则容器退出时自动重启。这个策略对生产环境很重要,服务器重启后容器能自动恢复;- 最后的
nginx:1.25.3是镜像名。
进入正在运行的容器:
docker exec -it my-nginx /bin/bash-it是两个参数的组合:-i保持标准输入打开,-t分配一个伪终端。合起来就是给你一个交互式的shell。如果容器里没有bash(比如alpine镜像),可以用/bin/sh。
停止和删除容器:
docker stop my-nginx docker rm my-nginxdocker stop会先发SIGTERM,等10秒后如果进程还没退出就发SIGKILL。如果确定进程能立即退出,可以用docker stop -t 0 my-nginx把等待时间设为0。
5.2 数据持久化的两种方式
容器里的数据默认不持久,容器一删数据就没了。持久化有两种方式:绑定挂载(Bind Mount)和数据卷(Volume)。
绑定挂载是把宿主机的某个目录直接挂到容器里。上面的例子-v /data/nginx/html:/usr/share/nginx/html:ro就是绑定挂载。优点是直观,你在宿主机上能直接看到和修改文件。缺点是依赖宿主机的目录结构,可移植性差,而且权限问题比较麻烦(容器里的用户ID和宿主机的用户ID可能对不上)。
数据卷是Docker管理的存储区域,默认在/var/lib/docker/volumes/下。创建和使用:
# 创建数据卷 docker volume create mydata # 使用数据卷 docker run -d --name mydb -v mydata:/var/lib/postgresql/data postgres:16数据卷的优点是Docker统一管理,可移植性好,备份和迁移方便。缺点是宿主机上不能直接看到文件(需要先找到卷的实际路径)。
两种方式怎么选?我的经验是:配置文件、静态资源用绑定挂载,数据库数据、应用产生的持久数据用数据卷。配置文件用绑定挂载方便修改,改完重启容器就生效;数据库数据用数据卷,因为数据库对文件权限和性能有要求,数据卷的管理更规范。
查看和清理数据卷:
docker volume ls docker volume inspect mydata docker volume prunedocker volume prune会删除所有没有被容器使用的数据卷。这个命令要特别小心,因为删掉的数据卷里的数据是找不回来的。执行前一定要确认没有重要数据。
5.3 容器网络模式解析
Docker容器默认使用bridge网络模式。每个容器分配一个独立的IP,容器之间可以通过IP互相访问,但默认不能通过容器名访问。要让容器之间通过名字互相访问,需要创建自定义网络:
docker network create mynet docker run -d --name app --network mynet myapp:1.0.0 docker run -d --name db --network mynet postgres:16这样app容器就可以通过db这个主机名访问数据库容器了。自定义网络内置了DNS解析,这是它和默认bridge网络最大的区别。
除了bridge,还有几种网络模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| bridge | 默认模式,容器有独立IP | 大多数场景 |
| host | 容器直接使用宿主机网络 | 对网络性能要求极高的场景 |
| none | 容器没有网络 | 只需要计算不需要网络的场景 |
| container | 共享另一个容器的网络 | 需要共享localhost的场景 |
host模式值得多说一句。用host模式时,容器不会分配独立IP,而是直接使用宿主机的网络栈。这意味着容器里监听80端口,宿主机上就是80端口,不需要端口映射。性能上少了一层NAT转换,会好一些。但缺点是端口会冲突,同一台机器上只能跑一个用host模式的80端口服务。
5.4 容器资源限制与监控
生产环境必须给容器设置资源限制,否则一个容器出问题可能拖垮整台机器。
docker run -d \ --name myapp \ --memory 512m \ --cpus 1.5 \ --memory-swap 1g \ myapp:1.0.0--memory 512m:限制容器最多使用512MB内存;--cpus 1.5:限制容器最多使用1.5个CPU核心的算力;--memory-swap 1g:内存+交换分区总共1GB,意味着交换分区可以用512MB。
查看容器资源使用情况:
docker stats这个命令会实时显示所有运行中容器的CPU、内存、网络、磁盘IO使用情况。排查性能问题时非常有用。
还有一个容易忽略的点:容器的OOM Killer。当容器内存超过限制时,Linux内核会杀掉容器里的进程。如果容器里的主进程被杀,容器就会退出。可以通过docker inspect查看容器的退出码,137表示被SIGKILL杀掉(通常是OOM),143表示被SIGTERM杀掉(正常停止)。
6. 应用部署实战:从单容器到多容器编排
6.1 单容器部署一个Web应用
先从一个最简单的场景开始:部署一个静态网站。
docker run -d \ --name my-site \ -p 80:80 \ -v /data/site/html:/usr/share/nginx/html:ro \ -v /data/site/conf/nginx.conf:/etc/nginx/nginx.conf:ro \ --restart unless-stopped \ nginx:1.25.3这个部署方案里,网站文件放在宿主机的/data/site/html,Nginx配置放在/data/site/conf/nginx.conf。要更新网站内容,直接改宿主机的文件就行,不需要重建容器。要改Nginx配置,改完执行docker restart my-site即可。
这种方式的优点是简单直接,适合个人站点或者小型项目。缺点是如果服务器上要跑多个服务,端口管理会变得混乱,而且每个服务的配置分散在各处,不好统一管理。
6.2 用Docker Compose编排多容器应用
当应用涉及多个容器(比如Web + 数据库 + 缓存),用docker run一个个启动就很麻烦了。Docker Compose通过一个YAML文件来定义和管理多容器应用。
一个典型的Web应用Compose文件:
version: "3.8" services: web: image: myapp:1.0.0 build: . ports: - "8000:8000" environment: - DATABASE_URL=postgresql://user:pass@db:5432/mydb - REDIS_URL=redis://cache:6379/0 depends_on: db: condition: service_healthy cache: condition: service_started restart: unless-stopped networks: - backend db: image: postgres:16 environment: - POSTGRES_USER=user - POSTGRES_PASSWORD=pass - POSTGRES_DB=mydb volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U user -d mydb"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped networks: - backend cache: image: redis:7-alpine volumes: - redisdata:/data restart: unless-stopped networks: - backend volumes: pgdata: redisdata: networks: backend: driver: bridge这个文件里有几个关键设计点。
depends_on配合healthcheck:depends_on只保证容器的启动顺序,不保证依赖的服务已经准备好接受连接。比如db容器启动了,但PostgreSQL可能还在初始化,这时候web容器去连接就会失败。加上condition: service_healthy之后,Compose会等db的healthcheck通过之后才启动web。这个细节在实际部署中非常重要,我见过很多次因为没配healthcheck导致应用启动时连不上数据库而崩溃的情况。
自定义网络:所有服务都加入backend网络,这样它们可以通过服务名互相访问(web里用db和cache作为主机名)。同时这些服务不会暴露到宿主机网络上,只有web的8000端口映射出来了,数据库和缓存对外不可见,安全性更好。
数据卷命名:pgdata和redisdata是命名卷,Docker会自动管理它们的实际存储位置。这样即使删除了容器,数据卷还在,重新up之后数据还在。
Compose的常用命令:
# 启动所有服务(后台运行) docker compose up -d # 查看服务状态 docker compose ps # 查看日志 docker compose logs -f web # 重新构建并启动 docker compose up -d --build # 停止并删除容器(数据卷保留) docker compose down # 停止并删除容器和数据卷(数据也删掉) docker compose down -vdocker compose down -v这个命令要特别小心,它会删除数据卷,数据库里的数据就全没了。执行前一定要确认。
6.3 环境变量与配置管理
生产环境的配置不应该硬编码在镜像里,而应该通过环境变量注入。Compose支持从.env文件读取环境变量:
# .env文件 DB_PASSWORD=your_secure_password APP_PORT=8000然后在compose文件里引用:
services: web: ports: - "${APP_PORT}:8000" environment: - DB_PASSWORD=${DB_PASSWORD}.env文件应该加入.gitignore,不要提交到代码仓库。对于敏感信息(数据库密码、API密钥),更好的做法是用Docker Secret(在Swarm模式下)或者外部的密钥管理服务。但在单机Compose环境下,.env文件配合严格的权限控制(chmod 600 .env)也是可以接受的。
6.4 部署后的验证与日志查看
部署完成后,验证服务是否正常:
# 检查容器状态 docker compose ps # 检查应用健康检查端点 curl -f http://localhost:8000/health # 查看应用日志 docker compose logs --tail=100 web日志查看是排查问题的第一手段。Docker默认使用json-file日志驱动,日志可以通过docker logs查看:
# 查看容器全部日志 docker logs myapp # 实时跟踪日志 docker logs -f myapp # 查看最近100行 docker logs --tail 100 myapp # 查看指定时间之后的日志 docker logs --since 2024-01-01T00:00:00 myapp如果日志量很大,建议配置日志轮转(前面在daemon.json里已经配了)。另外,生产环境可以考虑把日志输出到stdout,然后由Docker的日志驱动统一收集到日志系统(比如ELK或Loki),而不是写在容器内的文件里。
7. 常见问题与排查技巧实录
7.1 容器启动失败怎么排查
容器启动失败是最常见的问题。排查步骤应该是:先看状态,再看日志,最后看详细信息。
# 查看所有容器(包括已退出的) docker ps -a # 查看容器日志 docker logs <容器名> # 查看容器详细信息(包括退出码) docker inspect <容器名> | jq '.[0].State'退出码是重要的线索:
| 退出码 | 含义 | 常见原因 |
|---|---|---|
| 0 | 正常退出 | 进程执行完毕,如果是服务进程说明有问题 |
| 1 | 应用错误 | 代码异常、配置错误 |
| 137 | 被SIGKILL | OOM被杀或手动kill |
| 143 | 被SIGTERM | 正常停止 |
| 126 | 命令不可执行 | 权限问题或命令不存在 |
| 127 | 命令未找到 | 启动命令写错 |
如果日志里没有有用信息,可以尝试覆盖启动命令,进入容器手动排查:
docker run -it --entrypoint /bin/bash myapp:1.0.0这样会以bash启动容器,你可以手动执行启动命令,看具体报什么错。
7.2 端口冲突与网络问题
端口冲突的报错通常是Bind for 0.0.0.0:8080 failed: port is already allocated。排查方法:
# 查看宿主机上哪个进程占用了端口 sudo lsof -i :8080 # 或者 sudo netstat -tlnp | grep 8080如果确认是Docker容器占用的,找到对应的容器:
docker ps --format "table {{.Names}}\t{{.Ports}}" | grep 8080容器之间网络不通的排查:
# 进入容器测试网络连通性 docker exec -it app ping db # 查看容器的网络配置 docker inspect app | jq '.[0].NetworkSettings.Networks' # 查看自定义网络的容器列表 docker network inspect mynet一个常见问题是:容器A和容器B在同一个自定义网络里,但A ping不通B。原因可能是B没有启动,或者B不在这个网络里。用docker network inspect可以看到网络里有哪些容器。
7.3 磁盘空间被占满的处理
Docker用久了磁盘空间容易被占满,主要有几个来源:停止的容器、悬空镜像、未使用的数据卷、构建缓存、容器日志。
排查:
docker system df -v这个命令会详细列出每个镜像、容器、数据卷、构建缓存的大小。
清理:
# 清理停止的容器、悬空镜像、未使用的网络和构建缓存 docker system prune # 再加上未使用的数据卷(危险,确认后再执行) docker system prune --volumes # 只清理构建缓存 docker builder prune我个人的习惯是:每月做一次docker system prune,但--volumes参数从来不加,因为数据卷里可能有重要数据。数据卷的清理一定要手动确认。
7.4 容器时间不对的问题
容器里的时间默认是UTC,如果应用依赖本地时间,可能会出现时间差8小时的问题。解决办法有两种:
一种是在启动容器时挂载宿主机的时区文件:
docker run -d \ -v /etc/localtime:/etc/localtime:ro \ -v /etc/timezone:/etc/timezone:ro \ myapp:1.0.0另一种是在Dockerfile里设置时区:
ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone推荐第二种,因为时区是应用环境的一部分,应该在镜像里就确定好,而不是依赖宿主机的配置。
7.5 常见问题速查表
| 问题现象 | 可能原因 | 排查命令 | 解决方案 |
|---|---|---|---|
| 容器启动后立即退出 | 启动命令执行失败 | docker logs <容器> | 检查启动命令和依赖 |
| 端口无法访问 | 端口未映射或防火墙拦截 | docker port <容器> | 检查-p参数和防火墙规则 |
| 容器间无法通信 | 不在同一网络 | docker network inspect | 创建自定义网络并加入 |
| 磁盘空间不足 | 日志/镜像/卷占用 | docker system df -v | 清理无用资源 |
| 时间差8小时 | 时区未设置 | docker exec <容器> date | 挂载时区文件或设置TZ |
| 数据丢失 | 未使用数据卷 | docker inspect查看Mounts | 配置数据卷或绑定挂载 |
| 内存超限被杀 | 未设限制或限制过小 | docker inspect看退出码 | 调整--memory参数 |
| 镜像拉取慢 | 未配加速器 | docker info | 配置registry-mirrors |
7.6 几个我踩过的坑
第一个坑:用latest标签导致生产环境行为变化。有一次部署一个服务,本地测试用的是nginx:latest,当时是1.24版本。过了两个月,服务器上重新拉取nginx:latest,变成了1.25版本,配置文件语法有变化,导致服务起不来。从那以后,所有生产环境的镜像都锁定具体版本号。
第二个坑:容器日志把磁盘写满。一个跑了两年的日志收集容器,日志文件涨到了50GB,把服务器磁盘撑爆了。后来在daemon.json里加了日志轮转配置,每个容器日志最多100MB,保留3个文件。这个配置应该成为所有Docker环境的标配。
第三个坑:数据库容器直接删了导致数据丢失。早期用Docker跑数据库时,没配数据卷,直接docker rm删了容器,数据全没了。后来所有数据库容器都必须配数据卷,而且删除容器前一定先确认数据卷的状态。
第四个坑:容器内进程以root运行的安全隐患。默认情况下容器内进程以root运行,如果容器被攻破,攻击者可能利用某些漏洞影响宿主机。后来在Dockerfile里加了非root用户:
RUN useradd -m appuser USER appuser这个改动虽然简单,但能显著提升安全性。
8. 一些进阶方向与个人体会
8.1 从Compose到更复杂的编排
Docker Compose适合单机多容器场景。当服务需要跨多台机器部署、需要自动扩缩容、需要滚动更新时,就需要更复杂的编排工具了。Kubernetes是目前最主流的选择,但学习曲线比较陡。我的建议是:先把Docker和Compose用熟,理解容器、镜像、网络、存储这些基础概念,再去学Kubernetes会顺畅很多。因为Kubernetes里的Pod、Service、Volume这些概念,本质上都是Docker概念的延伸和抽象。
如果暂时用不上Kubernetes,Docker Swarm也是一个轻量级的选择。它内置在Docker引擎里,不需要额外安装,支持多机编排和滚动更新。虽然社区活跃度不如Kubernetes,但对于中小规模的应用来说,Swarm的复杂度低很多,运维成本也低。
8.2 镜像安全的一些实践
生产环境的镜像安全值得重视。几个基本实践:
- 使用官方镜像或可信来源的镜像,避免用来路不明的第三方镜像;
- 定期更新基础镜像,修复已知漏洞;
- 用非root用户运行容器进程;
- 镜像里不要包含敏感信息(密钥、密码),通过环境变量或Secret注入;
- 用镜像扫描工具(比如Trivy)定期扫描镜像漏洞。
# 用Trivy扫描镜像漏洞 trivy image myapp:1.0.08.3 个人使用体会
我用Docker大概有六七年了,从最开始只会docker run,到后来写Dockerfile、用Compose、再到接触Kubernetes,一路踩了不少坑。回头看,最大的体会是:Docker的价值不在于"能跑起来",而在于"到哪都能一样地跑起来"。
很多人学Docker的时候,关注点放在命令怎么敲、参数怎么配,这些当然重要,但更重要的是理解它背后的设计哲学:标准化、不可变基础设施、声明式配置。理解了这些,你写出来的Dockerfile和Compose文件才会是"对"的,而不只是"能跑"的。
另一个体会是:不要为了用Docker而用Docker。一个简单的静态网站,直接放Nginx里跑就行,没必要非得套个容器。Docker解决的是环境一致性和部署标准化的问题,如果你的场景里这些问题不突出,那用不用Docker都行。技术选型要看场景,不要看热度。
最后分享一个小技巧:如果你经常需要临时跑一个工具或者测试一个命令,可以用docker run --rm,容器退出后自动删除,不会留下垃圾。比如临时跑一个Python脚本:
docker run --rm -v $(pwd):/app -w /app python:3.11-slim python script.py这样既不用在宿主机上装Python,也不会留下任何残留文件。这个用法我在日常工作中用得非常多,推荐你也试试。