Run your own sync server.
Donut Sync is the small server that backs cross-device sync for profiles, proxies, groups, extensions, and (optionally) the full profile browser data. Sync through Donut’s hosted cloud is a paid feature, but if you run the server yourself it is completely free. Your data never leaves storage you control.
The server is a single container that talks to any S3-compatible bucket. You can run the bundled MinIO for a fully local setup, or point it at AWS S3, Cloudflare R2, Backblaze B2, or anything else that speaks the S3 API.
What you need
- Docker with Docker Compose (
docker compose, included in modern Docker Desktop / Engine). - An S3-compatible bucket. The quick start below runs MinIO for you, so you do not need one to start.
Quick start
Create a docker-compose.yml. This runs Donut Sync plus a local MinIO, so everything stays on one machine:
services:
donut-sync:
image: donutbrowser/donut-sync:latest
ports:
- "3929:3929"
environment:
# A long random secret. Clients present this as their bearer token.
SYNC_TOKEN: change-me-to-a-long-random-secret
PORT: "3929"
# Talk to the MinIO container on the compose network.
S3_ENDPOINT: http://minio:9000
S3_REGION: us-east-1
S3_ACCESS_KEY_ID: minioadmin
S3_SECRET_ACCESS_KEY: minioadmin
S3_BUCKET: donut-sync
S3_FORCE_PATH_STYLE: "true"
depends_on:
minio:
condition: service_healthy
restart: unless-stopped
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
volumes:
- minio_data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
# Optional: publish the MinIO console to inspect stored objects.
# ports:
# - "9001:9001"
volumes:
minio_data:
Start it:
docker compose up -d
The bucket named in S3_BUCKET is created automatically on first connect, so there is no manual setup step.
Verify it is running
# Liveness: the server process is up.
curl http://localhost:3929/health
# {"status":"ok"}
# Readiness: the server can reach S3.
curl http://localhost:3929/readyz
# {"status":"ready","s3":true}
If /readyz returns HTTP 503 with {"status":"not ready","s3":false}, the server started but cannot reach your storage. Re-check the S3_* values and that the bucket endpoint is reachable from the container.
Environment variables
| Variable | Required | Default | Description |
|---|---|---|---|
SYNC_TOKEN | Yes, for self-hosting | none | Bearer token clients send in the Authorization header. Use a long random value. |
PORT | No | 3929 | Port the server listens on. |
S3_ENDPOINT | No | http://localhost:8987 | S3-compatible endpoint URL. Set this for every real deployment (see below). The default targets a local dev MinIO. |
S3_REGION | No | us-east-1 | Storage region. Use auto for Cloudflare R2. |
S3_ACCESS_KEY_ID | No | minioadmin | Access key. |
S3_SECRET_ACCESS_KEY | No | minioadmin | Secret key. |
S3_BUCKET | No | donut-sync | Bucket name. Created automatically if it does not exist. |
S3_FORCE_PATH_STYLE | No | true | Path-style addressing. Keep true for MinIO, R2, and most S3-compatible services. Set to false for AWS S3, which uses virtual-hosted addressing. |
SYNC_JWT_PUBLIC_KEY | No | none | RS256 public key that enables JWT (multi-tenant cloud) auth. Not needed for self-hosting; use SYNC_TOKEN instead. |
Using external S3 storage
Drop the minio service and its depends_on block, then point Donut Sync at your provider. Because the server always sends an explicit endpoint, set S3_ENDPOINT for every provider, including AWS.
AWS S3 (note the explicit endpoint and S3_FORCE_PATH_STYLE: "false"):
services:
donut-sync:
image: donutbrowser/donut-sync:latest
ports:
- "3929:3929"
environment:
SYNC_TOKEN: change-me-to-a-long-random-secret
PORT: "3929"
S3_ENDPOINT: https://s3.us-east-1.amazonaws.com
S3_REGION: us-east-1
S3_ACCESS_KEY_ID: your-aws-access-key
S3_SECRET_ACCESS_KEY: your-aws-secret-key
S3_BUCKET: your-bucket-name
S3_FORCE_PATH_STYLE: "false"
restart: unless-stopped
Cloudflare R2:
services:
donut-sync:
image: donutbrowser/donut-sync:latest
ports:
- "3929:3929"
environment:
SYNC_TOKEN: change-me-to-a-long-random-secret
PORT: "3929"
S3_ENDPOINT: https://<account-id>.r2.cloudflarestorage.com
S3_REGION: auto
S3_ACCESS_KEY_ID: your-r2-access-key
S3_SECRET_ACCESS_KEY: your-r2-secret-key
S3_BUCKET: your-bucket-name
S3_FORCE_PATH_STYLE: "true"
restart: unless-stopped
Other providers (Backblaze B2, DigitalOcean Spaces, Wasabi, and similar) work the same way: set S3_ENDPOINT to the provider’s endpoint and keep S3_FORCE_PATH_STYLE: "true" unless the provider documents virtual-hosted addressing.
Connect Donut Browser
When you turn on sync for a profile (or open the sync settings), Donut Browser shows the Sync Configuration dialog. Fill in:
- Server URL:
http://your-server:3929, or your HTTPS reverse-proxy URL in production. - Sync Token: the exact value you set for
SYNC_TOKEN.
Donut Browser validates the connection by calling /health before saving. Once it is configured, you can enable sync on individual profiles, proxies, groups, and extensions, and the paid “cloud sync” gate is lifted because you are your own backend.
Securing your deployment
- Use a strong token. Generate one with
openssl rand -hex 32and keep it secret. The token is compared in constant time on the server. - Terminate TLS with a reverse proxy. The token travels as a bearer header, so it must not cross plain HTTP in production. Put Caddy, Nginx, or Traefik in front of the server.
- Limit exposure. On a VPS, bind the container to localhost and only expose it through the reverse proxy, or restrict the port with firewall rules.
- Scope your storage credentials. Use dedicated keys that can read and write only the sync bucket.
Caddy makes TLS automatic:
sync.yourdomain.com {
reverse_proxy localhost:3929
}
Nginx, terminating TLS yourself:
server {
listen 443 ssl;
server_name sync.yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:3929;
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;
}
}
With TLS in place, set the Server URL in Donut Browser to https://sync.yourdomain.com.