Baikal: CardDAV Contacts Not Syncing — Fix

The Problem

Your Baikal server is running but contacts won’t sync to your devices. iOS shows “Unable to update contacts” or silently fails. macOS only syncs one address book. Android (DAVx5) gives authentication errors. Thunderbird hangs during discovery.

Updated March 2026: Verified with latest Docker images and configurations.

These issues almost always trace back to one of five causes: broken .well-known redirects, SSL certificate problems, PHP upload limits blocking contact photos, wrong URL formats, or Docker port mapping mistakes.

Quick Diagnosis

Run through this checklist to narrow down the issue:

CheckCommandExpected Result
Server is reachablecurl -I https://your-domain/dav.phpHTTP 200 or 401
.well-known workscurl -v https://your-domain/.well-known/carddav301 redirect to /dav.php/
Auth workscurl -u user:pass https://your-domain/dav.php/addressbooks/user/default/ -X PROPFIND -H "Depth: 0"HTTP 207 Multi-Status
SSL validopenssl s_client -connect your-domain:443Shows Let’s Encrypt or valid CA
PHP limitsdocker exec baikal php -i | grep upload_max50M or higher

If any of these fail, jump to the matching fix below.

Fix 1: .well-known Redirect Returns Wrong URL

Symptom: iOS says “Unable to update contacts.” Thunderbird hangs during auto-discovery. DAVx5 can’t find the server.

Cause: When Baikal runs behind a reverse proxy, the .well-known/carddav redirect returns the internal hostname or HTTP scheme instead of your public HTTPS URL. iOS enforces RFC 7231 strictly — if the redirect says http://localhost/dav.php, iOS refuses to follow it.

Fix for Nginx Proxy Manager / Nginx:

Add this to your server block or NPM custom location:

location /.well-known/carddav {
    return 301 https://$http_host/dav.php/;
}
location /.well-known/caldav {
    return 301 https://$http_host/dav.php/;
}

The key is using $http_host (preserves the original hostname) and explicitly using https://.

Fix for Traefik:

Add a redirect middleware in your labels:

labels:
  - "traefik.http.middlewares.wellknown-carddav.redirectregex.regex=^https://(.*)/.well-known/carddav"
  - "traefik.http.middlewares.wellknown-carddav.redirectregex.replacement=https://$${1}/dav.php/"
  - "traefik.http.middlewares.wellknown-carddav.redirectregex.permanent=true"

Fix for Caddy:

your-domain.com {
    redir /.well-known/carddav /dav.php/ permanent
    redir /.well-known/caldav /dav.php/ permanent
    reverse_proxy baikal:80
}

Verify the fix:

curl -v https://your-domain/.well-known/carddav

The Location header must show https://your-domain/dav.php/ — not http://, not localhost, not an internal hostname.

Fix 2: SSL Certificate Issues

Symptom: iOS, macOS, and Thunderbird refuse to connect. Android (DAVx5) shows “Certificate not trusted.”

Cause: Self-signed certificates are rejected by all major CardDAV clients. Even expired Let’s Encrypt certificates trigger silent sync failures on iOS.

Fix:

Use Let’s Encrypt via your reverse proxy. If you’re using the ckulka/baikal Docker image with Apache, you can add certbot directly, but a reverse proxy is simpler:

# docker-compose.yml with Caddy for automatic HTTPS
services:
  baikal:
    image: ckulka/baikal:0.11.1-apache
    container_name: baikal
    volumes:
      - baikal-config:/var/www/baikal/config
      - baikal-data:/var/www/baikal/Specific
    restart: unless-stopped

  caddy:
    image: caddy:2.11.2
    container_name: caddy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
    restart: unless-stopped

volumes:
  baikal-config:
  baikal-data:
  caddy-data:

With a Caddyfile:

baikal.your-domain.com {
    redir /.well-known/carddav /dav.php/ permanent
    redir /.well-known/caldav /dav.php/ permanent
    reverse_proxy baikal:80
}

Caddy handles Let’s Encrypt automatically — no configuration needed.

Verify:

openssl s_client -connect your-domain:443 -servername your-domain 2>/dev/null | head -20

You should see i:C = US, O = Let's Encrypt (or another trusted CA), not self-signed.

Fix 3: Contact Photos Won’t Sync

