This guide covers Docker-specific configuration, customization, and best practices for deploying Cal.com with Docker and Docker Compose.
Overview
Cal.com provides official Docker images and Docker Compose configurations for easy deployment:
- Docker Hub:
calcom/cal.com
- Scarf Registry:
calcom.docker.scarf.sh/calcom/cal.com (default, for metrics)
- GitHub: Source Dockerfiles in the main repository
Support Policy: Cal.com maintains official Docker configurations, but users are responsible for their Docker-based installations. Community support is available via GitHub Discussions.
Docker Compose Services
The default docker-compose.yml includes multiple services:
Service Architecture
services:
database # PostgreSQL database
redis # Redis cache and queues
calcom # Main web application
calcom-api # API v2 service
studio # Prisma Studio (optional)
Database Service
database:
container_name: database
image: postgres
restart: always
volumes:
- database-data:/var/lib/postgresql
environment:
- POSTGRES_USER=unicorn_user
- POSTGRES_PASSWORD=magical_password
- POSTGRES_DB=calendso
networks:
- stack
Change default credentials in production!environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=calendso
Redis Service
redis:
container_name: redis
image: redis:latest
restart: always
volumes:
- redis-data:/data
networks:
- stack
ports:
- "${REDIS_PORT:-6379}:6379"
Redis is used for:
- Caching
- Job queues
- Session storage
- Rate limiting
calcom:
image: calcom.docker.scarf.sh/calcom/cal.com
build:
context: .
dockerfile: Dockerfile
args:
NEXT_PUBLIC_WEBAPP_URL: ${NEXT_PUBLIC_WEBAPP_URL}
NEXT_PUBLIC_LICENSE_CONSENT: ${NEXT_PUBLIC_LICENSE_CONSENT}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
CALENDSO_ENCRYPTION_KEY: ${CALENDSO_ENCRYPTION_KEY}
DATABASE_URL: ${DATABASE_URL}
restart: always
networks:
- stack
ports:
- 3000:3000
env_file: .env
environment:
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DATABASE_HOST}/${POSTGRES_DB}
depends_on:
- database
API v2 Service
calcom-api:
container_name: calcom-api
build:
context: .
dockerfile: apps/api/v2/Dockerfile
args:
DATABASE_URL: ${DATABASE_URL}
restart: always
networks:
- stack
ports:
- "${API_PORT:-80}:${API_PORT:-80}"
env_file: .env
environment:
- API_PORT=${API_PORT:-80}
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DATABASE_HOST}/${POSTGRES_DB}
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
- REDIS_URL=${REDIS_URL}
depends_on:
- database
- redis
Prisma Studio (Development)
studio:
image: calcom.docker.scarf.sh/calcom/cal.com
restart: always
networks:
- stack
ports:
- 5555:5555
env_file: .env
environment:
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DATABASE_HOST}/${POSTGRES_DB}
depends_on:
- database
command:
- npx
- prisma
- studio
Remove or comment out Prisma Studio in production to prevent unauthorized database access.
Dockerfile Deep Dive
Main Application Dockerfile
The Cal.com Dockerfile uses multi-stage builds:
Stage 1: Builder
FROM --platform=$BUILDPLATFORM node:20 AS builder
WORKDIR /calcom
# Build arguments
ARG NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
ARG NEXTAUTH_SECRET=secret
ARG CALENDSO_ENCRYPTION_KEY=secret
ARG DATABASE_URL
ARG MAX_OLD_SPACE_SIZE=6144
# Environment variables for build
ENV NEXT_PUBLIC_WEBAPP_URL=http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER \
NODE_OPTIONS=--max-old-space-size=${MAX_OLD_SPACE_SIZE} \
BUILD_STANDALONE=true
# Copy source and build
COPY package.json yarn.lock .yarnrc.yml ./
COPY .yarn ./.yarn
COPY apps/web ./apps/web
COPY packages ./packages
RUN yarn install
RUN yarn workspace @calcom/web run build
NEXT_PUBLIC_WEBAPP_URL is set to a placeholder during build and replaced at runtime by start.sh script.
Stage 2: Builder-Two
FROM node:20 AS builder-two
WORKDIR /calcom
ARG NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
ENV NODE_ENV=production
# Copy artifacts from builder
COPY --from=builder /calcom/yarn.lock ./yarn.lock
COPY --from=builder /calcom/node_modules ./node_modules
COPY --from=builder /calcom/packages ./packages
COPY --from=builder /calcom/apps/web ./apps/web
# Replace placeholder with actual URL
RUN scripts/replace-placeholder.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_WEBAPP_URL}
Stage 3: Runner
FROM node:20 AS runner
WORKDIR /calcom
# Install utilities
RUN apt-get update && \
apt-get install -y --no-install-recommends netcat-openbsd wget && \
rm -rf /var/lib/apt/lists/*
COPY --from=builder-two /calcom ./
ENV NODE_ENV=production
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=30s --retries=5 \
CMD wget --spider http://localhost:3000 || exit 1
CMD ["/calcom/scripts/start.sh"]
API v2 Dockerfile
FROM node:20-alpine AS build
ARG DATABASE_DIRECT_URL
ARG DATABASE_URL
WORKDIR /calcom
ENV NODE_ENV="production"
ENV NODE_OPTIONS="--max-old-space-size=8192"
ENV DATABASE_URL=${DATABASE_URL}
COPY . .
RUN yarn install
RUN yarn workspace @calcom/api-v2 run generate-schemas
RUN yarn workspace @calcom/api-v2 run build:docker
RUN yarn workspace @calcom/api-v2 run build
EXPOSE 80
CMD ["yarn", "workspace", "@calcom/api-v2", "start:prod"]
Environment Variables
Runtime Variables
These must be set in your .env file:
# Database
DATABASE_URL="postgresql://unicorn_user:magical_password@database:5432/calendso"
DATABASE_HOST="database"
POSTGRES_USER="unicorn_user"
POSTGRES_PASSWORD="magical_password"
POSTGRES_DB="calendso"
# Redis
REDIS_URL="redis://redis:6379"
REDIS_PORT=6379
# Application
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="your-secret-here"
CALENDSO_ENCRYPTION_KEY="your-encryption-key"
# VAPID (Push Notifications)
NEXT_PUBLIC_VAPID_PUBLIC_KEY="your-public-key"
VAPID_PRIVATE_KEY="your-private-key"
# API v2
API_PORT=80
API_URL="http://calcom-api:80"
NEXT_PUBLIC_API_V2_URL="http://localhost:5555/api/v2"
# License (optional)
CALCOM_LICENSE_KEY="your-license-key"
Build-time Arguments
For custom builds, pass these arguments:
docker build \
--build-arg NEXT_PUBLIC_WEBAPP_URL=https://cal.yourdomain.com \
--build-arg NEXTAUTH_SECRET=$(openssl rand -base64 32) \
--build-arg CALENDSO_ENCRYPTION_KEY=$(openssl rand -base64 24) \
--build-arg DATABASE_URL=$DATABASE_URL \
--build-arg MAX_OLD_SPACE_SIZE=8192 \
--build-arg CALCOM_TELEMETRY_DISABLED=1 \
-t calcom-custom:latest .
Startup Process
The scripts/start.sh handles initialization:
#!/bin/sh
set -x
# Replace build-time URL with runtime URL if different
scripts/replace-placeholder.sh "$BUILT_NEXT_PUBLIC_WEBAPP_URL" "$NEXT_PUBLIC_WEBAPP_URL"
# Wait for database
scripts/wait-for-it.sh ${DATABASE_HOST} -- echo "database is up"
# Run migrations
npx prisma migrate deploy --schema /calcom/packages/prisma/schema.prisma
# Seed app store
npx ts-node --transpile-only /calcom/scripts/seed-app-store.ts
# Start application
yarn start
Initialization Steps
- URL Replacement: Updates static files if
NEXT_PUBLIC_WEBAPP_URL changed
- Database Wait: Ensures PostgreSQL is ready
- Migrations: Applies pending database migrations
- App Store Seed: Syncs app metadata
- Application Start: Launches Next.js server
Migrations run automatically on every container start. This is safe and idempotent.
Docker Compose Usage
Running Services
# All services (database + redis + calcom + api + studio)
docker compose up -d
# Web application only (requires external database)
docker compose up -d calcom
# Web + API (requires external database and redis)
docker compose up -d calcom calcom-api
# With Prisma Studio
docker compose up -d calcom studio
# View logs
docker compose logs -f calcom
# Stop services
docker compose down
# Stop and remove volumes (⚠️ deletes data)
docker compose down -v
Updating Images
# Pull latest images
docker compose pull
# Rebuild custom images
docker compose build --no-cache
# Restart with new images
docker compose up -d
Scaling Services
# Run multiple Cal.com instances
docker compose up -d --scale calcom=3
# Requires load balancer configuration
Custom Docker Builds
Building from Source
# Clone repository
git clone https://github.com/calcom/cal.com.git
cd cal.com
# Start database for build
docker compose up -d database
# Build image
DOCKER_BUILDKIT=0 docker compose build calcom
# Run custom image
docker compose up -d
DOCKER_BUILDKIT=0 is required to enable network bridge during build. This limitation may be removed in future versions.
Custom Dockerfile
Create a custom Dockerfile extending the base image:
FROM calcom/cal.com:latest
# Add custom scripts
COPY ./custom-scripts /calcom/custom-scripts
RUN chmod +x /calcom/custom-scripts/*
# Add custom dependencies
RUN yarn add custom-package
# Override start command
CMD ["/calcom/custom-scripts/start.sh"]
# Build for multiple architectures
docker buildx create --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t calcom-custom:latest \
--push .
ARM Architecture
ARM Images
For Apple Silicon, Raspberry Pi, and ARM servers:
# Pull ARM-specific image
docker pull calcom/cal.com:v5.6.19-arm
# Update docker-compose.yml
services:
calcom:
image: calcom/cal.com:v5.6.19-arm
Building for ARM
docker build --platform linux/arm64 -t calcom-arm:latest .
Volumes and Persistence
Default Volumes
volumes:
database-data: # PostgreSQL data
redis-data: # Redis persistence
Backup Volumes
# Backup database volume
docker run --rm \
-v cal_database-data:/data \
-v $(pwd):/backup \
alpine tar czf /backup/database-backup.tar.gz -C /data .
# Restore database volume
docker run --rm \
-v cal_database-data:/data \
-v $(pwd):/backup \
alpine tar xzf /backup/database-backup.tar.gz -C /data
Inspect Volumes
# List volumes
docker volume ls
# Inspect volume
docker volume inspect cal_database-data
# Remove unused volumes
docker volume prune
Networks
Default Network
networks:
stack:
name: stack
external: false
All services communicate on the stack network.
Custom Networks
networks:
frontend:
name: frontend
backend:
name: backend
services:
calcom:
networks:
- frontend
- backend
database:
networks:
- backend
Health Checks
Built-in Health Check
The Dockerfile includes automatic health monitoring:
HEALTHCHECK --interval=30s --timeout=30s --retries=5 \
CMD wget --spider http://localhost:3000 || exit 1
Custom Health Checks
services:
calcom:
healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
Check Health Status
# View health status
docker ps
# Inspect health
docker inspect --format='{{.State.Health.Status}}' calcom
# View health logs
docker inspect --format='{{json .State.Health}}' calcom | jq
Production Optimizations
Resource Limits
services:
calcom:
deploy:
resources:
limits:
cpus: '2'
memory: 4G
reservations:
cpus: '1'
memory: 2G
Restart Policies
services:
calcom:
restart: unless-stopped # or 'always', 'on-failure'
Logging Configuration
services:
calcom:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Read-only Root Filesystem
services:
calcom:
read_only: true
tmpfs:
- /tmp
- /calcom/.next/cache
Troubleshooting
View Logs
# All services
docker compose logs
# Specific service
docker compose logs calcom
# Follow logs
docker compose logs -f calcom
# Last 100 lines
docker compose logs --tail=100 calcom
Container Shell Access
# Access running container
docker exec -it calcom sh
# Run one-off command
docker compose run --rm calcom sh
Database Connection Issues
# Test database from container
docker exec calcom wget --spider database:5432 || echo "Cannot reach database"
# Check database logs
docker compose logs database
# Test connection string
docker exec calcom npx prisma db pull --schema /calcom/packages/prisma/schema.prisma
Port Conflicts
Error: Bind for 0.0.0.0:3000 failed: port is already allocated
Solution: Change port mapping:
ports:
- "3001:3000" # Host:Container
Out of Memory
JavaScript heap out of memory
Solution: Increase memory:
environment:
- NODE_OPTIONS=--max-old-space-size=8192
Migration Failures
# Check migration status
docker exec calcom npx prisma migrate status \
--schema /calcom/packages/prisma/schema.prisma
# Manual migration
docker exec calcom npx prisma migrate deploy \
--schema /calcom/packages/prisma/schema.prisma
Security Best Practices
Secret Management
# Use Docker secrets (Swarm mode)
secrets:
db_password:
file: ./secrets/db_password.txt
services:
calcom:
secrets:
- db_password
environment:
- DATABASE_URL=postgresql://user:file:///run/secrets/db_password@db:5432/calendso
Non-root User
Modify Dockerfile to run as non-root:
RUN addgroup -g 1001 calcom && \
adduser -D -u 1001 -G calcom calcom
USER calcom
Network Isolation
networks:
frontend:
internal: false
backend:
internal: true # No external access
# Don't expose Prisma Studio in production
services:
studio:
profiles:
- development
# Run only production profile
# docker compose --profile production up -d
Monitoring and Observability
Prometheus Metrics
Expose metrics endpoint:
services:
calcom:
labels:
prometheus.scrape: "true"
prometheus.port: "3000"
prometheus.path: "/api/metrics"
Container Stats
# Real-time stats
docker stats calcom
# All containers
docker compose stats
Production Checklist
Next Steps