How to Self-Host Baikal with Docker Compose

What Is Baikal?

Baikal is a lightweight CalDAV and CardDAV server that syncs your calendars and contacts across all your devices. It replaces Google Calendar, Google Contacts, iCloud Calendar, and iCloud Contacts with something you control entirely. Baikal is built on the sabre/dav framework (PHP), includes a clean web-based admin panel for managing users, calendars, and address books, and runs on SQLite or MariaDB. It uses minimal resources — under 50 MB of RAM — and handles multi-user setups without breaking a sweat.

Prerequisites

  • A Linux server (Ubuntu 22.04+ recommended)
  • Docker and Docker Compose installed (guide)
  • 256 MB of free RAM (Baikal itself uses under 50 MB, but PHP and nginx need headroom)
  • 100 MB of free disk space
  • A domain name (optional for local use, strongly recommended for remote access — most CalDAV clients require HTTPS)

Docker Compose Configuration

Baikal uses a community-maintained Docker image (ckulka/baikal) since there is no official image from the project. The nginx variant is recommended — it is lighter and faster than the Apache variant.

SQLite is the default and works perfectly for personal use or small teams (under 50 users). No extra services needed.

Create a docker-compose.yml file:

services:
  baikal:
    image: ckulka/baikal:0.10.1-nginx
    container_name: baikal
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      # Stores admin settings, database config, and encryption keys
      - baikal_config:/var/www/baikal/config
      # Stores the SQLite database and application-specific data
      - baikal_data:/var/www/baikal/Specific

volumes:
  baikal_config:
  baikal_data:

Start the stack:

docker compose up -d

MariaDB Setup (Optional — For Large Deployments)

If you expect many users or want a database you can query and back up independently, use MariaDB instead of SQLite. You configure the database during the web wizard — this Compose file just makes MariaDB available.

Create a docker-compose.yml file:

services:
  baikal:
    image: ckulka/baikal:0.10.1-nginx
    container_name: baikal
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - baikal_config:/var/www/baikal/config
      - baikal_data:/var/www/baikal/Specific
    depends_on:
      mariadb:
        condition: service_healthy

  mariadb:
    image: mariadb:11.7.2
    container_name: baikal-db
    restart: unless-stopped
    environment:
      # Change these values before first run
      MYSQL_ROOT_PASSWORD: change-this-root-password
      MYSQL_DATABASE: baikal
      MYSQL_USER: baikal
      MYSQL_PASSWORD: change-this-baikal-password
    volumes:
      - mariadb_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  baikal_config:
  baikal_data:
  mariadb_data:

Start the stack:

docker compose up -d

When you reach the database step in the web wizard, select MySQL and enter:

FieldValue
MySQL hostmariadb
MySQL databasebaikal
MySQL usernamebaikal
MySQL passwordThe value you set for MYSQL_PASSWORD

Initial Setup

Baikal uses a web wizard for initial configuration. There are no environment variables to set — everything happens through the browser.

  1. Open http://your-server-ip:8080 in your browser
  2. The setup wizard loads automatically on first run
  3. Admin password — set a strong password for the admin panel. This is the only account that can manage users and collections.
  4. Time zone — select your time zone from the dropdown. This affects how events are displayed in the admin panel.
  5. Database — choose SQLite (default, recommended) or MySQL if you set up MariaDB above. For SQLite, no further configuration is needed. For MySQL, enter the connection details from the table above.
  6. Click Save changes to complete setup

After setup, the admin panel is available at http://your-server-ip:8080/admin/.

Creating Users

  1. Go to the admin panel at /admin/
  2. Log in with the admin password you set during setup
  3. Navigate to Users and resources
  4. Click Add user
  5. Enter a username (this becomes part of the CalDAV/CardDAV URL), display name, email, and password
  6. Click Save

Each user automatically gets a default calendar and address book.

Creating Additional Calendars and Address Books

  1. In the admin panel, go to Users and resources
  2. Click on a user
  3. Click Add calendar or Add address book
  4. Set the display name and (optionally) a description
  5. Click Save

Connecting Clients

Baikal’s CalDAV and CardDAV URLs follow this pattern:

CalDAV:  https://your-domain/dav.php/calendars/USERNAME/CALENDAR-NAME/
CardDAV: https://your-domain/dav.php/addressbooks/USERNAME/ADDRESS-BOOK-NAME/