Symptom: Contacts without photos sync fine. Any contact with a photo either fails silently or the photo is missing after sync.

Cause: PHP’s default upload_max_filesize is 2M. High-resolution contact photos encoded in base64 inside vCards can exceed this limit. The sync fails silently — no error message, the contact just doesn’t appear.

Fix:

Create a custom php.ini or set PHP values in your Docker Compose:

services:
  baikal:
    image: ckulka/baikal:0.11.1-apache
    container_name: baikal
    volumes:
      - baikal-config:/var/www/baikal/config
      - baikal-data:/var/www/baikal/Specific
      - ./custom-php.ini:/usr/local/etc/php/conf.d/99-baikal.ini
    restart: unless-stopped

volumes:
  baikal-config:
  baikal-data:

Create custom-php.ini:

upload_max_filesize = 50M
post_max_size = 50M
memory_limit = 256M

Verify:

docker exec baikal php -i | grep -E "upload_max_filesize|post_max_size"

Both should show 50M.

Fix 4: Wrong CardDAV URL Format

Symptom: Client connects but shows no contacts, or returns 404 errors.

Cause: Different clients expect different URL formats. Using the wrong one results in either a 404 or an empty address book.

Correct URLs for Baikal:

PurposeURL
Auto-discoveryhttps://your-domain/.well-known/carddav
All address bookshttps://your-domain/dav.php/addressbooks/USERNAME/
Default address bookhttps://your-domain/dav.php/addressbooks/USERNAME/default/
CalDAV discoveryhttps://your-domain/.well-known/caldav

Per-client setup:

ClientWhat to Enter
iOSSettings → Contacts → Accounts → Add Account → Other → Add CardDAV Account. Server: your-domain (no path needed — iOS uses .well-known). Username and password.
DAVx5 (Android)Add account → Login with URL. Enter: https://your-domain/dav.php/addressbooks/USERNAME/default/
ThunderbirdAddress Book → New → CardDAV. URL: https://your-domain/dav.php/addressbooks/USERNAME/default/ (do not use auto-discovery — it hangs on some versions)
macOS ContactsSystem Settings → Internet Accounts → Add Other Account → CardDAV. Server: your-domain.

Common mistakes:

  • Using http:// instead of https://
  • Including a trailing path when the client handles .well-known discovery
  • Using email address format when Baikal expects a username
  • Forgetting the trailing slash on the address book URL

Fix 5: macOS Only Syncs First Address Book

Symptom: You have multiple address books in Baikal but macOS Contacts only shows the first one.

Cause: This is a macOS Contacts.app limitation. It only subscribes to the first address book discovered on a CardDAV account. iOS handles multiple address books correctly from the same server.

Workaround:

Create a separate CardDAV account in macOS System Settings for each address book, pointing each to the specific address book URL:

Account 1: https://your-domain/dav.php/addressbooks/user/personal/
Account 2: https://your-domain/dav.php/addressbooks/user/work/
Account 3: https://your-domain/dav.php/addressbooks/user/family/

This is clunky but works. Alternatively, consolidate to a single address book and use contact groups for organization.

Fix 6: Authentication Failures

Symptom: 401 Unauthorized when connecting, even with correct credentials.

Causes and fixes:

  1. Username format mismatch. Baikal uses usernames, not email addresses. If your Baikal username is john, enter john — not [email protected].

  2. Special characters in password. Some clients don’t handle special characters (@, #, &) in passwords well over CardDAV. Try a password with only alphanumeric characters and basic symbols.

  3. Reverse proxy stripping auth headers. If using Nginx, ensure proxy_pass_header Authorization; is set:

location / {
    proxy_pass http://baikal:80;
    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;
    proxy_pass_header Authorization;
}

Test authentication directly:

curl -u USERNAME:PASSWORD https://your-domain/dav.php/ -X PROPFIND -H "Depth: 0"

HTTP 207 = auth works. HTTP 401 = wrong credentials or auth headers stripped.

Prevention

  • Always use Let’s Encrypt or another trusted CA — never self-signed certificates for CardDAV
  • Configure .well-known redirects before adding any clients
  • Set PHP upload limits to at least 50M from the start
  • Test with curl before configuring clients — it gives clearer error messages
  • Keep Baikal updated — CardDAV compatibility improvements ship in most releases
  • Document your Baikal username and the exact URL format your clients need

Comments