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

结果:

开发环境能运行,但是部署后:

服务器运行失败

原因:

这就是软件开发中的经典问题: 环境不一致问题。


✳️ 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/DebianWindows安装 使用 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 官方仓库来获得:

✳️ 创建 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 官方仓库:

curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
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 系统,替换上述命令中的 debianubuntu 即可。

最后再执行更新:

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 后台服务。

它负责:

可以理解为:

Docker 的管理员和执行者。


✳️ Image(镜像)

镜像就是创建容器的应用模板。

镜像中包含应用运行所需要的程序、依赖库和相关配置。

里面包含:

例如:

nginx镜像 = Debian + Nginx程序  + 配置文件

✳️ Container(容器)

镜像运行起来后的实例。

首先需要有一个镜像,然后通过镜像创建容器。

一句话总结:

Client 发命令,Daemon 执行管理,Image 是模板,Container 是运行中的实例。

这就是 Docker 最核心的工作流程。

国内拉取镜像加速

执行下面的命令时:

docker pull nginx

Docker 默认会从 Docker Hub 拉取镜像。中国大陆服务器访问 Docker Hub 时, 可能因为网络线路原因速度较慢,甚至出现超时或下载失败。

这里说的镜像加速器,加速的是 docker pulldocker rundocker 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镜像

✳️ 生产环境建议

如果是个人学习可以用上面的加速器。

如果企业服务器,更推荐:

  1. 使用云厂商自己的镜像仓库:

例如:

  1. 自建 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 是两个参数:


另外,不是所有镜像都有 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 端口

含义:

这样,外网用户访问:

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 提供了多种方式让容器访问持久化数据, 主要是下面三种:

  1. 容器自己的文件系统(不推荐保存重要数据)
  2. Docker Volume(Docker 管理的持久化存储)
  3. 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不需要你指定的宿主机目录配置文件、开发
容器层不需要容器内部临时文件

生产环境中:

构建自己的应用镜像

前面的 nginxpostgres 镜像可以直接从镜像仓库下载。

自己开发的后端项目没有现成镜像,需要通过 Dockerfile 把源代码构建成应用镜像。

下面做一个项目示例。它由三个镜像和三个容器组成:

作用镜像容器名
对外入口和反向代理nginx:1.30nginx
自己开发的 Python 应用my-backend:1.0backend
保存应用数据postgres:17postgres

请求过程如下:

其中只有后端镜像需要自己构建;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 是构建镜像的说明书,其中常用指令的含义是:

再创建 backend/.dockerignore,避免把无关文件、开发环境和敏感配置复制进镜像。


✳️ 构建命令

进入 backend 目录,构建自己的应用镜像:

cd docker-demo1/backend
docker build -t my-backend:1.0 .
cd ..

其中:

再下载另外两个官方镜像:

docker pull nginx:1.30
docker pull postgres:17

此时执行 docker images,应当可以看到 my-backend:1.0nginx:1.30postgres:17 三个镜像。

3. 配置 Nginx

项目前端页面文件 frontend/index.html,把 JavaScript 直接写在 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_USERPOSTGRES_PASSWORDPOSTGRES_DB 来初始化数据库。前面的示例使用的正是相同的 app123456appdb,所以可以安全复用 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

这里:

这样就可以启动:

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:

其中

和上一节的手工启动方式相比,Compose 把所有运行参数都保存在一个文件中:

上一节的手工操作Compose 的处理方式
手工创建 app-net 网络自动创建项目默认网络
手工创建并挂载数据卷根据 volumes 配置自动创建并挂载项目专属 Volume
等数据库健康后再启动后端通过 healthcheckdepends_on 控制
分别执行三条 docker run执行一条 docker compose up
分别设置环境变量、端口和挂载compose.yml 中集中声明
容器异常退出后需要手工处理通过 restart 设置重启策略

因此,应用代码和服务之间的调用关系没有变化,变化的只是容器的管理方式。

docker-demo1 目录启动全部服务:

docker compose up -d --build

--build 会在启动前构建后端镜像。以后修改了后端代码或依赖,也可以再次执行这条命令重新构建并启动。

用浏览器访问:

http://服务器IP

Nginx 会直接返回 frontend/index.html。页面打开后:

  1. JavaScript 自动调用查询接口,显示 PostgreSQL 中已有的笔记
  2. 在输入框中输入内容并点击“添加”
  3. JavaScript 调用添加接口,把数据写入 PostgreSQL
  4. 添加成功后再次查询,并刷新页面中的笔记列表

请求过程如下:

浏览器加载页面
      |
      | 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 rundocker psdocker 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

其中最需要注意 stopdown 的区别:

执行 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、.dockerignorecompose.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,再构建镜像并启动服务,
  检查容器状态和日志;不要部署到远程生产服务器。
- 最后列出创建或修改的文件、启动命令、验证结果以及仍需我确认的配置。

为了让生成结果更准确,还可以在任务说明中补充:

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 镜像是在目标服务器上现场构建的。

但是很多企业的生产服务器部署在内网,网络受限:

这时有一个非常经典的混合部署方案

这样安装包不用携带几百 MB 的官方镜像,体积小、更新方便;私有镜像又不依赖任何镜像仓库。


✳️ 1. 在构建机器上导出私有镜像

前面的构建机器上已经构建好了 my-backend:1.0,执行:

docker save -o my-backend-1.0.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

其余服务(nginxpostgres)保持不变。


✳️ 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 包一起带走。

另外注意:

实战项目

实战班的学员,以我们前面锻炼过的 白月药品订单管理系统 为例,使用 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

可以查看:



✳️ 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(悬空镜像)