Repository navigation
Operations
Running Anchor LFS day to day: what state it keeps, backups and restores, configuration changes and upgrades, signing key rotation, moving objects to S3, and removing objects nothing uses any more.
The examples use /var/lib/anchor-lfs as the data directory (the systemd layout) and the endpoint /org/repo. With Docker Compose the data directory is ./data.
Each endpoint's files live in a directory named after its path without the leading slash, with / replaced by _: the endpoint /org/repo uses org_repo.
| Path | Contents | If it is lost |
|---|---|---|
org_repo/ab/cd/<oid> in the data directory, or <s3_prefix>org_repo/ab/cd/<oid> in the bucket |
LFS objects, named by their SHA-256 and never changed once written | Downloads of those objects fail until a clone that has them runs git lfs push --all origin
|
locks/org_repo/locks.json |
The endpoint's file locks | Every lock on the endpoint is released |
locks/org_repo/locks.json.lock |
Empty file used to serialise lock changes | Nothing; it is recreated |
signing.key |
The auto-generated HMAC key, when signing_key is not configured |
A new key is generated at startup, and transfer URLs issued before it fail |
.tmp-* anywhere in the data directory |
Writes that have not finished | Nothing; ones older than an hour are removed automatically |
Locks and the signing key stay in the data directory with either storage backend. The configuration file, and any .env file or environment variables, are the rest of the state. They may hold the signing key and S3 credentials, so keep them as private as signing.key.
Copy the data directory, skipping temporary files:
rsync -a --exclude '.tmp-*' /var/lib/anchor-lfs/ /backup/anchor-lfs/This is safe while the server runs: objects and signing.key never change once written, and locks.json is replaced by an atomic rename, so every copied file is complete. Repeated runs copy only new objects.
With S3 storage the same command backs up locks and the signing key. Protect the objects with your provider's bucket versioning or replication to a second bucket.
Anyone holding signing.key can create transfer URLs for every object, so store backups as securely as the server itself.
- Stop the server
- Copy the backup back:
rsync -a /backup/anchor-lfs/ /var/lib/anchor-lfs/ - With Docker on Linux, give the files back to the container user:
sudo chown -R 100:101 ./data - Start the server and check
curl http://localhost:5420/readyreturns200
Anything that changed after the backup was taken is lost:
-
Objects uploaded since: the batch API reports them as missing. A clone that has them uploads them again with
git lfs push --all origin, which sends only the objects the server lacks. -
Locks taken or released since: list them with
git lfs locksand release stale ones withgit lfs unlock --force <path>(see File Locking). -
A different
signing.key: transfer URLs issued in the lasturl_expiryfail once, and running the Git command again succeeds.
The configuration file and environment variables are read once, at startup. To apply a change, restart the server; it waits up to shutdown_timeout for transfers in progress.
-
systemd:
sudo systemctl restart anchor-lfs -
Docker Compose,
config.tomlchanged:docker compose restart -
Docker Compose, environment changed (
docker-compose.ymlor.env):docker compose up -d, which recreates the container with the new environment
Run anchor-lfs check before restarting (docker compose run --rm anchor-lfs check with Docker Compose): it lists every problem in the new configuration and exits 1, so a mistake never takes the server down. An invalid configuration that reaches a restart stops the server at startup with every problem logged in docker compose logs or journalctl -u anchor-lfs.
Release images and their tags are listed under Published Images. In production, pin a minor version such as 0.1 in docker-compose.yml rather than latest, so a pull brings fixes but no unexpected changes.
- Read the release notes
- Back up the data directory
- Upgrade:
-
Published image: change the tag if needed, then
docker compose pull && docker compose up -d - Release binary: download and check the new archive, replace the binary and restart the service
-
Your own build:
git fetch --tags && git checkout v0.2.0, thenmake docker-build && docker compose up -d, ormake buildand restart the service
-
Published image: change the tag if needed, then
- Check that
curl http://localhost:5420/healthreports the new version and/readyreturns200
To roll back, deploy the previous version the same way. An option added in the newer release is an unknown key to the older one, which then refuses to start, so remove such options from the configuration first. If the release notes mention a change to stored data, restore the backup taken before upgrading.
Anyone holding the signing key can create transfer URLs for any object on any endpoint, so replace it if it may have leaked.
Configured key (signing_key, ANCHOR_LFS_SIGNING_KEY or ANCHOR_LFS_SIGNING_KEY_FILE):
- Generate a new key:
openssl rand -hex 32 - Put it in the configuration, environment or key file in place of the old one
- Restart the server
Auto-generated key:
- Stop the server
- Delete
signing.keyfrom the data directory - Start the server, which generates a new key
Transfer URLs signed with the old key are rejected with 403. Only those issued in the last url_expiry (default 10 minutes) were still valid, and Git LFS clients that hit one report an error and succeed when the command is run again, so rotate during a quiet period.
Presigned S3 URLs are signed with the S3 credentials, not this key. To revoke them, rotate the access key with your provider and update s3_access_key_id and s3_secret_access_key.
The bucket uses the same layout as the data directory under s3_prefix, so copying each endpoint's directory is the whole migration. The commands use the AWS CLI; for other providers add --endpoint-url.
- Create the bucket and credentials (see S3-Compatible Storage)
- Copy each endpoint's objects, skipping temporary files:
aws s3 sync /var/lib/anchor-lfs/org_repo s3://my-lfs-bucket/lfs/org_repo --exclude '*.tmp-*' - Set
backend = "s3"and the bucket settings under[storage], then restart the server - Run the same
aws s3 syncagain to copy objects uploaded between the first copy and the restart - Check that downloads work, for example with
git lfs fetch --allin a fresh clone, then delete the endpoint directories (org_repo) from the data directory
Keep the data directory itself: locks and the signing key stay there. To move from S3 back to local storage, swap the source and destination of aws s3 sync, and on Docker for Linux run sudo chown -R 100:101 ./data afterwards.
Anchor LFS never deletes an object, so every version of every LFS file is kept and storage only grows. With local storage, an upload is refused with 507 once the disk lacks room for it. The Git LFS API has no way to delete objects, and the server cannot tell which objects the repository's history still uses, so pruning is done by hand with a mirror of the repository.
For the endpoint /org/repo with local storage, in an empty working directory:
- List every object that any branch, tag or pull request uses:
Stop if the count is
git clone --mirror https://github.com/org/repo.git repo.git git -C repo.git lfs ls-files --all --long | cut -d ' ' -f 1 | sort -u > keep.txt wc -l < keep.txt
0: every object would be deleted. - List stored objects older than a day:
find /var/lib/anchor-lfs/org_repo -type f -mmin +1440 ! -name '.tmp-*' | sed 's|.*/||' | sort > stored.txt
- List the stored objects nothing uses, and review them before deleting anything:
comm -23 stored.txt keep.txt > prune.txt wc -l < prune.txt
- Delete them (in bash or zsh):
while read -r oid; do rm -- "/var/lib/anchor-lfs/org_repo/${oid:0:2}/${oid:2:2}/$oid"; done < prune.txt
For S3 storage, replace step 2 with:
cutoff=$(date -u -v-1d +%Y-%m-%dT%H:%M:%S 2>/dev/null || date -u -d '1 day ago' +%Y-%m-%dT%H:%M:%S)
aws s3api list-objects-v2 --bucket my-lfs-bucket --prefix lfs/org_repo/ \
--query 'Contents[].[LastModified, Key]' --output text |
awk -v cutoff="$cutoff" '$1 < cutoff { sub(/.*\//, "", $2); print $2 }' | sort > stored.txtand step 4 with:
while read -r oid; do aws s3 rm "s3://my-lfs-bucket/lfs/org_repo/${oid:0:2}/${oid:2:2}/$oid"; done < prune.txtThings to know before pruning:
- The one-day cut-off protects objects from a push still in progress: Git LFS uploads objects before it pushes the commits that use them.
- Objects used only outside the repository (unpushed branches, stashes, other remotes) are deleted too. A client that still has them uploads them again when it pushes those commits.
- With S3 bucket versioning, deleted objects stay recoverable, and billed, until a lifecycle rule expires old versions.
Proxied uploads over 8 MiB use S3 multipart uploads, which the server aborts when an upload fails. One cut short by a crash leaves its parts in the bucket, where they are not listed but are billed. A lifecycle rule removes them after a day:
aws s3api put-bucket-lifecycle-configuration --bucket my-lfs-bucket --lifecycle-configuration \
'{"Rules":[{"ID":"abort-incomplete-uploads","Status":"Enabled","Filter":{"Prefix":"lfs/"},"AbortIncompleteMultipartUpload":{"DaysAfterInitiation":1}}]}'This command replaces any lifecycle rules the bucket already has, so add the rule to them instead if there are some.
- Storage Backends: Local and S3 storage setup
- Running the Server: Shutdown, health checks and logging
- Security: Signed URLs and the signing key