Self-hosted Anki sync server

Anki has been distributed with a built-in sync server for several years now; initially it was pretty bare bones and didn’t support the syncing of media, but today it’s fairly full featured.

And it just so happens that I’m in need of a self-hosted Anki sync server, having abandoned AnkiWeb a few years ago, which so far hasn’t been a problem, but I’m increasingly feeling burdened by not having the Anki iOS app1 up to date with my laptop. Also in part a prod from a conversation with Saul Munn, who, I was surprised to learn, does the bulk of his reviews on his phone.

Let’s start with the simplest, stupidest approach: using the local network to sync between two devices.

I’m on macOS so the Anki binary lives at /Applications/Anki.app/Contents/MacOS/anki, so we’ll invoke the sync server like so:

$ env SYNC_USER1=user:pass /Applications/Anki.app/Contents/MacOS/anki --syncserver

This will start the sync server with the login credentials of user:pass, and with a default base folder of ~/.syncserver. This folder is where the Anki database2 will be stored; this is configurable through an environment variable. Once that is running you can open up Anki app and under Settings → Syncing set the Self-hosted sync server to http://localhost:8080. Then exit and hit Y, and you’ll be prompted to login with the user:pass you specified at startup, after which you’ll see logs emitted from the sync server attesting to the sync.

$ env SYNC_USER1=user:pass /Applications/Anki.app/Contents/MacOS/anki --syncserver
Anki starting...
2026-02-02T15:19:40.692237Z  INFO listening addr=0.0.0.0:8080
2026-02-02T15:19:43.584953Z  INFO request{uri="/sync/meta" ip="127.0.0.1" uid="user" client="25.02.5,29192d15,macos" session="cVcHAj"}: finished elap_ms=4 httpstatus=200
2026-02-02T15:19:43.589672Z  INFO request{uri="/sync/start" ip="127.0.0.1" uid="user" client="25.02.5,29192d15,macos" session="cVcHAj"}: finished elap_ms=0 httpstatus=200
2026-02-02T15:19:43.595830Z  INFO request{uri="/sync/applyChanges" ip="127.0.0.1" uid="user" client="25.02.5,29192d15,macos" session="cVcHAj"}: finished elap_ms=1 httpstatus=200
2026-02-02T15:19:43.596796Z  INFO request{uri="/sync/chunk" ip="127.0.0.1" uid="user" client="25.02.5,29192d15,macos" session="cVcHAj"}: finished elap_ms=0 httpstatus=200
2026-02-02T15:19:43.608980Z  INFO request{uri="/sync/applyChunk" ip="127.0.0.1" uid="user" client="25.02.5,29192d15,macos" session="cVcHAj"}: finished elap_ms=8 httpstatus=200
2026-02-02T15:19:43.626816Z  INFO request{uri="/sync/sanityCheck2" ip="127.0.0.1" uid="user" client="25.02.5,29192d15,macos" session="cVcHAj"}: finished elap_ms=9 httpstatus=200
2026-02-02T15:19:43.633878Z  INFO request{uri="/sync/finish" ip="127.0.0.1" uid="user" client="25.02.5,29192d15,macos" session="cVcHAj"}: finished elap_ms=5 httpstatus=200

Now the contents of ~/.syncserver will look like this. Perhaps with the addition of some SQLite WAL files.

$ pwd
/Users/si/.syncserver
$ eza 
user
$ eza user
collection.anki2  media  media.db

All that’s left is to do the same thing but on iOS. The only difference here being the IP address that you’re pointing at, connect to the same network, and use the address that the sync server is hosted on, and you’re off to the races.


While an improvement over independent mobile and laptop instances, there’s an annoying amount of friction introduced in this peer-to-peer scheme. Having to connect over the same network, sync one side, and then sync the other, really breaks the feeling of seamless continuity between devices. Instead, the setup that I’ve been using for several months now, and which has been working well, is to use a small VPS that’s within my Tailnet as my syncserver. Both my phone and laptop are within this same Tailnet already, so I needn’t expose this server to the public internet.

I personally find a ClickOps solution to be untenable, so I spent the incremental couple of hours converting my bare-bones setup to Terraform, and verifying that the destroy-create cycle works as expected. See the repository here.

I’m using DigitalOcean as my VPS provider here, creating a minimal 1vcpu-1gb machine hosted in sfo3. Then in the initialisation script we (1) install Tailscale, and authenticate to our Tailnet; (2) set up an additional layer of network protection with ufw for the virtual NIC, which is an addition to the digitalocean_firewall that we attached to the droplet; and then (3) install the Anki sync server from the Python distribution, and run it under systemd.

For some darn reason that I have yet to debug, I couldn’t get the Anki process to play nicely with journalctl, so I’m just redirecting the output to /var/log. It’s now incumbent on me to do some manual log rotation, in approximately seven years.

And now we just point the Self-hosted sync server field in Anki’s settings at our internal-to-Tailnet IP of the above host, and sync away.


  1. Which I bought solely for the purpose of supporting development, as Damien doesn’t have any other mechanism to support the open-source project; and it’s the sole(?) source of revenue.↩︎

  2. Databases. Anki has two main SQLite databases, collection.anki2 which stores the contents of the cards and the revision history, and media.db which stores the mapping between all media references and their place on disk.↩︎