Cal.com на VPS в Docker: свой планировщик встреч вместо Calendly
Cal.com — открытый движок бронирования встреч и самая известная альтернатива Calendly. Логика простая: вы даёте ссылку вида cal.example.ru/vashe-imya, посетитель видит свободные слоты, выбирает время — а система сама присылает подтверждение, напоминание и создаёт событие в вашем календаре. Проект распространяется под AGPLv3: базовое бронирование, синхронизация календарей и уведомления открыты и бесплатны при самостоятельном хостинге, платный ключ нужен только для функций уровня Enterprise (SSO, командные инсайты).
Зачем ставить его на свой сервер? Причины те же, по которым вообще уходят от SaaS: данные о встречах и контактах клиентов остаются у вас, нет лимитов бесплатного тарифа на число типов событий и интеграций, и вы сами решаете, когда обновляться. Для России добавляется практический аргумент — свой сервер не зависит от того, принимает ли зарубежный сервис карту российского банка. Подготовьтесь к тому, что Cal.com — это Next.js-приложение с PostgreSQL, и ресурсов ему нужно больше, чем лёгкому бэкенду: комфортно работать на 2 ядрах и 4 ГБ RAM, а на время сборки лучше иметь 8 ГБ, иначе процесс может упасть по нехватке памяти. Подходящую конфигурацию можно подобрать в разделе VPS для этого проекта.
Что понадобится
- VPS с Ubuntu 24.04, минимум 2 vCPU и 4 ГБ RAM (8 ГБ — с запасом под сборку).
- Docker и плагин Docker Compose.
- Домен или поддомен, направленный A-записью на IP сервера. Cal.com жёстко завязан на публичный адрес:
NEXT_PUBLIC_WEBAPP_URL«запекается» в клиентский бандл на этапе сборки, поменять его на лету без пересборки образа не получится. - SMTP-доступ для писем-приглашений и напоминаний. Можно внешний сервис, можно свой почтовый сервер на этом же VPS.
- git на сервере: готового образа на Docker Hub команда проекта не публикует, контейнер собирается из исходников.
Установка пошагово
Шаг 1. Готовим систему и периметр. Обновите пакеты и сразу закройте фаервол, оставив только SSH и веб-порты:
apt update && apt upgrade -y
ufw allow 22/tcp && ufw allow 80,443/tcp && ufw enable
Затем направьте домен на IP сервера и проверьте, что запись разошлась:
dig +short cal.example.ru
Команда должна вернуть IP вашего VPS. Если домен ещё не настроен, HTTPS-сертификат выпустить не получится, а ссылки в письмах будут вести в никуда.
Шаг 2. Ставим Docker и забираем исходники. Официальный скрипт на Ubuntu 24.04 отрабатывает без нареканий:
curl -fsSL https://get.docker.com | sh
git clone --recursive https://github.com/calcom/docker.git cal-com
cd cal-com
Флаг --recursive обязателен: без него подмодуль с кодом самого приложения останется пустым, и сборка не найдёт исходники.
Шаг 3. Готовим секреты. Скопируйте образец конфигурации и заполните значения. Два секрета Cal.com требует жёстко — без них приложение либо не стартует, либо ломается на первом входе:
cp .env.example .env
openssl rand -base64 32 # NEXTAUTH_SECRET
openssl rand -hex 16 # CALENDSO_ENCRYPTION_KEY
NEXTAUTH_SECRET подписывает сессии NextAuth, CALENDSO_ENCRYPTION_KEY шифрует чувствительные данные в базе — в частности, токены подключённых календарей. Оба значения держите в .env рядом с docker-compose.yml, а не внутри самого compose-файла: так они случайно не уедут в git. И не меняйте их после первого успешного старта — смена первого разлогинит всех пользователей, смена второго сделает нечитаемыми сохранённые интеграции, и календари придётся переподключать.
Шаг 4. Собираем и запускаем. В поставке уже есть docker-compose.yml с сервисом Cal.com и PostgreSQL. Порт наружу напрямую не публикуйте — привяжите к локальному адресу 127.0.0.1:3000:3000, снаружи доступ пойдёт через реверс-прокси. Сборка тяжелее, чем у большинства self-hosted проектов: Next.js компилируется прямо на сервере. Запуск и наблюдение за логами:
docker compose up -d --build
docker compose logs -f calcom
При первом старте контейнер сам применяет миграции базы данных Prisma. Дождитесь, пока в логах появится готовность приложения, и только потом идите в браузер.
Шаг 5. HTTPS через реверс-прокси. Cal.com слушает 3000-й порт и TLS сам не умеет — это задача nginx перед ним:
server {
listen 80;
server_name cal.example.ru;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name cal.example.ru;
ssl_certificate /etc/letsencrypt/live/cal.example.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cal.example.ru/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
Сертификат выпускается стандартно через certbot. После перезагрузки nginx откройте сайт, создайте первый аккаунт и подключите календарь в настройках.
Docker Compose
Готовый минимальный конфиг: PostgreSQL с healthcheck, приложение собирается из исходников и стартует только после того, как база готова принимать подключения. Пустых строк внутри блоков не оставляйте — рендерер блога делит текст по пустым строкам.
services:
database:
image: postgres
restart: unless-stopped
environment:
POSTGRES_USER: calcom
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: calcom
volumes:
- calcom_db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U calcom"]
interval: 10s
timeout: 5s
retries: 5
networks:
- calcom_net
calcom:
build:
context: .
args:
NEXT_PUBLIC_WEBAPP_URL: https://cal.example.ru
DATABASE_URL: postgresql://calcom:${POSTGRES_PASSWORD}@database:5432/calcom
image: cal-selfhosted:latest
restart: unless-stopped
depends_on:
database:
condition: service_healthy
environment:
DATABASE_URL: postgresql://calcom:${POSTGRES_PASSWORD}@database:5432/calcom
NEXTAUTH_URL: https://cal.example.ru
NEXT_PUBLIC_WEBAPP_URL: https://cal.example.ru
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
CALENDSO_ENCRYPTION_KEY: ${CALENDSO_ENCRYPTION_KEY}
EMAIL_FROM: ${EMAIL_FROM}
EMAIL_SERVER_HOST: ${SMTP_HOST}
EMAIL_SERVER_PORT: ${SMTP_PORT}
EMAIL_SERVER_USER: ${SMTP_USER}
EMAIL_SERVER_PASSWORD: ${SMTP_PASSWORD}
ports:
- "127.0.0.1:3000:3000"
networks:
- calcom_net
volumes:
calcom_db:
networks:
calcom_net:
Ключевой момент: NEXT_PUBLIC_WEBAPP_URL и DATABASE_URL продублированы в build.args и в environment. Первое нужно, чтобы Next.js правильно собрал клиентский бандл, второе — чтобы Prisma подключалась к базе уже в рантайме. Рядом положите .env:
POSTGRES_PASSWORD=сложный_пароль
NEXTAUTH_SECRET=сгенерированное_значение
CALENDSO_ENCRYPTION_KEY=сгенерированное_значение
EMAIL_FROM=noreply@cal.example.ru
SMTP_HOST=smtp.example.ru
SMTP_PORT=587
SMTP_USER=noreply@cal.example.ru
SMTP_PASSWORD=пароль_от_smtp
Частые ошибки и решения
Белый экран или падение контейнера на старте. Почти всегда это пустые или переиспользованные NEXTAUTH_SECRET и CALENDSO_ENCRYPTION_KEY. Проверьте, что обе переменные непустые и уникальны для каждой установки.
Ошибка P3009 или «Migration failed to apply» после обновления. Вы заменили тег образа и перезапустили контейнер, не применив миграции. Prisma помечает упавшую миграцию как failed и блокирует все следующие. Посмотрите статус:
docker compose run --rm calcom npx prisma migrate status
Если миграция действительно применена вручную или её эффект больше не нужен, блокировку снимают командой npx prisma migrate resolve --rolled-back <имя_миграции>. Вслепую делать это на проде не стоит — сначала прогоните обновление на копии.
Письма с подтверждением не приходят. Без переменных EMAIL_SERVER_HOST, EMAIL_SERVER_PORT, EMAIL_SERVER_USER, EMAIL_SERVER_PASSWORD и EMAIL_FROM Cal.com молча не отправляет письма: в интерфейсе ошибки не будет, ищите её в логах контейнера. Вторая причина — письма уходят, но падают в спам: если держите свой почтовый сервер без корректных SPF, DKIM и обратной PTR-записи для IP, Gmail и Яндекс будут их отбраковывать. Для транзакционных писем надёжнее внешний SMTP-релей.
Календарь не синхронизируется, видеозвонки не создаются. Проверьте, что NEXT_PUBLIC_WEBAPP_URL и NEXTAUTH_URL совпадают с реальным адресом сайта, а страница открывается именно по HTTPS. Интеграции возвращают пользователя на публичный URL, и любое расхождение ломает обратный редирект.
Медленная загрузка и таймауты за прокси. Cal.com — тяжёлое Next.js-приложение, а реверс-прокси по умолчанию ждёт ответ недолго. Проверьте, что в nginx не обрезаны таймауты, и убедитесь, что контейнеру хватает памяти: при нехватке RAM приложение начинает отвечать минутами. Если база стоит отдельно и подключения рвутся под нагрузкой, стоит поднять лимиты соединений PostgreSQL.
Бэкап и обновление
Перед любым обновлением — дамп базы. Это не формальность: миграции Prisma меняют схему, и откатиться без копии будет нечем.
docker exec -t $(docker ps -qf "name=database") pg_dump -U calcom calcom > backup_$(date +%F).sql
Правильный порядок обновления — миграции применяются до запуска веб-процесса:
docker compose pull calcom
docker compose stop calcom
docker compose run --rm calcom npx prisma migrate deploy
docker compose up -d calcom
В бэкап обязательно включите .env: без CALENDSO_ENCRYPTION_KEY расшифровать токены интеграций из дампа не получится, календари придётся подключать заново. Храните копии вне сервера — на другом хосте или в объектном хранилище.
Итог
После установки у вас есть свой планировщик встреч: публичная страница бронирования, синхронизация с календарями, автоматические письма и полный контроль над данными. Дальше стоит сделать три вещи: настроить резервное копирование по расписанию и проверить восстановление из дампа, подключить домен и почту так, чтобы приглашения не попадали в спам, и закрепить версию образа вместо latest — тогда обновление будет осознанным шагом с бэкапом, а не лотереей. Если сервер ещё не выбран, начните с подборки Docker VPS: под Cal.com нужны 2 ядра, 4 ГБ RAM и запас по памяти на время сборки.