Why I Pulled My Contacts from the Cloud (And How To Do It Without Losing Your Mind)
TL;DR: Google Contacts feels convenient until you realize your personal address book lives on someone else’s server, behind an API they can change or lock down at any time. This post walks through migrating 700+ contacts to a completely local-first stack running on Linux: Radicale as a lightweight CardDAV server, Syncthing for multi-device sync, and GNOME Contacts as the frontend.
The Moment It Clicked
I run Linux across all my devices. I self-host where it makes sense. I keep data off Big Tech’s servers whenever possible. But one piece of personal information always stayed in the cloud: my contacts.
Google Contacts just worked. New phones picked them up automatically. Sync was so frictionless that leaving never made sense. Until I realized something uncomfortable.
Every relationship in my life, every phone number and email address collected over two decades, lives on someone else’s servers. Subject to their terms of service. Subject to account lockouts. Subject to whatever API decisions Google decides to push next quarter. The export tools they provide are designed to keep you inside the ecosystem, not help you leave it.
So I pulled the plug. Everything comes home.
What You’re Building
The architecture is simpler than it sounds:
- Radicale – a lightweight, local-only CardDAV server that stores contacts as plain
.vcffiles in a directory you own. - Syncthing – synchronizes those raw vCard files across all your devices.
- GNOME Contacts – reads from the local Radicale instance natively via localhost, so everything feels like a normal desktop experience.
No brittle legacy bridges like SyncEvolution. No cloud account logged into GNOME settings. No web service between you and your data. Just files on disk, served locally by software running as a user-level systemd service.
Phase 1: Preparing the Local Server
Getting Radicale Running as a User Service
When you install Radicale via your package manager, it often defaults to running as a locked-down system-wide service that will not have permission to read or write files in your home directory. We need to switch that around.
First, disable the system-wide service if you have that installed:
sudo systemctl disable --now radicale
This frees up port 5232 and stops the system daemon so our user-level service can take over.
Next, configure a local environment. Create a user-level config file:
mkdir -p ~/.config/radicale
cat << EOF > ~/.config/radicale/config
[server]
hosts = 127.0.0.1:5232
[auth]
type = none
[storage]
type = multifilesystem
filesystem_folder = $HOME/Contacts
EOF
Two critical caveats here:
- Radicale’s Python engine cannot always expand the
~symbol, so use an absolute path$HOME/Contactsfor your storage folder instead of a tilde. - The value
type = nonemust be strictly lowercase. UsingNonewith a capital N silently crashes the server on startup.
Now create the systemd user service:
mkdir -p ~/.config/systemd/user
cat << 'EOF' > ~/.config/systemd/user/radicale.service
[Unit]
Description=Radicale Local Contacts Server
After=network.target
[Service]
ExecStart=/usr/bin/radicale
Restart=on-failure
[Install]
WantedBy=default.target
EOF
Enable and start it:
systemctl --user daemon-reload
systemctl --user enable --now radicale
Verify it runs by hitting curl http://127.0.0.1:5232. You should get back an HTML response, not a connection refused error.
The Fedora/Nobara Gotcha
Fedora-based distros (Nobara included) have an additional wrinkle. The radicale3 package includes a security hardening wrapper that forces it to run as a dedicated radicale system user. Since your contact files live in /home/youruser/, the system user cannot access them, and Radicale refuses to launch. If you are like me and tried the system package first you’ll need to switch to pip.
Remove the system package and install via pip instead:
sudo dnf remove radicale3
pip install --user radicale
Update your systemd service file to point to the new binary location and explicitly pass the config path:
[Service]
ExecStart=/home/youruser/.local/bin/radicale --config /home/youruser/.config/radicale/config
Restart=on-failure
Reload and restart. The pip-installed version has no corporate wrapper fighting you on permissions: it runs as your user, reads your files, and moves on with life.
Phase 2: Initializing the Data Structure
Radicale needs a properly-formatted CardDAV collection before you can drop contacts into it. Manually creating directories will not work because Radicale uses RFC-compliant URLs for address book discovery.
- Open your browser and go to
http://127.0.0.1:5232 - Leave the login blank and click Next
- Click Create new addressbook or calendar
- Title it something like “My Contacts” and select Address book (CardDAV)
- Click Create
Copy the collection URL that displays. The UI will show a specific URL for this collection, something like http://127.0.0.1:5232/username/c1d457f9-2568-fcc1-0369-.../. You need the exact URL in the next phase and there is no way to retrieve it again from the UI later.
Phase 3: Migrating Your Existing Contacts
Exporting from Google
You have two options for getting your existing contacts out of Google’s hands:
- Google Takeout: The nuclear option that downloads everything Google has on you, but the contacts export is a clean vCard file buried somewhere in the bundle.
- Google Contacts web UI: Head to contacts.google.com, click Export, select all, then choose vCard format. Faster and more targeted if that is all you need.
Either way, end up with one or more .vcf files and move them into Radicale’s collection directory (inside ~/Contacts/ in the UUID-named subdirectory from Phase 2).
The UID Trap
Most migrations silently break here, and it will cost you hours of debugging if you skip this.
Many vCard files lack a UID: property. Radicale will happily display these files, but GNOME’s Evolution Data Server strictly enforces CardDAV RFC compliance. Any contact file missing a unique ID gets silently dropped and ignored by GNOME Contacts.
Over the course of my own migration involving 700+ vCard files touched by at least three different pieces of software across two decades, I found that the majority had issues like missing UIDs, CRLF line-ending mismatches, and corrupted E164 phone number formatting. Every legacy address book is a crime scene waiting to be investigated.
Run this script against your Radicale directory before connecting GNOME:
find ~/Contacts/ -type f -name "*.vcf" | while read -r file; do
if ! grep -q "^UID:" "$file"; then
new_uid=$(uuidgen)
sed -i "/^BEGIN:VCARD/a UID:${new_uid}" "$file"
fi
done
After running it, restart Radicale so it re-indexes the files:
systemctl --user restart radicale
Now your contacts show up. Every one of them. No silent drops.
Phase 4: GNOME Desktop Integration
The Redirect Trap
If you point GNOME Online Accounts to http://127.0.0.1:5232, Radicale returns an HTTP 302 redirect to its web UI. GNOME freezes trying to follow it and hangs indefinitely. This is not a bug in your setup, it is a fundamental incompatibility between how Radicale handles redirects and how GOA parses responses.
You must provide GNOME with the exact, deep CardDAV URL from Phase 2:
- Open GNOME Settings -> Online Accounts -> Add an account and select Other, then choose CardDAV.
- Fill in:
- Server URL: The full collection URL you copied earlier (
http://127.0.0.1:5232/username/c1d4.../) - Username: Your system username (required by the UI, ignored by Radicale)
- Password: Type
none(required by the UI, ignored by Radicale)
- Server URL: The full collection URL you copied earlier (
- Click Connect
Open GNOME Contacts. Your contacts populate immediately. Any change you make in the app – editing a phone number, adding a birthday, updating an email – updates the .vcf file on disk instantly. No cloud intermediary. No polling interval. Just your desktop writing directly to files that Radicale serves.
Phase 5: Multi-Device Sync via Syncthing
Once it works on one machine, replicating it elsewhere is trivial:
- Share
~/Contactsvia Syncthing between all your devices - On each new machine, repeat Phase 1 (install Radicale as a user service) and Phase 4 (connect GNOME Online Accounts)
- You do not need to repeat Phase 2 or 3 – once Syncthing pulls the folder structure down, the local Radicale instance automatically recognizes the existing collection
Your contacts are now a set of plain text files synced between your machines by open-source software you control. No account credentials shared with a third party. No trusted-device verification screens. The data was yours before, it is yours now, and it stays yours.
What You Gained
This was never about making contacts work – they already worked fine in Google’s ecosystem. This is about something deeper.
You now own your personal address book the same way you own the files on your hard drive. A Terms of Service update cannot change what happens to them. A sudden API deprecation cannot break every app touching your data. An account suspension cannot render 700 relationships inaccessible because Google’s automated systems flagged something. The data lives on your hardware, served by software you installed, synced between machines by a protocol built around the simple idea that files should match across devices.
It took me a weekend to build this out properly and debug every trap along the way. Knowing my phone numbers and email addresses are not sitting on someone else’s servers waiting for a policy change was worth every minute of it.
Stay tuned for a follow-up post covering how to automate contact management with CLI tools (khard) and AI, so you can add, update, and deduplicate contacts without ever opening a GUI.
Relevant Links
- Radicale: https://radicale.org/
- Syncthing: https://syncthing.net/
- vCard Format (RFC 6350): https://tools.ietf.org/html/rfc6350
- Google Takeout: https://takeout.google.com/