前言

借助 Docker,您可将容器当做轻巧、模块化的虚拟机来使用。同时,您还将获得高度灵活性,实现对容器的高效创建、部署及复制,并在环境之间迁移它们,从而有助于您针对云来优化应用。

——《什么是 Docker?》

Docker Compose is a tool for running multi-container applications on Docker defined using the Compose file format. A Compose file is used to define how one or more containers that make up your application are configured. Once you have a Compose file, you can create and start your application with a single command: docker compose up

——《Docker Compose 官方介绍》

简单来说,Docker可以让我们把一个项目的所有依赖环境配置好,我们可以快速的运行起来,而无需处理环境的依赖问题;

而某个项目需要用到诸如数据库、Redis等其他项目的时候,使用Docker Compose可以将所有的项目和项目依赖通过一个yml完全配置好,我们只需要通过一行命令就可以快速启动这个项目。

Compose 使用的三个步骤:

  • 使用 Dockerfile 定义应用程序的环境。
  • 使用 docker-compose.yml 定义构成应用程序的服务,这样它们可以在隔离环境中一起运行。
  • 最后,执行 docker-compose up 命令来启动并运行整个应用程序。

——《Docker Compose - 菜鸟教程》

安装 Docker Compose

这部分,我们在这里仅介绍Linux的安装步骤,Windows和MacOS已经在Docker Desktop之中内置了Docker Compose的功能,如果你有其他特殊的需求,也可以参考官方文档。

基本上来说,只需要这两条命令即可:

1
2
sudo curl -L "https://github.com/docker/compose/releases/download/<版本号>/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

如果你使用的是Ubuntu系统,那么安装的步骤则会变得更为简单:

1
sudo apt install docker-compose

在安装完成后,我们可以通过下面的命令检查是否安装成功

1
docker compose --version

配置文件

接下来,我们聊聊配置文件,也就是Docker Compose的核心部分,在这里我们设计了两个服务,一种为源文件构造,另一种是拉取现有的镜像部署。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# compose.yml 
services: # 定义服务
web: # Web 应用
build: ./app # 使用 ./app 目录下的 Dockerfile 构建镜像
command: flask run --host=0.0.0.0 --port=5000 # 启动 Flask 应用
ports:
- "8000:5000" # 本机8000端口映射到容器5000端口
environment:
# 数据库连接URL,这里通过服务名 db 连接 Postgres
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
depends_on:
db:
condition: service_healthy # 等待数据库健康检查通过再启动

