Self-hosting
Set up with Docker
Run the sync server in Docker behind the reverse proxy you already have, in eight steps.
On this page
USERwith the account you use to log in to the server over SSHSERVER_IPwith the server’s IP address (or a host alias from your~/.ssh/config)sync.example.comwith the domain you want to use for sync
Step 1: DNS
sync (or whatever name you want) that points to SERVER_IP.
dig +short sync.example.comdig +short AAAA sync.example.comStep 2: Copy the files to the server
scp can’t use sudo, so the files go to your home folder first and are then moved into place as root. From the repository folder on your computer:
ssh USER@SERVER_IP mkdir -p burrow-syncscp server/* server/.dockerignore USER@SERVER_IP:~/burrow-sync/server/* skips hidden files like a local .env, which is why .dockerignore is listed on its own.
ssh USER@SERVER_IPsudo -imv /home/USER/burrow-sync /opt/burrow-sync
chown -R root:root /opt/burrow-sync
chmod 700 /opt/burrow-syncdocker compose versionStep 3: Find your reverse proxy
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'0.0.0.0:443->... under PORTS is your proxy. Its IMAGE tells you which one it is:
jc21/nginx-proxy-manager | ||
caddy | ||
traefik | ||
nginx |
PROXY below.
systemctl status nginx caddy apache2 --no-pager 2>/dev/null | grep -E '●|Active'PROXY_DIR below:
docker inspect PROXY --format '{{ index .Config.Labels "com.docker.compose.project.working_dir" }}'docker network connect burrow-sync PROXY works until the container is recreated).
docker inspect PROXY --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}'Step 4: Start the sync server
--internal means containers on it can’t reach the internet. Only the sync server and your proxy will join it.
docker network create --internal burrow-synccd /opt/burrow-sync
echo "REGISTRATION_CODE=$(openssl rand -hex 16)" > .env
chmod 600 .env
cat .env
docker compose up -d --buildcat .env prints. You need it in step 7.
docker compose ps should show healthy under STATUS. If it doesn’t, docker compose logs shows why.
Step 5: Connect the proxy
For 5A to 5D: add the proxy to the sync network
compose.yml or docker-compose.yaml):
cd PROXY_DIR
cp docker-compose.yml docker-compose.yml.backup
nano docker-compose.ymlservices:.
networks: list, add burrow-sync to it:
caddy:
image: caddy:2
networks:
- web # was already there
- burrow-sync # newnetworks: list, add one with both default and burrow-sync. Without default the proxy loses its connection to your other services:
caddy:
image: caddy:2
networks:
- default
- burrow-syncnetworks: block already exists, add just the two inner lines to it:
networks:
burrow-sync:
external: truedocker compose config --quiet && docker compose up -dconfig usually means wrong indentation (spaces only, no tabs). cp docker-compose.yml.backup docker-compose.yml restores the old file.
burrow-sync and your proxy:
docker network inspect burrow-sync --format '{{range .Containers}}{{.Name}} {{end}}'Configure your proxy
Open the admin UI, usually http://SERVER_IP:81.Go to Hosts → Proxy Hosts → Add Proxy Host. On the Details tab set Domain Names to sync.example.com, Scheme tohttp, Forward Hostname / IP toburrow-sync, Forward Port to3000, and tick Block Common Exploits.On the SSL tab choose Request a new SSL Certificate and tick Force SSL and HTTP/2 Support. Save. The certificate is issued within a few seconds.
X-Real-IP to the client’s address by default. Continue at step 6.
/etc/caddy/Caddyfile (or /etc/caddy). The left side is your Caddyfile. Add this block at the end:
sync.example.com {
reverse_proxy burrow-sync:3000 {
header_up X-Real-IP {remote_host}
}
}docker exec PROXY caddy reload --config /etc/caddy/Caddyfiledocker inspect PROXY --format '{{range .Args}}{{println .}}{{end}}' | grep -E 'entrypoints|certificatesresolvers'--entrypoints.NAME.address=:443 (often websecure) and --certificatesresolvers.NAME... (often letsencrypt or le). If nothing shows up, they are in a traefik.yml in PROXY_DIR.
/opt/burrow-sync/docker-compose.yml and add this right below container_name: burrow-sync, with your two names in place of websecure and letsencrypt:
labels:
- traefik.enable=true
- traefik.docker.network=burrow-sync
- traefik.http.routers.burrow-sync.rule=Host(`sync.example.com`)
- traefik.http.routers.burrow-sync.entrypoints=websecure
- traefik.http.routers.burrow-sync.tls.certresolver=letsencrypt
- traefik.http.services.burrow-sync.loadbalancer.server.port=3000docker compose up -d in /opt/burrow-sync. Continue at step 6.
/etc/nginx/conf.d. The left side is the folder with the site configs. Look at an existing one to see where its certificates come from (ssl_certificate and ssl_certificate_key), and get a certificate for sync.example.com the same way (usually certbot).
sync.conf in that folder, using the same certificate path pattern:
server {
listen 80;
server_name sync.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name sync.example.com;
ssl_certificate /etc/letsencrypt/live/sync.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sync.example.com/privkey.pem;
client_max_body_size 6m;
location / {
proxy_pass http://burrow-sync:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}docker exec PROXY nginx -tdocker exec PROXY nginx -s reload127.0.0.1:3100. Only programs on the server itself can reach that address. Start it with the other Compose file:
cd /opt/burrow-sync
echo "REGISTRATION_CODE=$(openssl rand -hex 16)" > .env
chmod 600 .env
cat .env
docker compose -f docker-compose.host-proxy.yml up -d --buildcurl http://127.0.0.1:3100/api/health{"ok":true}. From now on, use docker compose -f docker-compose.host-proxy.yml ... wherever this guide says docker compose ....
/etc/nginx/sites-available/sync with:
server {
listen 80;
server_name sync.example.com;
client_max_body_size 6m;
location / {
proxy_pass http://127.0.0.1:3100;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}ln -s /etc/nginx/sites-available/sync /etc/nginx/sites-enabled/sync
nginx -t && systemctl reload nginx
certbot --nginx -d sync.example.com/etc/caddy/Caddyfile and run systemctl reload caddy:
sync.example.com {
reverse_proxy 127.0.0.1:3100 {
header_up X-Real-IP {remote_host}
}
}http://127.0.0.1:3100, allow request bodies of at least 6 MB, and overwrite X-Real-IP with the client’s address.
Step 6: Check
curl https://sync.example.com/api/health{"ok":true}. If not:
Could not resolve host: the DNS record from step 1 isn’t visible yet. Wait a few minutes.A certificate error: the proxy has no certificate yet. Wait a moment and check its logs with docker logs PROXY --tail 50.502 Bad Gateway: the proxy can’t reach the sync server. For 5A to 5D, check that both are on theburrow-syncnetwork.
OTHER is its name from docker ps):
docker inspect OTHER --format '{{range $name, $_ := .NetworkSettings.Networks}}{{println $name}}{{end}}'bad address or a timeout:
docker run --rm --network NETWORK_NAME alpine wget -qO- -T 3 http://burrow-sync:3000/api/health{"ok":true}, that container is on the burrow-sync network and should be removed from it.
Step 7: Create accounts
Server: https://sync.example.comUser name: 3 to 32 characters from a-z, 0-9, dot, underscore and dash Account password: a strong password you don’t use anywhere else Invite code: the code from step 4
Step 8: Turn registration off
cd /opt/burrow-sync
echo "REGISTRATION_CODE=" > .env
docker compose up -d