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 .vcf files 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:

  1. Radicale’s Python engine cannot always expand the ~ symbol, so use an absolute path $HOME/Contacts for your storage folder instead of a tilde.
  2. The value type = none must be strictly lowercase. Using None with 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.

  1. Open your browser and go to http://127.0.0.1:5232
  2. Leave the login blank and click Next
  3. Click Create new addressbook or calendar
  4. Title it something like “My Contacts” and select Address book (CardDAV)
  5. 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:

  1. Open GNOME Settings -> Online Accounts -> Add an account and select Other, then choose CardDAV.
  2. 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)
  3. 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:

  1. Share ~/Contacts via Syncthing between all your devices
  2. On each new machine, repeat Phase 1 (install Radicale as a user service) and Phase 4 (connect GNOME Online Accounts)
  3. 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.


← Back to posts