db: # 数据库服务(PostgreSQL)
image: postgres:16 # 使用官方 Postgres 16 镜像
restart: unless-stopped # 意外退出时自动重启
environment:
POSTGRES_USER: postgres # 数据库用户名
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} # 从 .env 文件读取密码
POSTGRES_DB: ${POSTGRES_DB} # 默认数据库名
volumes:
- db_data:/var/lib/postgresql/data # 数据持久化,保存到数据卷 db_data 之中
healthcheck: # 健康检查,保证数据库可用
test: ["CMD-SHELL", "pg_isready -U postgres -d ${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 10

volumes: # 定义数据卷
db_data: # PostgreSQL 的数据卷

Service - Web服务(从源文件构造)

我们以Web服务为例,这里我们设计了一个Python Flask项目作为示例:

1
2
3
4
5
6
7
8
9
10
11
web:           # Web 应用服务(Flask 示例)
build: ./app # 使用 ./app 目录下的 Dockerfile 构建镜像
command: flask run --host=0.0.0.0 --port=5000 # 启动 Flask 应用
ports:
- "8000:5000" # 本机8000端口映射到容器5000端口
environment:
# 数据库连接URL,这里通过服务名 db 连接 Postgres
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
depends_on:
db:
condition: service_healthy # 等待数据库健康检查通过再启动
  • build:指定构建镜像的目录(./app 里需要有 Dockerfile)。
  • command:覆盖容器启动命令,这里用flask启动了一个允许所有IP访问的,端口为5000的服务端。

注意:这里应该允许所有IP访问(0.0.0.0),而不是只允许本地访问(127.0.0.1),否则会导致无法从外部连接到容器。

  • ports:端口映射,本机 8000 → 容器 5000。
  • env_file:从 .env 文件读取环境变量。
  • environment:额外定义的环境变量,在这里我们设置了数据库地址。
  • depends_on:表示依赖关系,这里web服务要等到db服务通过了健康检查,才会启动。

Services - db服务(拉取现有的镜像)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
db:            # 数据库服务(PostgreSQL)
image: postgres:16 # 使用官方 Postgres 16 镜像
restart: unless-stopped # 意外退出时自动重启
environment:
POSTGRES_USER: postgres # 数据库用户名
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} # 从 .env 文件读取密码
POSTGRES_DB: ${POSTGRES_DB} # 默认数据库名
volumes:
- db_data:/var/lib/postgresql/data # 数据持久化,保存到卷 db_data
healthcheck: # 健康检查,保证数据库可用
test: ["CMD-SHELL", "pg_isready -U postgres -d ${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 10
  • image:使用构建好的镜像。这里我们固定了版本为16,如果使用latest可能会导致新旧版本的兼容性导致问题。
  • restart:容器异常退出后会自动重启。
  • environment:POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB 仅在第一次初始化数据目录时生效。之后数据由卷持久化,再改这些变量不会重置已有库。
  • volumes:用 命名卷(db_data)作为文件的存储,后面说映射到容器内的存储目录。
  • healthcheck
    • CMD-SHELL:表示这条命令会在容器的 shell 里执行(相当于 bash -c)。
    • pg_isready:这是 PostgreSQL 自带的一个小工具,用来检测数据库是否可以连接。
    • -U postgres:指定用 postgres 这个用户去检测。
    • -d ${POSTGRES_DB}:指定要检测的数据库名字(这里我们使用的是变量,会从 .env 文件读取同名的环境变量值)。

数据卷

在 Docker Compose 中,数据卷的定义通常放在最底部的 volumes 字段中。我们可以为每个服务定义一个或多个数据卷,以便在容器之间共享数据或持久化数据。
数据卷无需我们关心文件在机器的存放位置,Docker 会自动处理,而且数据卷也可以方便的在不同的容器之间共享。

1
2
volumes:
db_data: # PostgreSQL 的数据卷

而实际上大多数情况是,一些项目需要大家git clone下来,在仓库的路径进行操作,如果我们希望直接把这个目录挂载到容器中,可以在 volumes 中进行配置。

1
2
3
web:
volumes:
- ./app:/app

上面的配置将宿主机的 ./app 目录挂载到容器的 /app 目录中,这样我们就可以在宿主机上直接修改代码,而容器内的应用会直接使用这个路径里的内容,同步改变的内容。

常用的命令

命令 作用 备注
docker compose up -d 后台启动所有服务 -d 表示 detached 模式,不占用当前终端
docker compose down 停止并清理容器、网络 数据卷默认保留,如果需要一起清理,可以使用docker compose down -v
docker compose ps 查看当前服务运行状态 类似 docker ps,但只显示 Compose 管理的容器
docker compose logs -f 查看日志(实时刷新) -f 类似 tail -f,适合调试
docker compose exec <服务名> bash 进入容器内部 比如:docker compose exec web bash 进入 web 容器

提示:

如果只想启动单个服务,可以用docker compose up -d web来启动。

迁移Docker项目到Docker Compose

接下来我们来尝试一下把现有的Docker项目迁移到Docker Compose,

说白了,其实就是把我们使用的各种参数转换成配置文件中的每一个字段,并且填写进去。

这里附上一份简单的转换表:

docker run Compose 字段 示例
–name app container_name container_name: app
-p 8080:80 ports ports: [“8080:80”]
-v host:ctr[:ro] volumes volumes: [“app_data:/var/lib/app”] 或 [“./cfg:/etc/app:ro”]
-e KEY=VAL environment / .env environment: [“KEY=${KEY}”]
–env-file .env env_file env_file: .env
–restart unless-stopped restart restart: unless-stopped
–network mynet networks networks: [“mynet”]
–health-cmd … healthcheck.test `test: [“CMD-SHELL”,“curl -f http://localhost/health
–cpus/–memory deploy.resources(本地用 deploy 限制有限) deploy: { resources: { limits: { cpus: “1.0”, memory: “512M”} } }
–dns/–add-host dns / extra_hosts extra_hosts: [“db.local:10.0.0.5”]
–log-driver logging.driver logging: { driver: “json-file” }

我们来看一个示例:

1
2
3
4
5
6
7
docker run -d --name db \
-p 5432:5432 \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=devpassword \
-e POSTGRES_DB=mydb \
--restart unless-stopped \
postgres:16

让我们转换成docker compose的配置文件,便得到了如下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
services:
db:
image: postgres:16
container_name: db
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL","pg_isready -U postgres -d ${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 10
ports:
- "5432:5432"
restart: unless-stopped

volumes:
pgdata:

上面我们使用了一些环境变量,在这里我们可以有两种方式使用他们,第一种便是直接写入到配置文件之中,也就是:

1
2
3
environment:
POSTGRES_PASSWORD: devpassword
POSTGRES_DB: mydb

但是这样的话,我们如果把这个配置文件分享给其他人用,或者是上传到Github仓库的时候,那就不是很安全了,所以我们还有第二种方式,也就是上面我使用的方式,我们新建一个.env文件来存储这些变量的值。

.env(与 yml 同目录):

1
2
POSTGRES_PASSWORD=devpassword
POSTGRES_DB=mydb

个人技巧

更新镜像以始终保持最新

在使用LobeChat的时候,我希望保持镜像的最新以体验最新的功能,所以在官方脚本的参考下设计了一个更完善的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#!/usr/bin/env bash
# auto-update.sh

set -euo pipefail

##################### Config #####################
WORK_DIR="${WORK_DIR:-/root/lobe-chat-db}" # docker-compose.yml 所在目录
IMAGE_REPO="${IMAGE_REPO:-lobehub/lobe-chat-database}" # Docker Hub 仓库
IMAGE_TAG="${IMAGE_TAG:-latest}" # 本地使用的 tag(默认 latest)
PAGE_SIZE="${PAGE_SIZE:-100}" # Docker Hub API 拉取的标签数量
SERVICE_NAME="${SERVICE_NAME:-}" # 只重启某个服务(可留空:重启全部)
# 如果你的 compose 文件有多文件合并,按需在这里追加:
COMPOSE_FILES=() # 例:COMPOSE_FILES=(-f docker-compose.yml -f docker-compose.prod.yml)
################### Config ###################

cd "$WORK_DIR" || { echo "❌ 无法进入工作目录:$WORK_DIR"; exit 1; }

need_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "❌ 需要命令:$1"; exit 1; }; }
need_cmd curl
need_cmd jq
need_cmd docker

echo "==> 工作目录:$WORK_DIR"
echo "==> 目标镜像:$IMAGE_REPO:$IMAGE_TAG"

# 读取本地镜像“语义化版本”:
# 1) 优先从 OCI label: org.opencontainers.image.version
# 2) 若读不到且 tag 形如 x.y.z,则使用 tag 作为本地版本
# 3) 否则返回空(后面只按远程检测是否有新层)
get_local_version() {
local label_ver tag_ver
label_ver="$(docker image inspect "${IMAGE_REPO}:${IMAGE_TAG}" 2>/dev/null \
| jq -r '.[0].Config.Labels["org.opencontainers.image.version"] // empty' || true)"
if [[ -n "${label_ver:-}" ]]; then
echo "$label_ver"
return
fi
# 回退:尝试读取本地 tag(仅当 tag 是 x.y.z)
tag_ver="$(docker image inspect "${IMAGE_REPO}:${IMAGE_TAG}" 2>/dev/null \
| jq -r '.[0].RepoTags[]? // empty' | grep -E "^${IMAGE_REPO}:[0-9]+\.[0-9]+\.[0-9]+$" \
| sed -E "s#^${IMAGE_REPO}:##" | sort -V | tail -n1 || true)"
echo "${tag_ver:-}"
}

# 从 Docker Hub API 获取最新的语义化版本(x.y.z)
get_remote_version() {
local api="https://hub.docker.com/v2/repositories/${IMAGE_REPO}/tags/?page_size=${PAGE_SIZE}"
local latest
latest="$(curl -fsSL "$api" \
| jq -r '.results[]?.name' \
| grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \
| sort -V | tail -n1 || true)"
echo "${latest:-}"
}

# 拉取镜像并判断是否真的有更新(不影响正在运行容器)
pull_and_changed() {
local img="$1"
local before after
before="$(docker image inspect "$img" -f '{{.Id}}' 2>/dev/null || true)"
docker pull "$img" >/dev/null
after="$(docker image inspect "$img" -f '{{.Id}}' 2>/dev/null || true)"
if [[ -n "$before" && -n "$after" && "$before" == "$after" ]]; then
return 1 # 未变化
fi
return 0 # 发生变化(有更新或首次拉取)
}

echo "==> 开始检查更新..."
LOCAL_VERSION="$(get_local_version || true)"
REMOTE_VERSION="$(get_remote_version || true)"

echo "本地语义化版本:${LOCAL_VERSION:-<未知>}"
echo "远程最新版本: ${REMOTE_VERSION:-<未知>}"

HAS_UPDATE=0

if [[ -n "${REMOTE_VERSION:-}" && "${REMOTE_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
# 如果本地也有语义化版本,则用 sort -V 比较
if [[ -n "${LOCAL_VERSION:-}" && "${LOCAL_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
# 比较结果:取两者排序后的末位为较大者
BIGGEST="$(printf "%s\n%s\n" "$LOCAL_VERSION" "$REMOTE_VERSION" | sort -V | tail -n1)"
if [[ "$BIGGEST" != "$LOCAL_VERSION" ]]; then
echo "==> 版本比较:远程更高(${LOCAL_VERSION} -> ${REMOTE_VERSION})"
HAS_UPDATE=1
else
echo "==> 版本比较:本地已是最新语义化版本"
fi
else
# 本地没有语义化版本可比,退化为“拉取并对比镜像 ID”的方式
echo "==> 本地缺少语义化版本;改为直接拉取 ${IMAGE_REPO}:${IMAGE_TAG} 并比较镜像 ID"
if pull_and_changed "${IMAGE_REPO}:${IMAGE_TAG}"; then
echo "==> 拉取后镜像发生变化,认为有更新"
HAS_UPDATE=1
else
echo "==> 镜像无变化,认为已是最新"
fi
fi
else
echo "==> 无法获取远程语义化版本;改为直接拉取 ${IMAGE_REPO}:${IMAGE_TAG} 并比较镜像 ID"
if pull_and_changed "${IMAGE_REPO}:${IMAGE_TAG}"; then
echo "==> 拉取后镜像发生变化,认为有更新"
HAS_UPDATE=1
else
echo "==> 镜像无变化,认为已是最新"
fi
fi

if [[ "$HAS_UPDATE" -eq 1 ]]; then
echo "==> 有更新,开始应用..."
# 先确保 yml 中其它镜像也同步最新(可选)
docker compose "${COMPOSE_FILES[@]}" pull || true

# 停止并删除旧容器(保留数据卷)
docker compose "${COMPOSE_FILES[@]}" down

# 以新镜像启动,若指定了 SERVICE_NAME 就只启动该服务
if [[ -n "${SERVICE_NAME}" ]]; then
docker compose "${COMPOSE_FILES[@]}" up -d "$SERVICE_NAME"
else
docker compose "${COMPOSE_FILES[@]}" up -d
fi

echo "==> 清理无用镜像层..."
docker image prune -f >/dev/null || true

# 打印新版本(尽量从 label 读取)
NEW_LOCAL_VERSION="$(get_local_version || true)"
echo "✅ 更新完成:$(date '+%F %T')"
echo "新本地版本:${NEW_LOCAL_VERSION:-<未知>}"
else
echo "✅ 所有内容已是最新,无需重启。"
fi

echo "==> 当前服务状态:"
docker compose "${COMPOSE_FILES[@]}" ps

如果需要适配其他服务,只需要修改WORK_DIRIMAGE_REPO的内容即可,同时IMAGE_TAG可以匹配不同TAG,方便使用。

后记

这篇文章我们只是简单聊了聊Docker Compose的安装、配置文件和常用命令,并没有涉及到一些更深层的内容和配置,如果想要了解更多,你可以参考官方文档和现有的教程。


本站由 Lynn 使用 Stellar 1.33.1 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。