Docker 和 Docker Compose
为什么需要 Docker?
✳️ 传统软件部署的问题
在没有 Docker 之前,例如部署一个 Web 系统:
前端系统
|
后端程序(Java/Python)
|
数据库(PostgreSQL)
|
缓存(Redis)
开发人员需要手动安装:
操作系统
|
Python/Java运行环境
|
各种依赖库
|
数据库
|
配置文件
|
启动脚本
例如:
开发人员电脑:
Python 3.11
PostgreSQL 16
Redis 7
测试服务器:
Python 3.9
PostgreSQL 14
Redis 6
生产服务器:
Python 3.8
PostgreSQL 12
Redis 5
结果:
开发环境能运行,但是部署后:
服务器运行失败
原因:
- Python版本不同
- 数据库版本不同
- 缺少依赖包
- 配置不一致
- 环境变量不同
这就是软件开发中的经典问题: 环境不一致问题。
✳️ Docker解决什么问题?
Docker的核心思想:把应用程序和它运行需要的环境一起打包。
传统方式:
服务器
操作系统
|
运行环境
|
依赖库
|
应用程序
每次部署都需要重新安装环境。
而Docker方式:
服务器
Docker
|
|
应用容器
[应用程序
+ 运行环境
+ 依赖库
+ 配置]
应用需要什么,都一起带过去。
Docker 通过 镜像(Image) 和 容器(Container) 来实现应用和环境的打包。
✳️ 镜像(Image)
Docker镜像是用来打包应用程序和运行环境的模板。
它的目的就是方便从一个镜像 创建容器。
例如:
一个 Python 镜像,可以用来在不同的服务器上创建 Python 容器。 一个 MySQL 镜像,可以用来在不同的服务器上创建 MySQL 容器。 一个 PostgreSQL 镜像,可以用来在不同的服务器上创建 PostgreSQL 容器。
✳️ 容器(Container)
容器就是镜像运行起来后的实例。
例如:
镜像:
postgres:17
相当于:
数据库安装包
运行:
docker run postgres:17
得到:
PostgreSQL数据库实例
也就是:
容器
所以, Docker带来的价值包括:
✳️ ① 环境统一
开发 -- 测试 -- 生产 都用同一个 Docker 镜像。
确保不会出现“我电脑上能运行”的问题,到了你的服务器就运行不了。
而且,Docker 镜像不关心宿主机发行版
例如,你的宿主机是 CentOS, 而拉取安装的 Docker 镜像是基于 Ubuntu 或者 Debian,依然可以运行。
因为 因为 Docker 使用的是:宿主机 Linux Kernel + 容器自己的用户空间
它只依赖宿主机的 Linux 内核,而不依赖宿主机的发行版和用户空间。
Docker 镜像中已经包含了除 Linux 内核外运行所需的环境和依赖。
✳️ ② 部署快速
以前,安装环境需要几个小时
而使用Docker后,往往只需要几分钟就可以完成部署。
✳️ ③ 易于迁移
例如,服务器A上运行Docker系统,要迁移到服务器B:
只需要复制:
compose.yml
数据文件
重新启动即可。
✳️ ④ 隔离应用
传统部署中,多个应用共享服务器环境,容易因为依赖、配置和资源问题互相影响;
Docker 通过容器隔离,让每个应用拥有独立的运行环境。
✳️ 企业现在大量使用 Docker
现代企业系统通常不是一个程序:
例如一个 AI 平台:
用户
|
Nginx
|
业务服务
|
-----------------
PostgreSQL
Redis
MinIO
向量数据库
大模型服务
如果不用 Docker:
每个组件:
- 单独安装
- 单独配置
- 单独维护
使用 Docker:
compose.yml
定义全部服务
↓
一条命令启动
docker compose up -d
Docker和虚拟机的区别
很多新人会问:
Docker是不是虚拟机?
不是。
✳️ 虚拟机
服务器
|
宿主机系统
|
虚拟机系统
|
应用
每个虚拟机都有完整操作系统。
✳️ Docker
服务器
|
Docker
|
容器
|
应用
多个容器共享宿主机内核。
所以:
Docker:
- 更轻量
- 启动更快
- 资源占用更少
得益于 Docker 的镜像分层机制,多个 Docker 镜像,如果它们依赖的基础层有公共部分, 比如都是基于 Debian 制作的,那么它们共享基础层,不会重复占用磁盘空间。 而且在容器运行的时候也会共享这些基础层的内存。
nginx 镜像 postgres 镜像
============== ==============
Layer 3:
配置文件
几MB
------------
Layer 2: Layer 2:
nginx软件 PostgreSQL
30MB 300MB
------------ ------------
Layer 1: Layer 1:
Debian 基础系统 Debian 基础系统
50MB 50MB
安装 Docker
注意下面的命令基本上都是需要 root 权限的,可以临时切换到 root 用户执行
也可以在命令前面加上 sudo 执行
✳️ Debian/Ubuntu 环境准备
Docker主要打包的应用都是Linux上面的,其他平台的小众,我们不讨论。
这里我们使用 Debian/Ubuntu Linux 作为运行Docker的操作系统。
大部分学员使用的电脑都是 Windows 或者 Mac,而不是 Linux,所以你们必须先安装一个 Linux 的虚拟机。 然后在虚拟机里面的 Ubuntu/Debian 上安装 Docker。
如果你是Windows系统的话,推荐安装 WSL2 + Ubuntu/Debian, Windows安装 使用 WSL2教程点击这里学习
Windows + WSL2 上安装 Docker 可以通过安装 Docker Desktop简化安装。
但是,为了演示更通用的方法,这里我们不使用 Docker Desktop, 而是直接在 WSL2 的 Ubuntu/Debian 上安装 Docker。
先更新apt软件包索引(package index),并升级apt版本到最新:
apt update
apt upgrade -y
安装基础工具:
apt install -y \
curl \
wget \
vim \
git \
ca-certificates
推荐使用 Docker CE 官方仓库来获得:
- 最新 Docker Engine
- Docker Compose
- Buildx
✳️ 创建 key 目录
install -m 0755 -d /etc/apt/keyrings
install -m 0755 -d /etc/apt/keyrings 等价于
mkdir -p /etc/apt/keyrings
chmod 755 /etc/apt/keyrings
✳️ 添加 Docker 官方仓库
如果是 Debian 系统,执行如下命令下载密钥和 添加 Docker 官方仓库:
- 下载 Docker 官方 GPG 密钥
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
- 添加 Docker 官方仓库
cat > /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
如果是 Ubuntu 系统,替换上述命令中的 debian 为 ubuntu 即可。
最后再执行更新:
apt update
这里如果失败,可能是IPv6网络问题,可以尝试指定只使用IPv4:
cat >/etc/apt/apt.conf.d/99force-ipv4 <<EOF
Acquire::ForceIPv4 "true";
EOF
然后再 执行 apt update。
✳️ 安装 Docker 引擎
apt install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin
查看:
docker version
✳️ 不使用 sudo 运行 Docker(推荐)
普通用户默认执行 docker ps 需要 sudo 权限:
可能报:
permission denied
可以这样,把当前用户加入 docker 用户组:
sudo usermod -aG docker $USER
然后退出重新登录,让当前用户权限更改生效,即可使用 docker 命令而无需 sudo。
测试:
docker ps
不用 sudo 即可。
✳️ 启动 Docker 服务
systemctl enable docker
systemctl start docker
查看状态:
systemctl status docker
正常:
active (running)
Docker架构
Docker Client
↓
Docker Daemon
↓
---------------------
Image Container
镜像 容器
✳️ Docker Client(客户端)
你操作 Docker 的入口。
比如你输入:
docker run nginx
这个命令就是通过 Docker Client 发出去的。
可以理解为:
你发出的操作指令。
✳️ Docker Daemon(守护进程)
真正干活的 Docker 后台服务。
它负责:
- 管理镜像(Image)
- 创建容器(Container)
- 启动/停止容器
- 管理网络、存储
可以理解为:
Docker 的管理员和执行者。
✳️ Image(镜像)
镜像就是创建容器的应用模板。
镜像中包含应用运行所需要的程序、依赖库和相关配置。
里面包含:
- 操作系统基础环境
- 软件
- 依赖
- 配置
例如:
nginx镜像 = Debian + Nginx程序 + 配置文件
✳️ Container(容器)
镜像运行起来后的实例。
首先需要有一个镜像,然后通过镜像创建容器。
一句话总结:
Client 发命令,Daemon 执行管理,Image 是模板,Container 是运行中的实例。
这就是 Docker 最核心的工作流程。
国内拉取镜像加速
执行下面的命令时:
docker pull nginx
Docker 默认会从 Docker Hub 拉取镜像。中国大陆服务器访问 Docker Hub 时, 可能因为网络线路原因速度较慢,甚至出现超时或下载失败。
这里说的镜像加速器,加速的是 docker pull、docker run 和 docker build 拉取镜像的过程;
它和前面安装 Docker 软件时使用的 APT 软件仓库不是一回事。
国内访问 Docker Hub 慢,需要配置镜像加速器。
目前比较推荐: 毫秒镜像, DaoCloud 镜像站,等等
✳️ Linux 配置方法
编辑:
sudo vim /etc/docker/daemon.json
写:
{
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker.m.daocloud.io"
]
}
重启 Docker:
sudo systemctl restart docker
检查:
docker info
看到:
Registry Mirrors:
https://docker.1ms.run/
https://docker.m.daocloud.io/
说明配置成功。
✳️ 配置后怎么使用?
不用改命令。
仍然:
docker pull nginx
Docker 会通过加速器网站加速获取镜像:
docker pull nginx
↓
国内镜像加速器
↓
Docker Hub镜像
✳️ 生产环境建议
如果是个人学习可以用上面的加速器。
如果企业服务器,更推荐:
- 使用云厂商自己的镜像仓库:
例如:
- 阿里云 ACR
- 腾讯云 TCR
- 华为云 SWR
- 自建 Harbor
常用命令
✳️ 查看本地镜像
使用如下命令查看本地已经下载的镜像:
docker images
✳️ 获取镜像
通常是使用 docker pull 命令从 Docker Hub 或其他镜像仓库下载。
比如,要获取官方的 Nginx 镜像:
docker pull nginx
✳️ 从镜像创建并启动容器
执行如下命令:
docker run nginx
docker run 会创建一个新的容器并启动它;如果本地没有对应镜像,还会先尝试拉取镜像
更常见的是 后台运行容器:
docker run -d nginx
运行的容器可以理解为:
镜像启动后的一个正在运行的软件。
✳️ 一个镜像可以创建多个容器
比如连续执行:
docker run -d nginx
docker run -d nginx
docker run -d nginx
会基于同一个 nginx 镜像创建出 3 个相互独立的容器,而不是把同一个容器运行三次。
每个容器在运行过程中会产生自己各自不同的数据,保存在容器内部的文件系统中,互不干扰。
Docker Hub
|
|
↓
┌─────────────┐
│ nginx镜像 │
└─────────────┘
|
|
docker run nginx
|
┌──────────┴──────────┐
↓ ↓
┌────────────────┐ ┌────────────────┐
│ nginx容器 A │ │ nginx容器 B │
│ │ │ │
│ 状态: stopped │ │ 状态: running │
│ │ │ │
│ 已创建但没运行 │ │ 正在运行 │
└────────────────┘ └────────────────┘
docker run 命令实际上是创建容器 + 启动容器的组合命令。
等价于:
docker create nginx
docker start 容器ID
使用 --name 可以给容器指定一个名字:
docker run -d --name mynginx nginx
如果启动时没有 --name 参数,那么缺省情况下 Docker 会随机生成一个名字,例如 happy_morse。
同时,每个容器还会有一个唯一的 Container ID。
✳️ 查看容器
查看容器的命令是:
docker ps
可以查看容器的名字和 ID。
默认只显示正在运行(Running)的容器,不会显示已经停止的容器。
如果要 查看全部容器,包括已经停止的容器,可以加上 -a 参数:
docker ps -a
✳️ 停止容器运行
停止运行容器的命令是:
docker stop 容器ID
也可以根据容器名字停止:
docker stop 容器名字
即使容器停止了,这个容器仍然存在,直到执行 docker rm 删除。
✳️ 启动停止的容器
可以启动停止状态的容器:
docker start 容器ID
或者
docker start 容器名字
✳️ 删除已经停止的容器
删除已经停止的容器的命令是:
docker rm 容器ID
也可以根据容器名字删除:
docker rm 容器名字
注意:正在运行的容器不能直接删除,需要先停止。
docker rm 删除的是容器,容器内部的数据也会被删除。 但是不会删除镜像,镜像仍然保留在本地。
✳️ 进入容器
通过在容器中新启动运行一个 shell进程,并且把当前终端连接到这个 shell,就可以进入容器内部。
docker exec -it 容器名 bash
例如:
docker exec -it mynginx bash
这里面 -it 是两个参数:
-i:interactive 交互式,保持标准输入打开,这样你才能在容器中输入命令-t:tty 终端, 分配一个伪终端
另外,不是所有镜像都有 bash。
有的镜像只有 sh(比如基于 Alpine 的镜像),或者其他 shell,所以如果 bash 不存在,可以尝试:
docker exec -it 容器名 sh
进入容器之后,你会发现当前的用户是 root 用户, 因为 docker exec 默认使用容器配置的默认用户来执行命令, 而很多镜像构建时没有额外指定别的用户,默认用户就是 root。
✳️ 查看资源
docker stats
输出类似
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
47c75e99c889 mynginx 0.00% 14.62MiB / 7.396GiB 0.19% 180B / 126B 0B / 8.19kB 17
按 Ctrl + C 退出。
✳️ 查看日志
docker logs 容器名
实时:
docker logs -f 容器名
容器端口映射
如果希望外网用户能访问容器里运行的网络服务(比如 Nginx 提供的网页),
就需要在创建容器时使用 -p 参数,把容器的端口映射到宿主机的端口:
docker run -d \
--name nginx \
-p 8080:80 \
nginx
其中 -p 8080:80 表示:
宿主机 8080 端口 → 容器 80 端口
数据流向:
外网用户
|
↓
http://服务器IP:8080
|
↓
宿主机 8080 端口
|
↓
nginx 容器 80 端口
含义:
8080:宿主机上监听的端口,外网用户访问服务器时使用这个端口80:容器内应用监听的端口,也就是 Nginx 默认监听的端口
这样,外网用户访问:
http://服务器IP:8080
就可以看到 Nginx 提供的默认页面。
这里做映射的时候并没有指定宿主机的 IP 地址,默认是映射到宿主机的所有网卡上。 几乎所有宿主机的 IP 地址加端口访问都可以映射到,等价于:
docker run -d \
--name nginx \
-p 0.0.0.0:8080:80 \
nginx
如果只想绑定某一个 IP地址,可以指定:
docker run -d \
--name nginx \
-p 192.168.1.100:8080:80 \
nginx
同一个宿主机端口不能同时映射给多个容器。
Docker 网络
如果 如果多个容器需要互相通信, 通常需要 把它们加入同一个 自定义Docker 网络。
查看已有网络:
docker network ls
创建自定义网络:
docker network create app-net
创建网络后,需要让容器加入该网络。
✳️ 方式一:创建容器时指定网络:
使用参数 --network指定网络:
docker run -d \
--name postgres \
--network app-net \
postgres
docker run -d \
--name nginx \
--network app-net \
nginx
此时两个容器都连接到了 app-net:
app-net
nginx ------------ postgres
✳️ 方式二:给已有容器连接网络:
docker network connect app-net 容器名
加入同一个网络后,容器之间可以直接通过容器名访问,Docker 会自动解析对应 IP。
例如:
postgres:5432
表示访问名为 postgres 的数据库容器的 5432 端口。
常见架构:
用户请求
|
nginx
|
app-net
|
postgres
其中 app-net 就像一个内部通信通道,让不同容器可以通过名称互相找到,而不需要手动配置 IP。
容器网络(比如 app-net)用于容器之间的通信,而 -p 端口映射用于宿主机外面的用户访问容器。
两者互不冲突:对外提供服务的容器(比如 Nginx)通常需要端口映射; 而 postgres 这种只给其他容器使用的数据库容器,不需要映射端口,通过容器网络访问即可,这样反而更安全。
Docker 数据持久化
Docker 提供了多种方式让容器访问持久化数据, 主要是下面三种:
- 容器自己的文件系统(不推荐保存重要数据)
- Docker Volume(Docker 管理的持久化存储)
- Bind Mount(用户指定的宿主机目录)
1. 容器自己的文件系统
如果你直接:
docker run nginx
没有挂载任何东西。
那么:
容器
|
|
自己的 writable layer
例如,进入:
docker exec -it nginx bash
创建:
echo hello > /tmp/test.txt
这个文件存在:容器可写层(writable layer)
容器被停止重新启动之后,这个文件仍然存在。
但是,如果删除容器:
docker rm nginx
文件就会消失。
数据库数据这样的需要长期保存的数据(用户、订单、表等), 不适合放在容器自己的文件系统中,因为容器随时可能被删除。
实际上数据最终还是保存在宿主机上。
保存位置根据Docker的存储驱动不同而不同。
比如,如果 Storage Driver 是 overlayfs,
那么容器的 /tmp/test.txt 文件可能保存在宿主机的:
/var/lib/docker/rootfs/overlayfs/<container_id>/tmp/test.txt
2. Docker Volume
数据库数据通常推荐使用 Docker Volume 来保存。
这种方式是在宿主机上创建一个 Docker 管理的持久化存储空间,然后挂载到容器中。
✳️ 创建 volume
先需要创建一个 Volume,比如:
docker volume create pgdata
上面的命令,通常实际产生的 Volume 目录在:/var/lib/docker/volumes/pgdata/_data
Docker Volume 是 Docker 管理的一块持久化存储空间, 它表现得像一个独立的数据目录,而不是一个 LVM 逻辑卷,也不是创建一个新的块设备。
查看已有 Volume:
docker volume ls
例如:
DRIVER VOLUME NAME
local pgdata
✳️ 使用 Volume
先下载 PostgreSQL 镜像:
docker pull postgres:17
启动容器时,通过 -v 参数挂载 Volume:
docker run -d --name postgres \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=123456 \
-e POSTGRES_USER=app \
-e POSTGRES_DB=appdb \
postgres:17
其中:
-v 数据卷名称:容器内目录
Docker 会自动把 Volume 挂载到容器内的这个目录。
容器认为自己访问:
/var/lib/postgresql/data
实际上数据存储在宿主机,比如:
/var/lib/docker/volumes/pgdata/_data
✳️ 删除容器后恢复数据
后续即使你删除这个容器:
docker rm postgres
重新创建容器还可以指定使用之前的数据:
docker run \
-v pgdata:/var/lib/postgresql/data \
postgres
3. Bind Mount
Bind Mount 和 Volume 一样也是把数据存储在宿主机上,删除容器也不会丢失数据。
但是它是用户指定的宿主机目录,而不是 Docker 管理的 Volume。
例如,使用命令:
docker run -d --name nginx \
-v /home/user/nginx/conf:/etc/nginx/conf.d \
nginx
就是 把宿主机上的目录 /home/user/nginx/conf,
挂载到容器里面的目录 /etc/nginx/conf.d
不需要创建 Volume,直接指定宿主机目录即可。
三种方式对比
| 方式 | 是否需要创建volume | 数据在哪里 | 适合 |
|---|---|---|---|
| Volume | 需要 | Docker管理目录 | 数据库、生产数据 |
| Bind Mount | 不需要 | 你指定的宿主机目录 | 配置文件、开发 |
| 容器层 | 不需要 | 容器内部 | 临时文件 |
生产环境中:
- MySQL、Redis、PostgreSQL → 通常用 volume,迁移管理更方便
- Nginx 配置、代码开发目录 → 经常用 bind mount
- 临时缓存 → 容器层即可
构建自己的应用镜像
前面的 nginx、postgres 镜像可以直接从镜像仓库下载。
自己开发的后端项目没有现成镜像,需要通过 Dockerfile 把源代码构建成应用镜像。
下面做一个项目示例。它由三个镜像和三个容器组成:
| 作用 | 镜像 | 容器名 |
|---|---|---|
| 对外入口和反向代理 | nginx:1.30 | nginx |
| 自己开发的 Python 应用 | my-backend:1.0 | backend |
| 保存应用数据 | postgres:17 | postgres |
请求过程如下:
其中只有后端镜像需要自己构建;Nginx 和 PostgreSQL 使用官方镜像。
Nginx 既返回前端页面,也把页面发出的 API 请求转发给后端。
三个容器加入同一个 Docker 网络后,可以直接用容器名互相访问。
包括如下步骤,为了给大家节省时间,我已经把这些都打包做好了。
点击这里下载项目目录压缩包,解压复制到宿主机合适的目录下。
1. 准备项目目录和后端
项目目录结构如下:
docker-demo1/
├── backend/
│ ├── app.py
│ ├── requirements.txt
│ ├── Dockerfile
│ └── .dockerignore
├── frontend/
│ └── index.html
└── nginx/
└── default.conf
backend/app.py 是一个最小的笔记接口服务。具体内容见下载项目里面对应的文件。
应用启动时会在 PostgreSQL 中创建数据表。
backend/requirements.txt:
fastapi
uvicorn[standard]
psycopg[binary]
其中 psycopg 是 Python 连接 PostgreSQL 的驱动。
2. 编写 Dockerfile,构建应用镜像
在 backend 目录中创建 Dockerfile:
# 使用已经包含 Python 运行环境的基础镜像
FROM python:3.13-slim
# 后续命令都在容器的 /app 目录中执行
WORKDIR /app
# 先复制并安装依赖,便于 Docker 复用构建缓存
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# 再复制项目代码
COPY . .
# 说明应用在容器内监听 8000 端口
EXPOSE 8000
# 容器启动时运行后端服务
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
Dockerfile 是构建镜像的说明书,其中常用指令的含义是:
FROM:指定基础镜像WORKDIR:设置容器内的工作目录COPY:把构建目录中的文件复制到镜像RUN:在构建镜像时执行命令EXPOSE:说明应用监听的容器端口,这个端口对同一个 Docker 网络内的其他容器可见CMD:指定容器启动时默认执行的命令
再创建 backend/.dockerignore,避免把无关文件、开发环境和敏感配置复制进镜像。
✳️ 构建命令
进入 backend 目录,构建自己的应用镜像:
cd docker-demo1/backend
docker build -t my-backend:1.0 .
cd ..
其中:
-t my-backend:1.0:给镜像设置名称和版本标签- 最后的
.:把当前目录作为构建上下文,Dockerfile 中的COPY只能复制该上下文中的文件
再下载另外两个官方镜像:
docker pull nginx:1.30
docker pull postgres:17
此时执行 docker images,应当可以看到 my-backend:1.0、nginx:1.30 和 postgres:17 三个镜像。
3. 配置 Nginx
项目前端页面文件 frontend/index.html,把 JavaScript 直接写在 HTML 中。
具体内容见下载项目里面对应的文件。主要功能是:
-
页面加载时,JavaScript 会请求
GET /api/notes并显示已有数据; -
提交表单时,会请求
POST /api/notes添加数据。 -
列表内容通过
textContent写入页面,避免把用户输入当成 HTML 执行。
我们需要把配置Nginx,设置 前端页面的访问 和 后端 API 的访问 。
需要自己写一个配置文件,就是项目目录里面的 nginx/default.conf。
server {
listen 80;
# 前端静态文件在容器内的位置
root /usr/share/nginx/html;
index index.html;
# 浏览器访问 / 时返回前端页面
location / {
try_files $uri $uri/ /index.html;
}
# 把 /api/ 请求转发给后端容器
location /api/ {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
root /usr/share/nginx/html 指定前端文件的位置;
backend 是稍后创建的后端容器名。
proxy_pass 末尾没有 /,所以浏览器请求 /api/notes 时,后端收到的路径仍然是 /api/notes。
4. 创建/运行 三个容器
删除掉前面创建的 容器,确保不冲突。
✳️ 创建三个容器共用的网络
前面已经创建过容器网络 app-net,直接复用即可, 如果没有创建过,可以执行:
docker network create app-net
✳️ 创建 PostgreSQL 使用的 Volume
前面的 Volume 章节已经创建了 pgdata,这里直接复用它,不需要再创建新的 Volume。
✳️ 创建/运行 三个容器
先启动 PostgreSQL 容器:
docker run -d \
--name postgres \
--network app-net \
-e POSTGRES_USER=app \
-e POSTGRES_PASSWORD=123456 \
-e POSTGRES_DB=appdb \
-v pgdata:/var/lib/postgresql/data \
--health-cmd="pg_isready -U app -d appdb" \
--health-interval=5s \
--health-timeout=5s \
--health-retries=10 \
postgres:17
这里的密码只是为了让示例容易运行,实际项目应使用更安全的密码,并通过环境变量文件或密钥管理工具注入。
PostgreSQL 只会在数据目录为空时读取 POSTGRES_USER、POSTGRES_PASSWORD 和 POSTGRES_DB 来初始化数据库。前面的示例使用的正是相同的 app、123456 和 appdb,所以可以安全复用 pgdata。如果你的 pgdata 曾被用其他账号或数据库名初始化,应改用新的 Volume,或先备份后清空该 Volume。
执行下面的命令检查数据库状态:
docker ps
等 postgres 容器的 STATUS 出现 (healthy) 后,再启动后端容器:
docker run -d \
--name backend \
--network app-net \
-e DATABASE_URL=postgresql://app:123456@postgres:5432/appdb \
my-backend:1.0
连接地址中的 postgres 是数据库容器名,不是 localhost。在容器内,localhost 只表示当前容器本身。
最后在 docker-demo1 目录启动 Nginx 容器:
docker run -d \
--name nginx \
--network app-net \
-p 8080:80 \
--mount type=bind,source="$(pwd)/nginx/default.conf",target=/etc/nginx/conf.d/default.conf,readonly \
--mount type=bind,source="$(pwd)/frontend",target=/usr/share/nginx/html,readonly \
nginx:1.30
这里只把 Nginx 的端口映射到宿主机。后端和数据库不需要直接暴露给外部,它们通过 app-net 网络通信。
此时执行 docker ps,应当看到三个正在运行的容器:
nginx nginx:1.30 0.0.0.0:8080->80/tcp
backend my-backend:1.0 8000/tcp
postgres postgres:17 5432/tcp
✳️ 验证三个容器协同工作
用浏览器访问 http://服务器IP:8080/index.html,
页面会自动查询已有笔记,也可以在输入框中添加笔记。
这次请求依次经过 Nginx、自己构建的后端应用和 PostgreSQL。只要能够查询到刚添加的数据,就说明三个容器已经配合工作。
当项目代码或依赖发生变化后,需要重新执行 docker build 生成新镜像,再用新镜像重新创建后端容器。
其实现在我们基本都不需要自己从头来写 Dockerfile,可以让 AI coding Agent 帮我们写。
提示词类似:
基于本项目,请帮我写一个 Dockerfile,用于构建一个 Python 后端应用。
Python 版本设定为 3.13,使用 slim 版本基础镜像。
Docker Compose
为什么需要 Docker Compose
一个项目往往需要运行多个容器,例如:
docker run nginx
docker run postgres
docker run redis
docker run your-backend-service
这时候需要管理:
- 容器启动顺序
- 网络连接
- 配置
- 数据卷
- 环境变量
- 重启策略
- 服务发现
这种手工一个一个运行的方式,容易出错,而且不方便管理。
这时候,就适合使用 Docker Compose 来定义并编排多个容器。
Docker Compose 属于容器编排(Container Orchestration),但它是轻量级的、单机级别的容器编排工具。
严格来说,行业里说“容器编排”时,更多指的是 Kubernetes 这一类大规模集群管理系统。这不在本文学习范围内。
只需要一个Compose 配置文件:
compose.yml
这样一次启动就可以了:
docker compose up
以前的 Docker Compose V1 使用的配置文件名是 docker-compose.yml,
而现在 Docker Compose V2 使用的配置文件名是 compose.yml。
V2 兼容 V1,也可以使用文件名 docker-compose.yml。
Docker Compose 配置文件可以非常简单,
比如部署一个 Nginx 服务, 项目目录demo里面创建配置文件 compose.yml,内容如下:
services:
nginx:
image: nginx:1.30
ports:
- "8080:80"
restart: always
这里:
image:指定使用的镜像,nginx:1.30表示使用 nginx 1.30 版本镜像ports:端口映射,"8080:80"表示把宿主机的 8080 端口映射到容器的 80 端口restart:重启策略,always表示容器退出后总是重启,除非手动停止。
这样就可以启动:
docker compose up -d
这里,执行时, Docker Compose 会自动检查本机是否存在对应nginx 1.30 版本镜像。
如果不存在,会自动执行 pull拉取 nginx 1.30 版本镜像,创建并启动一个容器, 并把宿主机的 8080 端口映射到容器的 80 端口。
查看:
docker ps
访问:
http://服务器IP:8080
部署示例
还是以前面 docker-demo1 项目为例,演示如何使用 Compose 来部署。
先删除前面手工创建的三个容器,避免和 Compose 创建的容器重复提供相同服务:
docker rm -f nginx backend postgres
docker rm -f 对正在运行的容器会先停止再删除。
前面手工创建的 app-net 网络和 pgdata Volume 不会被 Compose 使用,保留它们不影响后续操作;不需要时也可以删除:
docker network rm app-net
docker volume rm pgdata
不需要新建项目目录,直接在上一节的 docker-demo1 项目目录中增加一个 compose.yml,
用 Compose 统一完成构建、网络、启动顺序、数据持久化和重启策略。
compose.yml 内容示例如下:
# 项目名:决定 Compose 创建的容器、网络、Volume 的名字前缀
name: docker-demo1
services:
postgres:
image: postgres:17
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: 123456
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 5s
timeout: 5s
retries: 10
restart: always
backend:
# Compose 启动服务前,使用 ./backend/Dockerfile 构建后端镜像。
build:
context: ./backend
dockerfile: Dockerfile
image: my-backend:1.0
environment:
DATABASE_URL: postgresql://app:123456@postgres:5432/appdb
expose:
- "8000"
depends_on:
postgres:
condition: service_healthy
restart: always
nginx:
image: nginx:1.30
ports:
- "80:80"
volumes:
# 项目专用的 Nginx 配置
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
# 前端静态文件目录
- ./frontend:/usr/share/nginx/html:ro
depends_on:
- backend
restart: always
# 顶层声明:告诉 Compose 本项目需要哪些 Volume
volumes:
pgdata:
其中
-
backend.build表示:Compose 启动服务前,使用./backend/Dockerfile构建后端镜像。 -
expose和ports的区别是:expose只是说明容器内应用使用的端口,并不会自动把端口开放给宿主机;而ports会把容器端口映射到宿主机端口。同一个 Compose 项目中的服务会自动加入同一个默认网络,所以:
- Nginx 可以通过
backend:8000访问后端 - 后端可以通过
postgres:5432访问数据库 - 容器之间应该使用服务名通信,不要使用
localhost
- Nginx 可以通过
-
environment中的DATABASE_URL是完整的 PostgreSQL 连接地址。其中主机名使用 Compose 服务名postgres,不能写成localhost。 -
PostgreSQL 的
healthcheck使用pg_isready检查数据库是否已经可以接受连接;后端的depends_on.condition: service_healthy会让 Compose 等数据库健康后再启动后端。 -
PostgreSQL 的数据保存在名为
pgdata的 Volume 中。服务配置中写的是pgdata:/var/lib/postgresql/data,注意开头不是./,所以这是 Volume 挂载,而不是 bind mount。Compose 还要求在配置文件顶层用
volumes:声明本项目使用的 Volume。第一次启动时,Compose 会自动创建名为docker-demo1_pgdata的 Volume,即自动加上项目名前缀。项目名由配置文件顶层的
name: docker-demo1指定。如果不写name,Compose 会默认使用项目目录名作为项目名。显式写出来更稳妥:把项目目录改名或复制到别的目录后,项目名不变,创建出来的容器、网络和 Volume 的名字也就不变,不会因为目录名变化而和已有资源失去关联。注意 Compose 不会复用前面手工创建的
pgdataVolume,而是创建自己的docker-demo1_pgdata。这样 Compose 启动的 PostgreSQL 会从全新的空库开始初始化,和前面手工示例保存在pgdata中的数据互不干扰。 -
Nginx 的
depends_on表示先启动后端服务,但不负责检查后端是否已经可以处理请求;需要严格检查服务状态时,也应该为后端配置healthcheck。 -
Nginx 服务挂载了两个文件路径:
./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro:挂载项目专用的 Nginx 配置./frontend:/usr/share/nginx/html:ro:把前端静态文件目录挂载到 Nginx 容器中
两个挂载都使用
:ro,表示容器只能读取,不能修改宿主机文件。
和上一节的手工启动方式相比,Compose 把所有运行参数都保存在一个文件中:
| 上一节的手工操作 | Compose 的处理方式 |
|---|---|
手工创建 app-net 网络 | 自动创建项目默认网络 |
| 手工创建并挂载数据卷 | 根据 volumes 配置自动创建并挂载项目专属 Volume |
| 等数据库健康后再启动后端 | 通过 healthcheck 和 depends_on 控制 |
分别执行三条 docker run | 执行一条 docker compose up |
| 分别设置环境变量、端口和挂载 | 在 compose.yml 中集中声明 |
| 容器异常退出后需要手工处理 | 通过 restart 设置重启策略 |
因此,应用代码和服务之间的调用关系没有变化,变化的只是容器的管理方式。
在 docker-demo1 目录启动全部服务:
docker compose up -d --build
--build 会在启动前构建后端镜像。以后修改了后端代码或依赖,也可以再次执行这条命令重新构建并启动。
用浏览器访问:
http://服务器IP
Nginx 会直接返回 frontend/index.html。页面打开后:
- JavaScript 自动调用查询接口,显示 PostgreSQL 中已有的笔记
- 在输入框中输入内容并点击“添加”
- JavaScript 调用添加接口,把数据写入 PostgreSQL
- 添加成功后再次查询,并刷新页面中的笔记列表
请求过程如下:
浏览器加载页面
|
| GET /
↓
Nginx 返回 index.html
浏览器中的 JavaScript
|
| GET /api/notes 或 POST /api/notes
↓
Nginx 转发给 backend:8000
|
↓
后端查询或写入 PostgreSQL
数据库数据保存在 docker-demo1_pgdata Volume 中,实际存放在宿主机的 /var/lib/docker/volumes/docker-demo1_pgdata/_data 目录。所以重新创建容器或刷新页面后,已经添加的数据仍然存在。
查看本项目创建的 Volume:
docker volume ls
可以看到:
local docker-demo1_pgdata
示例中的数据库密码仅用于演示。生产环境不应把密码直接写进版本库,应通过安全的环境变量或密钥管理方案提供。
为了让示例尽量简单,这里在应用启动时使用 CREATE TABLE IF NOT EXISTS 自动建表。真实项目通常应该使用 Alembic、Django Migration 等数据库迁移工具管理表结构。
常用 Compose 命令
上面的示例中已经用到了 docker compose up,Compose 的其他操作也都有对应命令。
它们和前面的 docker run、docker ps、docker logs 等命令是并行的两套:前者操作整个项目,后者操作单个容器。
常用命令如下:
# 在前台创建并启动全部服务(可看到日志,Ctrl + C 停止)
docker compose up
# 在后台创建并启动全部服务
docker compose up -d
# 启动前先重新构建镜像(修改代码或依赖后使用)
docker compose up -d --build
# 查看本项目的容器状态
docker compose ps
# 查看全部服务的日志,-f 实时滚动
docker compose logs
docker compose logs -f
# 只看某个服务的日志
docker compose logs -f backend
# 停止全部服务,容器保留
docker compose stop
# 再次启动已停止的服务
docker compose start
# 重启全部服务
docker compose restart
# 停止并删除容器和网络(Volume 保留)
docker compose down
# 停止并删除容器、网络和 Volume(数据会丢失,谨慎使用)
docker compose down -v
其中最需要注意 stop、down 的区别:
执行 docker compose down -v 会在停止并删除容器的同时删除本项目创建的 Volume,数据库数据会一起丢失。
只是停止并重建容器(比如 docker compose down 之后再 docker compose up -d)不会影响 Volume 中的数据。
| 命令 | 容器 | 网络 | Volume |
|---|---|---|---|
docker compose stop | 停止,保留 | 保留 | 保留 |
docker compose down | 停止并删除 | 删除 | 保留 |
docker compose down -v | 停止并删除 | 删除 | 删除 |
down 删除的是 Compose 创建的容器和网络,配置都在 compose.yml 中,所以之后随时可以再 up 重建,非常方便;只有 Volume 中的数据删了就找不回来。
✳️ 验证配置
修改 compose.yml 之后,可以先用下面的命令检查语法并展开最终的配置,不必真的启动:
docker compose config
YAML 缩进错误、引用了未声明的 Volume 等问题,都能在这一步提前发现。
AI 生成 Compose 配置
真实项目的技术栈、启动命令、端口和目录结构各不相同。
如果项目已经有代码,可以让 AI Coding Agent(例如 Codex/OpenCode )先检查整个项目,
再创建与项目匹配的 Dockerfile、.dockerignore、compose.yml 和 Nginx 配置,而不必从空白文件开始手写。
使用时,先在 Codex 中打开项目根目录,然后可以输入下面这样的任务说明:
请先检查当前项目的目录结构、依赖文件、启动命令和已有部署配置,
然后为这个项目创建可用于生产部署的 Docker Compose 配置。
要求:
- 不要凭空假设技术栈,只配置项目实际需要的服务。
- 如果应用缺少 Dockerfile 和 .dockerignore,请一起创建。
- 创建 compose.yml,并正确设置:
- 服务的构建目录和启动命令
- 容器内部端口及必要的宿主机端口
- 服务之间通过 Compose 服务名通信
- 数据库等有状态服务的持久化 Volume
- 合适的 restart 策略和 healthcheck
- 如果项目包含前端静态文件和后端 API,请创建 Nginx 配置:
- 静态资源由 Nginx 直接提供
- /api/ 请求转发到后端服务
- 不要把密码、Token、私钥或真实 .env 内容写进镜像及版本库。
请创建 .env.example,并在 compose.yml 中引用环境变量。
- 固定基础镜像的明确版本,不要直接使用 latest。
- 保留已有配置,不要修改无关的业务代码;如确实需要修改,请先说明原因。
- 生成后执行 docker compose config 检查配置。
如果当前环境能够运行 Docker,再构建镜像并启动服务,
检查容器状态和日志;不要部署到远程生产服务器。
- 最后列出创建或修改的文件、启动命令、验证结果以及仍需我确认的配置。
为了让生成结果更准确,还可以在任务说明中补充:
- 部署环境,例如 Ubuntu 服务器、CPU 架构和开发/生产环境
- 对外开放的端口和域名
- 前端构建产物所在目录
- 后端实际监听的地址和端口
- 数据库、Redis 等服务是使用容器还是外部服务器
- 哪些目录或数据需要持久化
- 环境变量名称,但不要直接发送真实密码和密钥
AI Coding Agent 生成配置后,仍然需要检查:
# 展开并检查最终的 Compose 配置
docker compose config
# 构建项目镜像
docker compose build
# 启动服务
docker compose up -d
# 查看容器状态
docker compose ps
# 查看最近的日志
docker compose logs --tail=100
重点确认对外端口、环境变量、Volume 挂载目录、数据库备份方式和 Nginx 路由是否符合实际项目。
AI 生成的配置应该作为经过检查和测试的项目代码使用,不能未经验证就直接部署到生产服务器。
内网混合部署
前面的部署示例中,backend 镜像是在目标服务器上现场构建的。
但是很多企业的生产服务器部署在内网,网络受限:
- 自己项目构建的私有镜像,不可能推送到 Docker Hub 这样的公共仓库,内网也未必建设了 Harbor 这类私有镜像仓库
- 目标服务器往往也不适合现场执行
docker build:需要准备构建环境,构建过程不可控,还可能把源代码泄露到客户环境
这时有一个非常经典的混合部署方案:
- ✅ 自己项目构建的私有镜像:用
docker save导出为 tar 文件,随安装包一起拷贝到目标服务器,再用docker load导入 - ✅ Nginx、PostgreSQL、Redis 这类公有官方镜像:不放进 tar 包,部署时让服务器联网从公共镜像仓库拉取
这样安装包不用携带几百 MB 的官方镜像,体积小、更新方便;私有镜像又不依赖任何镜像仓库。
✳️ 1. 在构建机器上导出私有镜像
前面的构建机器上已经构建好了 my-backend:1.0,执行:
docker save -o my-backend-1.0.tar my-backend:1.0
其中:
-o指定导出的 tar 文件名- 镜像的名称和标签会一起保留在 tar 文件中,导入后仍然是
my-backend:1.0
✳️ 2. 准备安装包
安装包就是去掉构建环境的项目目录,加上导出的镜像 tar 文件:
docker-demo1-release/
├── compose.yml # 修改:backend 去掉 build,直接使用 image
├── my-backend-1.0.tar # 私有镜像
├── frontend/
│ └── index.html
└── nginx/
└── default.conf
compose.yml 中的 backend 服务去掉 build 配置,改为直接使用镜像:
backend:
# 部署时不再现场构建,直接使用 docker load 导入的镜像
# build:
# context: ./backend
# dockerfile: Dockerfile
image: my-backend:1.0
environment:
DATABASE_URL: postgresql://app:123456@postgres:5432/appdb
expose:
- "8000"
depends_on:
postgres:
condition: service_healthy
restart: always
其余服务(nginx、postgres)保持不变。
✳️ 3. 在目标服务器上导入镜像并启动
把安装包拷贝到目标服务器,先导入私有镜像:
docker load -i my-backend-1.0.tar
查看:
docker images
能看到 my-backend:1.0,说明导入成功。
然后在项目目录中启动:
docker compose up -d
执行过程如下:
docker compose up -d
|
| 本地已存在 my-backend:1.0
| → 直接使用,不会拉取
|
| 本地不存在 nginx:1.30、postgres:17
↓
自动从公共镜像仓库拉取(可配置前面讲过的镜像加速器)
也就是说:Compose 发现本地已经存在 image 指定的镜像,就直接使用;只有本地不存在的镜像才会联网拉取。这正是这个混合方案能工作的原因。
这个方案的前提是:目标服务器可以访问公共镜像仓库(或镜像加速器)。
如果目标服务器完全断网,就只能把所有镜像(包括官方镜像)都 docker save 进 tar 包一起带走。
另外注意:
- 导出镜像的 CPU 架构要和目标服务器一致,比如 x86_64 服务器应使用
amd64架构镜像,否则容器无法运行 compose.yml中镜像要使用明确的版本号,保证和docker load导入的镜像标签一致,Compose 才不会因为标签不匹配而重新拉取
实战项目
实战班的学员,以我们前面锻炼过的 白月药品订单管理系统 为例,使用 Docker Compose 部署这个系统到虚拟机服务器。
Docker 清理
随着 Docker 使用时间增加,会产生大量无用资源,例如停止的容器、未使用的镜像、构建缓存等,可以定期清理。
✳️ 1. 查看 Docker 资源占用
查看当前 Docker 磁盘使用情况:
docker system df
示例输出:
TYPE TOTAL ACTIVE SIZE
Images 10 5 3.2GB
Containers 8 3 500MB
Local Volumes 5 2 1GB
Build Cache 20 0 2GB
可以查看:
- 镜像占用空间
- 容器占用空间
- Volume 数据占用空间
- 构建缓存占用空间
✳️ 2. 清理无用资源
执行:
docker system prune
Docker 会提示确认:
WARNING! This will remove:
- stopped containers
- unused networks
- dangling images
- unused build cache
确认后:
y
即可清理。
✳️ 3. 注意事项
docker system prune 默认会删除:
✅ 已停止的容器 ✅ 未使用的网络 ✅ 悬空镜像(dangling images) ✅ 无用的构建缓存
不会删除:
❌ 正在运行的容器 ❌ 正在使用的镜像 ❌ Docker Volume 数据
✳️ 4. 深度清理(谨慎使用)
如果需要删除更多未使用资源:
docker system prune -a
区别:
docker system prune
只删除:
dangling images
例如:
<none>:<none>
而:
docker system prune -a
会删除:
所有没有被容器使用的镜像
例如:
nginx:latest
postgres:17
如果当前没有容器使用,也会被删除。
✳️ 5. 删除 Volume(高风险)
如果执行:
docker system prune --volumes
会额外删除:
未使用的 Docker volumes
注意:
Volume 里面通常存储数据库数据,例如:
PostgreSQL
MySQL
Redis
删除后可能导致数据丢失。
✳️ 常用组合
开发环境:
docker system prune -a
生产环境:
docker system prune
清理前建议先查看:
docker system df
确认空间占用后再执行。---
这样更适合作为 Docker 入门/运维教程,因为原版本里“无用镜像”这个说法容易让新人误以为所有没运行的镜像都会被删除。
实际 docker system prune 默认只清理 dangling images(悬空镜像)。