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:
| Check | Command | Expected Result |
|---|---|---|
| Server is reachable | curl -I https://your-domain/dav.php | HTTP 200 or 401 |
| .well-known works | curl -v https://your-domain/.well-known/carddav | 301 redirect to /dav.php/ |
| Auth works | curl -u user:pass https://your-domain/dav.php/addressbooks/user/default/ -X PROPFIND -H "Depth: 0" | HTTP 207 Multi-Status |
| SSL valid | openssl s_client -connect your-domain:443 | Shows Let’s Encrypt or valid CA |
| PHP limits | docker exec baikal php -i | grep upload_max | 50M 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:
| Purpose | URL |
|---|---|
| Auto-discovery | https://your-domain/.well-known/carddav |
| All address books | https://your-domain/dav.php/addressbooks/USERNAME/ |
| Default address book | https://your-domain/dav.php/addressbooks/USERNAME/default/ |
| CalDAV discovery | https://your-domain/.well-known/caldav |
Per-client setup:
| Client | What to Enter |
|---|---|
| iOS | Settings → 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/ |
| Thunderbird | Address Book → New → CardDAV. URL: https://your-domain/dav.php/addressbooks/USERNAME/default/ (do not use auto-discovery — it hangs on some versions) |
| macOS Contacts | System Settings → Internet Accounts → Add Other Account → CardDAV. Server: your-domain. |
Common mistakes:
- Using
http://instead ofhttps:// - Including a trailing path when the client handles
.well-knowndiscovery - 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:
-
Username format mismatch. Baikal uses usernames, not email addresses. If your Baikal username is
john, enterjohn— not[email protected]. -
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. -
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-knownredirects before adding any clients - Set PHP upload limits to at least 50M from the start
- Test with
curlbefore 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
Related
Get self-hosting tips in your inbox
Get the Docker Compose configs, hardware picks, and setup shortcuts we don't put in articles. Weekly. No spam.
Comments