Skip to main content
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

Cal.com Web Service

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

  1. URL Replacement: Updates static files if NEXT_PUBLIC_WEBAPP_URL changed
  2. Database Wait: Ensures PostgreSQL is ready
  3. Migrations: Applies pending database migrations
  4. App Store Seed: Syncs app metadata
  5. 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"]

Multi-platform Builds

# 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

Remove Development Tools

# 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

  • Change default database credentials
  • Generate strong secrets (NEXTAUTH_SECRET, CALENDSO_ENCRYPTION_KEY)
  • Generate VAPID keys for push notifications
  • Remove or secure Prisma Studio service
  • Configure proper NEXT_PUBLIC_WEBAPP_URL
  • Set up external database or configure backups
  • Configure Redis persistence
  • Set resource limits and restart policies
  • Configure logging rotation
  • Enable health checks
  • Use secrets management (not .env file)
  • Set up reverse proxy (Nginx, Traefik, Caddy)
  • Configure SSL certificates
  • Test backup and restore procedures
  • Monitor container health and logs
  • Document custom configurations

Next Steps

Build docs developers (and LLMs) love