Scheduling
    PostgreSQL

    Deploy Cal.com on a VPS

    Self-host Cal.com scheduling on a RamNode VPS with PostgreSQL, Docker Compose, Caddy HTTPS, calendar integrations, and backups.

    Cal.com is an open-source scheduling platform. This guide runs its official image with PostgreSQL in Docker Compose behind Caddy, with HTTPS, mail delivery and backups.

    Prerequisites

    • RamNode KVM VPS running Ubuntu 24.04 LTS (26.04 LTS also works), at least 2 GB RAM and 40 GB disk; 4 GB RAM and 2 vCPUs are more comfortable during startup.
    • Domain such as cal.example.com, with an A record (and AAAA if using IPv6) pointing at the VPS.
    • Root SSH access initially and SMTP relay credentials for booking confirmations and resets.
    • Optional Google Cloud or Microsoft Entra app credentials for calendar sync.

    Replace domains and example secrets throughout. See DNS and Docker Compose.

    Prepare the server

    Run as root, then verify deploy can log in with SSH in a second terminal before disabling root and password access:

    shell
    apt update && apt -y full-upgrade
    apt install -y rsync ufw unattended-upgrades
    timedatectl set-timezone UTC
    adduser deploy
    usermod -aG sudo deploy
    rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy

    After testing the new login, run as root:

    shell
    cat > /etc/ssh/sshd_config.d/99-hardening.conf <<'CONFIG'
    PermitRootLogin no
    PasswordAuthentication no
    CONFIG
    sshd -t && systemctl restart ssh
    ufw allow OpenSSH
    ufw allow 80/tcp
    ufw allow 443/tcp
    ufw enable
    fallocate -l 2G /swapfile
    chmod 600 /swapfile
    mkswap /swapfile && swapon /swapfile
    echo '/swapfile none swap sw 0 0' >> /etc/fstab
    dpkg-reconfigure -plow unattended-upgrades

    Allow your actual SSH port before enabling UFW if it differs from 22. Docker can bypass UFW for published ports; bind Cal.com only to localhost. See cloud firewall.

    Install Docker Engine and Compose

    As deploy:

    shell
    sudo apt install -y ca-certificates curl
    sudo install -m 0755 -d /etc/apt/keyrings
    sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
    sudo chmod a+r /etc/apt/keyrings/docker.asc
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    sudo apt update
    sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
    sudo usermod -aG docker deploy
    sudo tee /etc/docker/daemon.json > /dev/null <<'JSON'
    {"log-driver":"json-file","log-opts":{"max-size":"10m","max-file":"3"}}
    JSON
    sudo systemctl restart docker

    Log out and back in, then check docker run --rm hello-world and docker compose version.

    Deploy Cal.com with Docker Compose

    shell
    sudo mkdir -p /opt/calcom && sudo chown deploy:deploy /opt/calcom
    cd /opt/calcom
    openssl rand -hex 24     # PostgreSQL password
    openssl rand -base64 32   # NEXTAUTH_SECRET
    openssl rand -base64 24   # CALENDSO_ENCRYPTION_KEY

    Create /opt/calcom/.env, substituting your own values:

    shell
    DOMAIN=cal.example.com
    POSTGRES_PASSWORD=change-me-db
    NEXTAUTH_SECRET=change-me-nextauth
    CALENDSO_ENCRYPTION_KEY=change-me-encryption
    EMAIL_FROM=cal@example.com
    EMAIL_SERVER_HOST=smtp.example.com
    EMAIL_SERVER_PORT=587
    EMAIL_SERVER_USER=smtp-user
    EMAIL_SERVER_PASSWORD=smtp-password
    shell
    chmod 600 /opt/calcom/.env

    Keep the encryption key securely: losing it makes stored calendar credentials unreadable. Create /opt/calcom/compose.yml:

    shell
    services:
      calcom-db:
        image: postgres:16
        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 -d calcom"]
          interval: 10s
          timeout: 5s
          retries: 5
    
      calcom:
        image: calcom/cal.com:latest
        restart: unless-stopped
        environment:
          DATABASE_URL: postgresql://calcom:${POSTGRES_PASSWORD}@calcom-db:5432/calcom
          DATABASE_DIRECT_URL: postgresql://calcom:${POSTGRES_PASSWORD}@calcom-db:5432/calcom
          NEXT_PUBLIC_WEBAPP_URL: https://${DOMAIN}
          NEXTAUTH_URL: https://${DOMAIN}/api/auth
          NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
          CALENDSO_ENCRYPTION_KEY: ${CALENDSO_ENCRYPTION_KEY}
          EMAIL_FROM: ${EMAIL_FROM}
          EMAIL_SERVER_HOST: ${EMAIL_SERVER_HOST}
          EMAIL_SERVER_PORT: ${EMAIL_SERVER_PORT}
          EMAIL_SERVER_USER: ${EMAIL_SERVER_USER}
          EMAIL_SERVER_PASSWORD: ${EMAIL_SERVER_PASSWORD}
          CALCOM_TELEMETRY_DISABLE: "1"
          NODE_ENV: production
        depends_on:
          calcom-db:
            condition: service_healthy
        ports:
          - "127.0.0.1:3000:3000"
    
    volumes:
      calcom-db:
    shell
    cd /opt/calcom
    docker compose config --quiet
    docker compose up -d
    docker compose logs -f calcom

    First startup runs migrations and can take several minutes. Wait for the ready message on port 3000. After testing, pin the image to a release tag rather than latest.

    Configure Caddy and HTTPS

    shell
    sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
    sudo apt update && sudo apt install -y caddy

    Set /etc/caddy/Caddyfile to:

    shell
    cal.example.com {
        encode zstd gzip
        reverse_proxy 127.0.0.1:3000
    }
    shell
    sudo caddy validate --config /etc/caddy/Caddyfile
    sudo systemctl reload caddy

    Caddy obtains a certificate once DNS points to the server and port 80 is accessible. Check journalctl -u caddy -f if it does not.

    First-run setup

    Open https://cal.example.com and follow the setup wizard at /auth/setup. Create the first admin account and choose the license option appropriate for your use (the AGPLv3 community edition needs no key). Enable two-factor authentication in Settings > Security. Test SMTP with a password reset for your own account.

    For Google Calendar or Outlook integration, configure the corresponding app credentials under Settings > Admin > Apps. Register these exact HTTPS redirect URIs with the provider:

    • Google: https://cal.example.com/api/integrations/googlecalendar/callback
    • Microsoft: https://cal.example.com/api/integrations/office365calendar/callback

    Connect a calendar under Settings > Calendars, set availability, create an event type and book a test slot from a private browser window. If only your team needs accounts, add NEXT_PUBLIC_DISABLE_SIGNUP: "true" to the Cal.com environment, recreate with docker compose up -d and verify sign-up is disabled; some versions read this setting only at build time.

    Backups and restore

    Back up PostgreSQL plus .env (especially CALENDSO_ENCRYPTION_KEY) and compose.yml. Create /opt/calcom/backup.sh:

    shell
    #!/usr/bin/env bash
    set -euo pipefail
    cd /opt/calcom
    TS=$(date +%F-%H%M)
    DEST=/var/backups/calcom
    mkdir -p "$DEST"
    docker compose exec -T calcom-db pg_dump -U calcom -Fc calcom > "$DEST/db-$TS.dump"
    cp .env "$DEST/env-$TS"
    cp compose.yml "$DEST/compose-$TS.yml"
    chmod 600 "$DEST"/*
    find "$DEST" -type f -mtime +14 -delete
    shell
    sudo chmod 700 /opt/calcom/backup.sh
    sudo /opt/calcom/backup.sh
    sudo crontab -e
    # Add: 30 3 * * * /opt/calcom/backup.sh

    Copy backups off the VPS. See backups and object storage. To restore, put the saved .env and compose.yml in /opt/calcom, then:

    shell
    cd /opt/calcom
    docker compose up -d calcom-db
    docker compose exec -T calcom-db pg_restore -U calcom -d calcom --clean --if-exists < /var/backups/calcom/db-YYYY-MM-DD-HHMM.dump
    docker compose up -d

    Updating

    Cal.com runs migrations at startup; back up and read release notes before changing versions.

    shell
    cd /opt/calcom
    sudo ./backup.sh
    docker compose pull
    docker compose up -d
    docker compose logs -f calcom

    For rollback, restore the earlier image tag and its pre-update database dump. Upgrade PostgreSQL major versions separately with a dump and restore.

    Troubleshooting

    SymptomCheck
    Caddy cannot get a certificateConfirm dig +short cal.example.com, ports 80/443 and journalctl -u caddy -f.
    502 responseMigrations may still be running. Check docker compose logs -f calcom, free -m and kernel OOM logs; move to 4 GB if needed.
    Links use localhostSet NEXT_PUBLIC_WEBAPP_URL and NEXTAUTH_URL to HTTPS and docker compose up -d --force-recreate calcom.
    Login loopNEXTAUTH_URL must match https://cal.example.com/api/auth.
    Calendar OAuth redirect failsRegister the exact HTTPS callback URI above.
    Booking mail failsVerify authenticated SMTP relay settings and port 587; see outgoing SMTP policy.
    Restored calendars cannot connectRestore the original CALENDSO_ENCRYPTION_KEY or reconnect calendars.