Cal.diy Docker Compose 部署指南
用 Docker Compose 部署 Cal.diy v6.2.0:cal.com/cal.com 镜像位甄别(cal.diy 无 tag 坑)、.env 密钥生成、4 服务编排(Web/PostgreSQL/Redis/API v2)、588 个迁移首次启动回显、wait-for-it 与 STRIPE_API_KEY 崩溃循环排查。Docker Engine 29.8.1 逐命令实测。
Cal.diy Docker Compose 部署指南
目标:用 Docker Compose 在单机部署 Cal.diy v6.2.0 自托管预约调度系统,包含 Web 应用、PostgreSQL 数据库、Redis 与 API v2 四个服务,逐命令实测(真实回显),覆盖首次启动迁移、管理员初始化与常见启动失败排查。已在 Docker Engine 29.8.1(Compose v5.5.1)实测通过。
一、环境信息
- 实测服务器:Linux 5.15 内核,x86_64
- Docker Engine 29.8.1,Docker Compose v5.5.1
- 镜像版本:
calcom/cal.com:v6.2.0(Cal.diy 的官方应用发布位,2026-03-02 推送)、postgres:16、redis:7 - 组件版本核实:GitHub Releases 最新稳定版 v6.2.0(2026-03-01 发布,非 Pre-release),查询日期 2026-10-19
- 网络:专用 compose 网络
cal-diy-stack,显式子网10.89.56.0/24 - 宿主端口:
8127(Web 应用)、8128(PostgreSQL 维护口)、8129(API v2) - 资源占用:四个服务合计内存上限 3.6G(db 512m + redis 128m + web 1536m + api 1536m),实测稳态 RSS 约 1.6G
部署前核对宿主端口未被占用:
# ss -ltn | grep -E ':(8127|8128|8129)\s'
(无输出,端口空闲)
创建部署目录:
# mkdir -p /opt/data/cal.diy-v6.2.0 && cd /opt/data/cal.diy-v6.2.0
注:本文实测工作目录为
/opt/data/quzuo/cal.diy-v6.2.0,读者可按自身磁盘规划调整。
二、镜像选型
Cal.diy 的镜像发布位有两个易混淆的名称,2026-10-19 在 Docker Hub 实查结论:
| 仓库 | 结论 |
|---|---|
calcom/cal.com | 官方发布位。5,155,588 pulls,tags 含 latest、v6.2.0、v6.2.0-arm(2026-03-02 更新)。选它。 |
calcom/cal.diy | 仓库存在但 0 个 tag,docker pull calcom/cal.diy 会报 manifest unknown。官方文档指向它与实际发布位不一致,勿用。 |
关于 API v2:官方 Compose 文件中 calcom-api 服务只有 build: 定义(从源码构建),没有对应的预构建镜像发布位。如需 API v2,须从 v6.2.0 源码 tag 自行构建(本章第五节给出实测构建流程);仅用 Web 应用与预约页的话可以完全不部署它。
版本锁定建议:使用 v6.2.0 明确 tag 而非 latest,与《Cal.diy 介绍与选型指南》核实的最新稳定版对应。
三、前置条件
- Docker Engine 与 Compose 插件已安装,且当前用户在 docker 组(或以 root 执行)
- 服务器时间已通过 NTP 校准(预约系统对时间敏感,站内《时间同步与服务》有部署方法)
- 内存 ≥ 2G 空闲
- 如需从 Docker Hub 拉取镜像缓慢,可配置镜像加速或代理:
# export https_proxy=http://192.168.1.62:7897 http_proxy=http://192.168.1.62:7897
四、部署步骤
4.1 编写 .env
创建 /opt/data/cal.diy-v6.2.0/.env(密钥值须替换为自己的随机串,生成命令见 4.2):
POSTGRES_USER=cal_com
POSTGRES_PASSWORD=q1w2e3r4
POSTGRES_DB=cal_com
DATABASE_HOST=database
NEXT_PUBLIC_WEBAPP_URL=http://192.168.146.128:8127
NEXT_PUBLIC_WEBSITE_URL=http://192.168.146.128:8127
NEXTAUTH_URL=http://192.168.146.128:8127
NEXTAUTH_SECRET=EdA+dt9c3AJXpN83ojjsKF1cS+PNSFiPWxHzQUQK3qo=
CALENDSO_ENCRYPTION_KEY=VvTwwmXeLHe4IYqzZ90gzNIc7tbaDxDn
ALLOWED_HOSTNAMES='"192.168.146.128:8127","localhost:3000"'
CALCOM_TELEMETRY_DISABLED=1
JWT_SECRET=(随机生成,API v2 用)
STRIPE_API_KEY=
STRIPE_WEBHOOK_SECRET=
要点:
NEXT_PUBLIC_WEBAPP_URL/NEXTAUTH_URL必须写成访问者实际使用的地址(含端口),否则预约页跳转与登录回调会落到错误域名ALLOWED_HOSTNAMES是 JSON 数组格式的字符串(内层双引号不能省),非标准域名/IP 访问必须配置,否则 403STRIPE_API_KEY值留空即可,但变量必须存在(空值可过启动校验,缺失变量会崩溃循环,见 4.6 与第八章)- 邮箱登录(magic link)依赖 SMTP 配置,本文采用账号密码注册,不需要 SMTP 变量
4.2 生成随机密钥
# openssl rand -base64 32
EdA+dt9c3AJXpN83ojjsKF1cS+PNSFiPWxHzQUQK3qo=
# openssl rand -base64 24
VvTwwmXeLHe4IYqzZ90gzNIc7tbaDxDn
NEXTAUTH_SECRET 与 JWT_SECRET 各生成一个;CALENDSO_ENCRYPTION_KEY 官方文档要求 24 位,可用 openssl rand -base64 24。这两个密钥生成后必须妥善保存,更换会导致全部会话失效、第三方凭据无法解密。
4.3 编写 docker-compose.yml
name: cal-diy
volumes:
database-data:
redis-data:
networks:
stack:
name: cal-diy-stack
ipam:
config:
- subnet: 10.89.56.0/24
services:
database:
container_name: cal-com-6-2-0-db
image: postgres:16
restart: always
volumes:
- database-data:/var/lib/postgresql
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
ports:
- "8128:5432"
networks:
- stack
mem_limit: 512m
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 6
redis:
container_name: cal-com-6-2-0-redis
image: redis:7
restart: always
volumes:
- redis-data:/data
networks:
- stack
mem_limit: 128m
calcom:
container_name: cal-com-6-2-0-web
image: calcom/cal.com:v6.2.0
restart: always
networks:
- stack
ports:
- "8127:3000"
env_file: .env
environment:
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DATABASE_HOST}:5432/${POSTGRES_DB}
- DATABASE_DIRECT_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DATABASE_HOST}:5432/${POSTGRES_DB}
depends_on:
- database
mem_limit: 1536m
与官方 compose 的差异说明(官方 138 行原文件见 https://cal.diy/docker):
- 官方
database卷挂载是database-data:/var/lib/postgresql(含 data 子目录自动继承,勿改为/var/lib/postgresql/data,二者卷升级语义不同,保持官方写法) - 本文增加了显式子网、内存上限、db 维护端口 8128 与 healthcheck,均为运维增强项,不影响官方行为
- PostgreSQL 数据卷在应用外独立成卷,升级 Web 应用版本时数据库结构由启动时的
prisma migrate deploy自动前滚,升级前仍需备份(见《Cal.diy 日常运维与排障手册》)
4.4 启动并核对网络
# docker compose up -d
[+] Running 21/21
✔ calcom Pulled 142.7s
✔ e9a3d6bcbcbf Pull complete
...(共 21 层,略)
✔ 4 Networks Created
✔ Container cal-com-6-2-0-db Started
✔ Container cal-com-6-2-0-redis Started
✔ Container cal-com-6-2-0-web Started
确认网络子网与出网路径:
# docker network inspect cal-diy-stack --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'
10.89.56.0/24
# ip route get 192.168.1.62
192.168.1.62 via 192.168.146.2 dev ens33 src 192.168.146.128
首次启动会拉取镜像并执行数据库迁移,整个 Web 服务就绪约需 1-2 分钟。观察迁移日志:
# docker logs cal-com-6-2-0-web 2>&1 | grep -E "migrat|Applying" | tail -6
Applying migration `20250904135225_add_oauth_access_token_expiry`
Applying migration `20251007125405_add_booking_phone_number`
Applying migration `20251103000000_fix_booking_status_denormalization`
Applying migration `20251201000000_rename_booking_uid_index`
Applying migration `20260205133541_profile_username_migration`
Applying migration `20260218084325_team_user_field_index`
588 migrations found in prisma/migrations
Following migration had more than one parent: 20250902132329_test_single_booking_mode_reminder_emails
...
All migrations have been successfully applied.
(v6.2.0 全量迁移 588 个,空库首次启动约 1-3 分钟执行完毕,以 All migrations have been successfully applied. 为准。)
4.5 验证服务状态
# docker compose ps
NAME IMAGE STATUS
cal-com-6-2-0-web calcom/cal.com:v6.2.0 Up 3 minutes (healthy)
cal-com-6-2-0-db postgres:16 Up 3 minutes (healthy)
cal-com-6-2-0-redis redis:7 Up 3 minutes
# curl -s -o /dev/null -w "%{http_code}\n" http://192.168.146.128:8127/
200
# curl -s http://192.168.146.128:8127/ -L -o /dev/null -w "%{url_effective} -> %{http_code}\n"
http://192.168.146.128:8127/auth/setup?step=1 -> 200
首次访问根路径会 302 到 /auth/setup?step=1 初始化向导,按页面提示设置姓名、用户名、邮箱与密码即完成第一个账号创建。密码策略实测要求:至少 7 位且同时包含大写字母、小写字母与数字(源码 packages/lib/auth/isPasswordValid.ts),例如 Q1w2e3r4。
首个注册用户默认是普通角色,访问管理后台前需在数据库提权(实测复现):
# docker exec cal-com-6-2-0-db psql -U cal_com -d cal_com \
-c "UPDATE users SET role='ADMIN' WHERE id=1 RETURNING id, username, role;"
id | username | role
----+----------+-------
1 | admin | ADMIN
(1 row)
UPDATE 1
提权后重新登录,访问 /settings/admin/flags 可见 Feature flags 管理页即生效。
4.6 常见启动失败与修复
现象 1:Web 容器日志大量 Unknown argument: database
# docker logs cal-com-6-2-0-web 2>&1 | head -20
Usage: /calcom/scripts/wait-for-it.sh host:port [-s] [-t timeout] [-- command args]
-q: silent mode...
Unknown argument: database
v6.2.0 的启动脚本以裸主机名调用 wait-for-it.sh,而该脚本要求 host:port 形式,属上游脚本缺陷。实测影响有限:脚本报错后继续执行迁移,此时 PostgreSQL 通常已就绪,服务正常拉起。若迁移偶发连接失败,重启一次即可(docker compose restart calcom)。
现象 2:API v2 容器崩溃循环 Missing environment variable: STRIPE_API_KEY.
# docker logs cal-com-6-2-0-api 2>&1 | grep -m1 "ExceptionHandler"
{"message":"Missing environment variable: STRIPE_API_KEY.","level":"error",...}
API v2 启动时强制要求 STRIPE_API_KEY、STRIPE_WEBHOOK_SECRET 变量存在(自托管无 Stripe 也要求占位)。在 .env 中补上两个空值变量并 docker compose up -d calcom-api 即可恢复。这是 Cal.diy 社区版最典型的启动坑。
现象 3:访问 403
ALLOWED_HOSTNAMES 未包含访问所用的主机名:端口组合,按 4.1 补齐后重建 Web 容器(docker compose up -d --force-recreate calcom)。
五、部署 API v2(可选)
API v2 是独立 NestJS 服务,官方无预构建镜像,需从源码构建。v6.2.0 源码获取:
# git clone --depth 1 --branch v6.2.0 https://github.com/calcom/cal.diy src
(约 550MB)
构建镜像(实测 168 秒完成,产物 2.38GB 压缩体积):
# docker build -t cal-com-6-2-0-api:v6.2.0 src/apps/api/v2
#14 DONE 168.2s
# docker images cal-com-6-2-0-api
REPOSITORY TAG IMAGE ID CREATED SIZE
cal-com-6-2-0-api v6.2.0 c2d59eb7847c 5 minutes ago 2.38GB
在 docker-compose.yml 追加服务(环境变量与 Web 应用共用 .env):
calcom-api:
container_name: cal-com-6-2-0-api
image: cal-com-6-2-0-api:v6.2.0
restart: always
networks:
- stack
ports:
- "8129:80"
environment:
- API_PORT=80
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DATABASE_HOST}:5432/${POSTGRES_DB}
- DATABASE_DIRECT_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DATABASE_HOST}:5432/${POSTGRES_DB}
- DATABASE_READ_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DATABASE_HOST}:5432/${POSTGRES_DB}
- DATABASE_WRITE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DATABASE_HOST}:5432/${POSTGRES_DB}
- REDIS_URL=redis://redis:6379
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
- JWT_SECRET=${JWT_SECRET}
- CALENDSO_ENCRYPTION_KEY=${CALENDSO_ENCRYPTION_KEY}
- WEB_APP_URL=${NEXT_PUBLIC_WEBAPP_URL}
- STRIPE_API_KEY=${STRIPE_API_KEY}
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
- CALCOM_TELEMETRY_DISABLED=1
depends_on:
- database
- redis
mem_limit: 1536m
启动并验证:
# docker compose up -d calcom-api
# docker logs cal-com-6-2-0-api 2>&1 | tail -3
[PrismaReadService] Connected to read database
[PrismaWriteService] Connected to write database
[NestJS worker application bootstrapped successfully.
[RedisService] IoRedis connected!
# curl -s http://192.168.146.128:8129/v2/me | head -c 200
{"status":"error","timestamp":"...","path":"/v2/me","error":{"code":"UnauthorizedException","message":"ApiAuthStrategy - No authentication method provided. ...
返回业务级 401(而非 502/404)即说明 API v2 已就绪。注意社区版 API v2 的 license 限制:API Key 认证链路校验 CALCOM_LICENSE_KEY(企业授权),未配置时任何 API Key 都会得到 401 Invalid or missing CALCOM_LICENSE_KEY;携带 Web 会话 Cookie(NEXT_AUTH 通道)调用不经此校验,实测可用,复现流程见《Cal.diy 快速入门与实战示例》。
六、数据持久化
| 数据 | 载体 | 说明 |
|---|---|---|
| 预约/用户/事件类型等业务数据 | 卷 cal-diy_database-data → /var/lib/postgresql | 核心状态,备份重点是它 |
| Redis 缓存与队列 | 卷 cal-diy_redis-data → /data | 可丢失性数据,重建容器不丢业务数据 |
| 应用静态资源 | 随镜像 | 无状态,版本由镜像 tag 固定 |
验证卷已生成:
# docker volume ls | grep cal-diy
local cal-diy_database-data
local cal-diy_redis-data
宿主侧直连数据库验证(8128 端口):
# docker exec cal-com-6-2-0-db psql -U cal_com -d cal_com -c '\dt' | head -5
List of relations
Schema | Name | Type | Owner
--------+--------------------------+-------+---------
public | Account | table | cal_com
public | ApiKey | table | cal_com
public | App | table | cal_com
表名混合大小写(Prisma 默认 PascalCase),SQL 维护时必须带双引号,详见《Cal.diy 日常运维与排障手册》。
七、环境变量速查
| 变量 | 必填 | 说明 |
|---|---|---|
DATABASE_URL / DATABASE_DIRECT_URL | 是 | PostgreSQL 连接串,compose 内指向 database 服务 |
NEXTAUTH_URL | 是 | 站点对外地址(含协议与端口),登录回调基准 |
NEXT_PUBLIC_WEBAPP_URL / NEXT_PUBLIC_WEBSITE_URL | 是 | 应用与站点对外地址,预约页链接生成基准 |
NEXTAUTH_SECRET | 是 | 会话签名密钥,openssl rand -base64 32 生成 |
CALENDSO_ENCRYPTION_KEY | 是 | 第三方凭据加密密钥(24 位),更换后已存凭据不可解密 |
ALLOWED_HOSTNAMES | 建议配 | 允许访问的主机名:端口 JSON 数组,非标域名访问必配 |
CALCOM_TELEMETRY_DISABLED | 建议 | 设 1 关闭匿名遥测 |
STRIPE_API_KEY / STRIPE_WEBHOOK_SECRET | API v2 必填 | 变量必须存在,自托管可留空 |
JWT_SECRET | API v2 必填 | API v2 令牌签名密钥 |
CALCOM_LICENSE_KEY | 否 | 企业 license,社区版不配置则 API Key 通道不可用 |
EMAIL_SERVER_HOST / EMAIL_SERVER_PORT | 否 | SMTP 服务器,配置后启用 magic link 登录与邮件通知 |
八、常见问题
Q1:首次打开一直跳 setup 向导,重复填写?
NEXTAUTH_URL 与浏览器访问地址不一致时 Cookie 域不匹配,向导状态无法保存。核对 .env 三处 URL 变量与实际访问地址完全一致后 --force-recreate 重建 Web 容器。
Q2:注册时提示密码不合规?
策略为 ≥7 位且含大写+小写+数字。q1w2e3r4 缺大写会被拒,改用 Q1w2e3r4 类口令。
Q3:API v2 一直重启?
看日志是否 Missing environment variable: STRIPE_API_KEY.,按 4.6 现象 2 处理。
Q4:预约页能打开但登录后跳回首页?
多为 ALLOWED_HOSTNAMES 缺项或 Cookie Secure 语义与 HTTP 站点冲突:自托管 HTTP 站点确保未设置 NEXTAUTH_COOKIE_SECURE=true 之类的 HTTPS 专用变量。
Q5:如何完全重置环境?
docker compose down -v 删除卷即清空全部数据(会删库,先备份);仅重建应用不动数据用 docker compose restart calcom。
九、参考链接
- 官方 Docker 部署文档:
https://cal.diy/docker - 官方安装总览:
https://cal.diy/installation - 镜像发布位:
https://hub.docker.com/r/calcom/cal.com - 源码仓库:
https://github.com/calcom/cal.diy - 站内同批文档:《Cal.diy 介绍与选型指南》《Cal.diy 快速入门与实战示例》《Cal.diy 日常运维与排障手册》