前言
借助 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-composesudo 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 services: web: build: ./app command: flask run --host=0.0.0.0 --port=5000 ports: - "8000:5000" environment: DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} depends_on: db: condition: service_healthy db: image: postgres:16 restart: unless-stopped environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - db_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL" , "pg_isready -U postgres -d ${POSTGRES_DB}" ] interval: 5s timeout: 3s retries: 10 volumes: db_data:
Service - Web服务(从源文件构造)
我们以Web服务为例,这里我们设计了一个Python Flask项目作为示例:
1 2 3 4 5 6 7 8 9 10 11 web: build: ./app command: flask run --host=0.0.0.0 --port=5000 ports: - "8000:5000" environment: 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: image: postgres:16 restart: unless-stopped environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - db_data:/var/lib/postgresql/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 会自动处理,而且数据卷也可以方便的在不同的容器之间共享。
而实际上大多数情况是,一些项目需要大家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 set -euo pipefailWORK_DIR="${WORK_DIR:-/root/lobe-chat-db} " IMAGE_REPO="${IMAGE_REPO:-lobehub/lobe-chat-database} " IMAGE_TAG="${IMAGE_TAG:-latest} " PAGE_SIZE="${PAGE_SIZE:-100} " SERVICE_NAME="${SERVICE_NAME:-} " COMPOSE_FILES=() 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 " 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_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:-} " } 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 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 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 "==> 有更新,开始应用..." docker compose "${COMPOSE_FILES[@]} " pull || true docker compose "${COMPOSE_FILES[@]} " down 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 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_DIR
和IMAGE_REPO
的内容即可,同时IMAGE_TAG
可以匹配不同TAG,方便使用。
后记
这篇文章我们只是简单聊了聊Docker Compose的安装、配置文件和常用命令,并没有涉及到一些更深层的内容和配置,如果想要了解更多,你可以参考官方文档和现有的教程。