The default calendar is named default and the default address book is named default. So for a user called john:

CalDAV:  https://your-domain/dav.php/calendars/john/default/
CardDAV: https://your-domain/dav.php/addressbooks/john/default/

Most clients support auto-discovery, so you only need to provide the base URL:

https://your-domain/dav.php

iOS (Native Calendar and Contacts)

  1. Open Settings → Calendar → Accounts → Add Account → Other
  2. Tap Add CalDAV Account
  3. Server: your-domain (no port, no path — iOS discovers the rest)
  4. Username: your Baikal username
  5. Password: your Baikal password
  6. Tap Next — iOS auto-discovers all calendars
  7. Repeat for contacts: Settings → Contacts → Accounts → Add Account → Other → Add CardDAV Account with the same server, username, and password

macOS Calendar and Contacts

  1. Open Calendar → Settings → Accounts → Add Account → Other CalDAV Account
  2. Account Type: Manual
  3. Username: your Baikal username
  4. Password: your Baikal password
  5. Server Address: https://your-domain/dav.php
  6. For contacts: Contacts → Settings → Accounts → Other Contacts Account → CardDAV with the same details

Android (DAVx5)

  1. Install DAVx5 from F-Droid or the Play Store
  2. Tap + to add an account
  3. Select Login with URL and user name
  4. Base URL: https://your-domain/dav.php
  5. User name: your Baikal username
  6. Password: your Baikal password
  7. DAVx5 auto-discovers all calendars and address books — select which ones to sync
  8. Enable sync in Android settings for the DAVx5 account

Thunderbird

  1. Open Calendar → New Calendar → On the Network
  2. Username: your Baikal username
  3. Location: https://your-domain/dav.php/calendars/USERNAME/default/
  4. Thunderbird prompts for your password, then discovers available calendars
  5. For contacts: Install the CardBook add-on, then add a CardDAV address book with URL https://your-domain/dav.php/addressbooks/USERNAME/default/

Configuration

CalDAV Auto-Discovery

For clients that support auto-discovery (iOS, macOS, DAVx5), configure your reverse proxy to handle .well-known URLs:

/.well-known/caldav  → redirect to /dav.php
/.well-known/carddav → redirect to /dav.php

This lets clients find the CalDAV/CardDAV server using just the domain name, without needing the full /dav.php path.

Changing the Admin Password

Log into the admin panel at /admin/, navigate to Settings, and update the admin password.

File Permissions

The nginx variant of the Docker image runs as UID 101 (the nginx user). If you use bind mounts instead of named volumes, ensure the host directories are owned by UID 101:

mkdir -p ./config ./data
chown -R 101:101 ./config ./data

With named volumes (as shown in the Compose examples above), Docker handles permissions automatically.

Reverse Proxy

Most CalDAV/CardDAV clients require HTTPS. Set up a reverse proxy with SSL termination in front of Baikal.

Nginx Proxy Manager:

  • Scheme: http
  • Forward Hostname: baikal (or 127.0.0.1 if not on the same Docker network)
  • Forward Port: 80 (the container’s internal port)
  • Enable SSL with Let’s Encrypt
  • Add Custom Location for /.well-known/caldav — redirect (301) to /dav.php
  • Add Custom Location for /.well-known/carddav — redirect (301) to /dav.php

Caddy (in your Caddyfile):

cal.yourdomain.com {
    redir /.well-known/caldav /dav.php 301
    redir /.well-known/carddav /dav.php 301
    reverse_proxy baikal:80
}

See Reverse Proxy Setup for full configuration with other proxies.

Backup

Baikal stores all its data in two volumes. Back up both:

  • baikal_config — admin settings, database configuration, encryption keys
  • baikal_data — the SQLite database (if using SQLite), calendar data, and contact data

Backing Up Named Volumes

# Stop Baikal to ensure data consistency (optional but recommended for SQLite)
docker compose stop baikal

# Back up both volumes
docker run --rm \
  -v baikal_config:/source/config:ro \
  -v baikal_data:/source/data:ro \
  -v $(pwd):/backup \
  alpine tar czf /backup/baikal-backup-$(date +%Y%m%d).tar.gz -C /source .

