docker
Docker 是现在后端开发、微服务、DevOps、云原生开发中非常常见的一项技术。
很多人在第一次接触 Docker 时,会觉得它就是“启动容器的工具”,然后开始背各种 docker run、docker ps、docker exec 命令。
但只记命令很难把 Docker 用明白。它要解决的问题是:
如何把应用程序和它所依赖的运行环境统一打包,让程序在不同机器上都能够以相同的方式运行。
比如一个 Python 项目在本地运行正常,但是部署到服务器以后可能出现:
1 | Python 版本不同 |
最终就会出现经典问题:
1 | 在我电脑上明明可以运行。 |
Docker 的作用,就是尽可能减少这种环境差异。
下面从核心概念讲起,依次是镜像、容器、数据卷、网络、Dockerfile、Docker Compose,最后是真实项目中的用法。
一、先把 Docker 的核心概念搞清楚
学习 Docker 之前,先要理解几个概念:
1 | Image |
Docker 的日常使用基本都围绕它们展开。
Docker 和虚拟机有什么区别
传统虚拟机,比如 VMware 中安装一个 Ubuntu,大致结构是:
1 | 物理机 |
每个虚拟机都包含一套比较完整的操作系统。
Docker 则更接近:
1 | 物理机 |
多个容器共享宿主机内核,因此 Docker 容器通常比传统虚拟机更加轻量。
可以简单理解为:
1 | 虚拟机:虚拟出一整台电脑 |
Image 和 Container
其中最基础的是这两个:
1 | Image |
Image 中文一般叫:
1 | 镜像 |
它可以理解成应用运行环境的模板。
例如:
1 | nginx |
都可以作为 Docker 镜像存在。
比如:
1 | docker pull nginx |
表示下载 nginx 镜像。
查看本地镜像:
1 | docker images |
而 Container,也就是:
1 | 容器 |
可以理解成镜像运行以后产生的实例。
例如:
1 | docker run nginx |
整个过程可以理解成:
1 | nginx Image |
同一个镜像可以启动多个容器:
1 | nginx Image |
如果熟悉面向对象,也可以简单类比:
1 | Image ≈ Class |
当然这个类比并不完全严谨,但很适合初学阶段理解。
Dockerfile 和 Docker Compose
如果只是运行 nginx、Redis、MySQL,我们可以直接使用别人已经制作好的镜像。
但自己的项目怎么办?
比如:
1 | FastAPI |
这时候就需要:
1 | Dockerfile |
Dockerfile 用来描述:
如何把自己的项目构建成 Docker 镜像。
例如:
1 | 使用 Python 3.12 |
这些步骤都可以写进 Dockerfile。
但是一个真实项目通常不只有一个容器。
例如:
1 | Vue |
如果每个服务都单独写一条 docker run,管理起来会非常麻烦。
所以 Docker 又提供了:
1 | Docker Compose |
通过一个 compose.yaml 文件,就可以统一管理整个项目。
可以记住:
1 | Dockerfile |
二、Docker 的基本使用
安装好 Docker 后,可以先执行:
1 | docker --version |
查看 Docker 版本。
再执行:
1 | docker compose version |
检查 Docker Compose。
最后运行:
1 | docker run hello-world |
如果出现:
1 | Hello from Docker! |
说明 Docker 已经能够正常运行。
Windows 开发环境一般使用:
1 | Docker Desktop |
Ubuntu 或 Linux 服务器则通常直接安装 Docker Engine。
安装完成后,日常开发基本都围绕下面几个操作:
1 | 拉取镜像 |
镜像操作
下载 nginx:
1 | docker pull nginx |
指定标签(alpine 是基于 Alpine Linux 的精简版本):
1 | docker pull nginx:alpine |
查看镜像:
1 | docker images |
也可以写:
1 | docker image ls |
删除镜像:
1 | docker rmi nginx |
或者:
1 | docker image rm nginx |
Docker 镜像通常使用:
1 | 镜像名称:Tag |
例如:
1 | python:3.12 |
如果不指定 Tag:
1 | docker pull nginx |
通常会使用:
1 | nginx:latest |
不过真实项目,尤其是生产环境,最好明确指定版本。
例如:
1 | redis:7 |
而不是全部使用:
1 | latest |
否则某一天重新拉取镜像以后,实际版本可能发生变化。
创建和管理容器
运行 nginx:
1 | docker run nginx |
此时 nginx 会直接占用当前终端。
如果希望后台运行:
1 | docker run -d nginx |
其中:
1 | -d |
表示:
1 | detach |
也就是后台运行。
查看正在运行的容器:
1 | docker ps |
查看所有容器:
1 | docker ps -a |
给容器指定名称:
1 | docker run -d --name my-nginx nginx |
之后就可以直接使用容器名称进行操作。
停止:
1 | docker stop my-nginx |
重新启动:
1 | docker start my-nginx |
重启:
1 | docker restart my-nginx |
删除:
1 | docker rm my-nginx |
如果容器正在运行:
1 | docker rm -f my-nginx |
端口映射
Docker 容器内部有自己的网络空间。
即使 nginx 在容器内部监听:
1 | 80 |
宿主机也不能直接访问。
所以需要做端口映射。
例如:
1 | docker run -d \ |
这里:
1 | 8080:80 |
表示:
1 | 宿主机端口 : 容器端口 |
也就是:
1 | localhost:8080 |
此时浏览器访问:
1 | http://localhost:8080 |
就可以访问 nginx。
其他服务也一样,比如:
1 | FastAPI |
查看日志和进入容器
查看日志:
1 | docker logs my-nginx |
持续查看:
1 | docker logs -f my-nginx |
查看最后 100 行:
1 | docker logs --tail 100 my-nginx |
实际开发中,如果一个容器启动失败,第一件事通常就是:
1 | docker logs 容器名 |
如果需要进入容器:
1 | docker exec -it my-nginx bash |
进入之后,就像操作普通 Linux 一样:
1 | ls |
退出:
1 | exit |
有些精简镜像没有 bash,这时候可以使用:
1 | docker exec -it my-nginx sh |
查看容器详细信息:
1 | docker inspect my-nginx |
查看容器资源占用:
1 | docker stats |
可以看到:
1 | CPU |
三、Docker 数据和网络
容器虽然很好用,但有两个问题必须解决:
1 | 数据保存在哪里? |
这两个问题分别对应:
1 | Volume |
Volume:解决数据持久化
假设直接启动 PostgreSQL:
1 | docker run postgres |
官方 postgres 镜像声明了 VOLUME,不指定挂载时,数据库数据会写入 Docker 自动创建的匿名 Volume。
如果以后删除容器:
1 | docker rm postgres |
匿名 Volume 不会随容器一起删除(除非使用 docker rm -v),但它只有一串随机 ID,很难找到并在新容器中复用,数据实际上就等于丢了。
数据库显然不能这么用。
Docker 为此提供:
1 | Volume |
创建 Volume:
1 | docker volume create postgres-data |
查看:
1 | docker volume ls |
然后启动 PostgreSQL:
1 | docker run -d \ |
关键部分:
1 | -v postgres-data:/var/lib/postgresql/data |
表示:
1 | Docker Volume |
这样数据库数据就不再完全依赖容器本身。
即使删除:
1 | docker rm postgres |
Volume 仍然存在。
重新创建 PostgreSQL,并挂载同一个 Volume,就可以继续读取之前的数据。
Bind Mount:把本地目录挂进容器
除了 Volume,还可以直接挂载宿主机目录。
例如:
1 | docker run -d \ |
这里:
1 | ./html |
是宿主机目录。
1 | /usr/share/nginx/html |
是容器目录。
两者绑定以后:
1 | 宿主机修改文件 |
这种方式叫:
1 | Bind Mount |
开发环境中很常见。
一般可以这样选择:
1 | 数据库数据 |
Network:解决容器之间通信
假设现在有两个容器:
1 | backend |
那么 backend 怎么访问 postgres?
很多初学者第一反应是:
1 | localhost:5432 |
但这是错误的。
因为:
1 | 容器里的 localhost |
指的是:
1 | 容器自己 |
而不是宿主机,也不是另一个容器。
正确做法是创建一个 Docker 网络:
1 | docker network create app-network |
启动 PostgreSQL:
1 | docker run -d \ |
启动 backend:
1 | docker run -d \ |
此时 backend 可以通过:
1 | postgres:5432 |
访问数据库。
例如:
1 | postgresql://postgres:123456@postgres:5432/app |
这里:
1 | postgres |
就是容器名称。
Docker 网络会自动把容器名称解析成对应容器的地址。
以后遇到:
1 | Connection refused |
首先就要检查:
1 | 是不是把数据库地址错误写成 localhost 了? |
四、Dockerfile:把自己的项目放进 Docker
假设有一个 FastAPI 项目:
1 | docker-demo/ |
main.py:
1 | from fastapi import FastAPI |
requirements.txt:
1 | fastapi |
然后创建:
1 | Dockerfile |
内容:
1 | FROM python:3.12-slim |
这个 Dockerfile 已经包含了最常用的几个指令。
FROM、WORKDIR、COPY、RUN
首先:
1 | FROM python:3.12-slim |
表示当前镜像基于:
1 | python:3.12-slim |
构建。
然后:
1 | WORKDIR /app |
表示后面的命令都默认在:
1 | /app |
目录中执行。
类似:
1 | cd /app |
接下来:
1 | COPY requirements.txt . |
表示把宿主机中的:
1 | requirements.txt |
复制到镜像中的:
1 | /app |
再执行:
1 | RUN pip install --no-cache-dir -r requirements.txt |
安装 Python 依赖。
注意:
1 | RUN |
是在:
1 | 构建镜像阶段 |
执行的。
最后:
1 | COPY . . |
把项目代码复制进镜像。
EXPOSE 和 CMD
1 | EXPOSE 8000 |
表示应用预计使用:
1 | 8000 |
端口。
但 EXPOSE 本身并不会自动把端口暴露给宿主机。
真正运行时仍然需要:
1 | -p 8000:8000 |
最后:
1 | CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] |
表示容器启动时默认执行:
1 | uvicorn |
这里需要特别注意:
1 | --host 0.0.0.0 |
如果使用:
1 | 127.0.0.1 |
程序可能只能在容器内部访问。
构建并运行镜像
构建:
1 | docker build -t fastapi-demo . |
这里:
1 | -t fastapi-demo |
表示给镜像命名。
最后的:
1 | . |
表示:
1 | 当前目录作为 Build Context |
构建完成后查看:
1 | docker images |
然后启动:
1 | docker run -d \ |
浏览器打开:
1 | http://localhost:8000 |
应该能够看到:
1 | { |
FastAPI 文档:
1 | http://localhost:8000/docs |
这样就完成了一个最基本的项目 Docker 化。
Dockerfile 的缓存机制
Dockerfile 中指令的顺序会影响构建缓存。
例如不要简单写成:
1 | COPY . . |
因为只要任何项目代码发生改变:
1 | COPY . . |
这一层缓存就会失效。
后面的依赖安装也可能重新执行。
更推荐:
1 | COPY requirements.txt . |
因为依赖文件:
1 | requirements.txt |
通常变化频率远低于业务代码。
这样平时只修改:
1 | main.py |
Docker 就可以复用前面的依赖缓存。
构建速度会快很多。
.dockerignore
项目里还应该创建:
1 | .dockerignore |
例如:
1 | .git |
它的作用类似:
1 | .gitignore |
只是它控制的是:
哪些内容不要放进 Docker Build Context。
如果不写 .dockerignore,项目中的:
1 | node_modules |
都有可能一起发送给 Docker。
结果就是:
1 | 构建很慢 |
所以真实项目最好都配置 .dockerignore。
五、Docker Compose:管理完整项目
当一个项目开始出现:
1 | backend |
继续使用大量 docker run 命令就不合适了。
这时候应该使用:
1 | Docker Compose |
假设我们有:
1 | FastAPI |
可以创建:
1 | compose.yaml |
内容如下:
1 | services: |
然后只需要:
1 | docker compose up |
就可以启动整个项目。
后台运行:
1 | docker compose up -d |
重新构建:
1 | docker compose up -d --build |
Compose 配置怎么理解
在:
1 | services: |
下面,每一个对象都可以理解成一个服务。
例如:
1 | services: |
相当于三个容器服务。
build:
1 | backend: |
表示:
1 | 根据当前目录中的 Dockerfile 构建 |
image:
1 | redis: |
表示:
1 | 直接使用 redis:7 镜像 |
端口:
1 | ports: |
相当于:
1 | docker run -p 8000:8000 |
环境变量:
1 | environment: |
相当于:
1 | -e DATABASE_URL=xxx |
数据卷:
1 | volumes: |
用来持久化 PostgreSQL 数据。
Compose 的服务通信
Docker Compose 默认会给整个项目创建一个内部网络。
因此:
1 | backend |
可以直接访问:
1 | postgres |
PostgreSQL:
1 | postgres:5432 |
Redis:
1 | redis:6379 |
所以:
1 | DATABASE_URL |
应该写:
1 | postgresql://postgres:123456@postgres:5432/app |
而不是:
1 | postgresql://postgres:123456@localhost:5432/app |
可以把服务名理解成 Compose 网络中的内部域名。
整体关系:
1 | Browser |
depends_on 和 healthcheck
很多人会写:
1 | depends_on: |
但这只意味着:
1 | 先启动 postgres 容器 |
它不一定意味着:
1 | PostgreSQL 已经真正完成初始化 |
所以数据库最好配合:
1 | healthcheck: |
例如:
1 | healthcheck: |
然后 backend:
1 | depends_on: |
这样整体启动过程就是:
1 | 启动 PostgreSQL |
这样启动顺序会更可靠。
Compose 常用命令
启动:
1 | docker compose up |
后台启动:
1 | docker compose up -d |
重新构建:
1 | docker compose up -d --build |
查看服务:
1 | docker compose ps |
查看日志:
1 | docker compose logs |
实时查看:
1 | docker compose logs -f |
只看 backend:
1 | docker compose logs -f backend |
进入 backend:
1 | docker compose exec backend bash |
停止:
1 | docker compose stop |
重新启动:
1 | docker compose start |
删除容器和 Compose 网络:
1 | docker compose down |
如果执行:
1 | docker compose down -v |
还会删除 Volume。
所以数据库项目中一定要谨慎。
六、真实项目中的 Docker 使用方式
下面把前面的内容放进一个更接近真实的项目结构里。假设项目:
1 | my-project/ |
整个系统:
1 | Frontend |
前端使用多阶段构建
Vue 或 React 项目适合使用:
1 | Multi-stage Build |
例如:
1 | FROM node:22-alpine AS builder |
第一阶段:
1 | Node.js |
第二阶段:
1 | nginx |
最终生产镜像不需要:
1 | Node.js |
只需要:
1 | nginx |
这样可以明显减小镜像体积。
使用环境变量
真实项目基本都需要:
1 | DATABASE_URL |
Docker 可以通过:
1 | -e |
传入。
例如:
1 | docker run \ |
也可以通过 .env:
1 | APP_ENV=production |
然后:
1 | docker run --env-file .env my-app |
需要注意:
1 | 不要把真正的 Token、密码、API Key 直接写进 Dockerfile。 |
例如不建议:
1 | ENV API_KEY=xxxxxx |
因为 Docker 镜像可能被:
1 | 上传 |
敏感信息就可能泄露。
设置自动重启
服务器上的容器一般希望能够自动恢复。
可以:
1 | docker run \ |
Compose 中:
1 | services: |
常见策略:
1 | no |
生产环境中:
1 | unless-stopped |
是比较常见的选择。
限制 CPU 和内存
Docker 也可以限制容器资源。
例如:
1 | docker run \ |
表示:
1 | 最大内存:512 MB |
这样可以防止某个容器因为:
1 | 内存泄漏 |
耗尽整台服务器资源。
资源监控:
1 | docker stats |
七、Docker 常见问题和排错思路
实际使用中,报错大多集中在下面几类。
容器启动后立即退出
先查看:
1 | docker ps -a |
然后:
1 | docker logs 容器名 |
常见原因:
1 | 启动命令错误 |
Docker 容器通常和它的主进程生命周期绑定。
也就是说:
1 | 主进程结束 |
所以:
1 | Exited |
本身并不一定是 Docker 的问题。
很多时候是容器里的应用退出了。
端口被占用
如果出现:
1 | port is already allocated |
说明宿主机端口已经被占用。
Windows:
1 | netstat -ano | findstr :8000 |
Linux:
1 | sudo lsof -i :8000 |
也可以换宿主机端口:
1 | ports: |
此时访问:
1 | localhost:8001 |
实际映射到:
1 | container:8000 |
修改代码以后没有变化
如果 Dockerfile 中:
1 | COPY . . |
表示代码是在构建镜像时复制进去的。
所以修改代码后通常需要:
1 | docker compose up -d --build |
重新构建镜像。
开发环境如果想实现实时同步,可以使用:
1 | Bind Mount |
把源码目录挂进容器。
容器访问不了数据库
最常见错误之一:
1 | localhost |
写错。
如果数据库也是 Compose 服务:
1 | postgres: |
后端应该连接:
1 | postgres:5432 |
而不是:
1 | localhost:5432 |
因为:
1 | Container 中的 localhost |
Docker 占用磁盘越来越大
先查看:
1 | docker system df |
可能占空间的有:
1 | Images |
清理停止容器:
1 | docker container prune |
清理无用镜像:
1 | docker image prune |
清理网络:
1 | docker network prune |
统一清理:
1 | docker system prune |
更加彻底:
1 | docker system prune -a |
但要特别注意:
1 | Volume 不要随便删。 |
特别是:
1 | docker volume prune |
因为里面可能存着:
1 | MySQL |
的重要数据。
八、把整个 Docker 工作流串起来
一个项目 Docker 化的流程基本是:
1 | 项目源码 |
如果项目包含多个服务:
1 | Frontend Dockerfile |
实际开发过程中通常就是:
1 | git clone 项目地址 |
进入项目:
1 | cd project |
准备环境变量:
1 | cp .env.example .env |
修改 .env。
然后:
1 | docker compose up -d --build |
查看:
1 | docker compose ps |
如果有问题:
1 | docker compose logs -f |
停止:
1 | docker compose down |
传统部署可能需要:
1 | 安装 Python |
使用 Docker 后,服务器上的基础环境可以大幅简化成:
1 | 安装 Docker |
九、常用 Docker 命令速查
镜像
1 | docker pull nginx |
容器
1 | docker run -d my-app |
端口
1 | docker run -p 8000:8000 my-app |
日志
1 | docker logs container |
进入容器
1 | docker exec -it container bash |
Volume
1 | docker volume ls |
Network
1 | docker network ls |
Compose
1 | docker compose up |
磁盘管理
1 | docker system df |
总结
Docker 的核心是这几块:
1 | Docker |
从源码到运行的主线是:
1 | 源码 |
再加上:
1 | Container |
就能把多个服务组成一个完整的应用。
如果能够独立完成:
1 | 给后端写 Dockerfile |
日常开发中的 Docker 用法基本就够了。
后续继续深入时,可以再学习:
1 | Docker BuildKit |
这些内容都建立在前面讲到的几个概念之上:
1 | Image |