# Restart Baikal
docker compose start baikal

Restoring from Backup

docker compose down

docker run --rm \
  -v baikal_config:/target/config \
  -v baikal_data:/target/data \
  -v $(pwd):/backup:ro \
  alpine sh -c "cd /target && tar xzf /backup/baikal-backup-YYYYMMDD.tar.gz"

docker compose up -d

MariaDB Backup

If using MariaDB instead of SQLite, dump the database separately:

docker exec baikal-db mariadb-dump -u baikal -p'your-password' baikal > baikal-db-$(date +%Y%m%d).sql

See Backup Strategy for a complete 3-2-1 backup approach.

Troubleshooting

Calendar Not Syncing Across Devices

Symptom: Events created on one device do not appear on another, even after waiting several minutes. Fix: Verify that both devices are pointed at the exact same CalDAV URL. The URL is case-sensitive and must include the correct calendar name. In DAVx5, open the account and tap the sync button. On iOS, pull down on the calendar list to force a refresh. Check Baikal logs for errors:

docker compose logs baikal

If events sync one direction but not the other, the second client may be using a different calendar path.

Permission Denied or 403 Errors

Symptom: Client returns 403 Forbidden when creating or modifying events/contacts. Fix: This usually means the client is trying to write to a calendar or address book that belongs to a different user. Verify the URL includes the correct username. Each user can only access their own collections unless you configure shared access through the admin panel. Also check file permissions if using bind mounts — the container needs write access as UID 101:

chown -R 101:101 ./config ./data
docker compose restart baikal

Web Wizard Doesn’t Load on First Run

Symptom: Accessing http://your-server-ip:8080 shows a blank page, a 500 error, or the Baikal dashboard instead of the setup wizard. Fix: The wizard only runs when the config volume is empty. If a previous failed setup left partial configuration files, the wizard won’t appear. Remove the config volume and start fresh:

docker compose down
docker volume rm baikal_config
docker compose up -d

If you see a 500 error, check that the baikal_config and baikal_data volumes are writable by the container. With bind mounts, ensure UID 101 owns the directories.

Upgrade Issues After Updating the Docker Image

Symptom: After pulling a new version of ckulka/baikal, the admin panel shows errors or calendar sync breaks. Fix: Baikal runs database migrations automatically on startup, but occasionally a migration can fail. Check the logs:

docker compose logs baikal

If you see database migration errors, the safest path is:

  1. Stop Baikal: docker compose down
  2. Back up both volumes (see Backup section above)
  3. Start Baikal again: docker compose up -d
  4. If the migration still fails, restore from your backup and wait for a patch release

Pin your image tag (as shown in the Compose examples) so upgrades only happen when you explicitly change the tag.

iOS/macOS Shows “Cannot Verify Server Identity”

Symptom: Apple devices refuse to connect and show certificate warnings. Fix: Apple is strict about SSL certificates. Self-signed certificates will not work. You need a valid certificate from Let’s Encrypt or another CA. Set up a reverse proxy with automatic SSL (see the Reverse Proxy section above). Also ensure your .well-known redirects are configured — Apple clients rely on these for CalDAV/CardDAV discovery.

Resource Requirements

  • RAM: 20-40 MB idle, under 50 MB under normal load
  • CPU: Negligible — CalDAV/CardDAV sync is not compute-intensive
  • Disk: ~50 MB for the application image, calendar and contact data is tiny (a few KB per entry, even heavy users rarely exceed 10 MB)

Baikal is one of the lightest self-hosted services you can run. It fits comfortably on a Raspberry Pi alongside a dozen other containers.

Verdict

Baikal is the best CalDAV/CardDAV server for most people who want calendar and contact sync without the bloat. It has a clean admin panel for managing users and collections, supports both SQLite and MariaDB, works with every major CalDAV/CardDAV client, and uses almost no resources. For a household or small team that just needs reliable calendar and contact sync, Baikal is the right choice.

If you want an even more minimal setup and don’t need a web admin panel, Radicale stores data as flat files and uses even less memory. If you need file sync, office collaboration, or other groupware features alongside calendars and contacts, Nextcloud is the better (but heavier) option. For pure CalDAV/CardDAV, Baikal hits the sweet spot between simplicity and usability.