GIT LFS · S3 PROXY

Run your own Git LFS
in minutes.

Git LFS is a Git extension that swaps large files for small text pointers, keeping heavy binaries out of the repository. git-lfs3 is a stateless Git LFS server on Cloudflare Pages — it signs short-lived URLs so your client transfers files straight to your own S3-compatible bucket.

Why git-lfs3

GitHub's built-in LFS meters both storage and transfer, and the free tier is shared across every repository and fork. Backed by Cloudflare R2, git-lfs3 makes transfer free and drops storage to a fraction of the price.

MetricGitHub LFSgit-lfs3 + R2
Storage$0.07 / GB·mo$0.015 / GB·mo
Transfer (egress)$0.0875 / GiB over 10 GiBFree
Free tier10 GiB shared, all repos + forks10 GB storage · unlimited bandwidth
URL latency~270 ms~20–100 ms · same datacenters

Latency is low enough to serve whole websites straight from LFS, bypassing the 25 MiB Cloudflare Pages file-size limit.

How it works

git-lfs3 implements the Git LFS Batch API. For each object your client transfers, the worker presigns a time-limited, path-style S3 URL from the credentials in your LFS URL and returns it. Your client then uploads or downloads bytes directly to the bucket.

The worker is stateless. It never stores your credentials and makes no outbound request on your behalf. See SECURITY.md for the full trust model.

Path-segment options

Supply region, service, or sessionToken as key=value segments before the endpoint. service defaults to s3. R2 works with the defaults; Amazon S3 requires the bucket's region.

…@<INSTANCE>/region=auto/<ENDPOINT>/<BUCKET>

Consequences of statelessness

  • The worker can't tell whether an object already exists, so git lfs push re-uploads every object. An S3 PUT of identical content is idempotent, so this is harmless — just extra transfer.
  • It still returns a signed URL for an object missing from the bucket; the failure surfaces later as an S3 transfer error, not a Git LFS "not found".
  • It signs at most 1000 objects per batch; Git LFS chunks larger transfers automatically.

Deploy the proxy

Fork the repository, then create a Cloudflare Pages project connected to your fork with these settings:

Framework presetNone
Build commandnpm install --omit=dev
Build output directory/

--omit=dev installs aws4fetch for Cloudflare's bundler while skipping the local-dev and test tooling, so it isn't uploaded as static assets.

After the first deploy, Cloudflare assigns a *.pages.dev hostname — this becomes your <INSTANCE> in the steps below. You can also attach a custom domain.

Signed URL lifetime
Set the EXPIRY environment variable (seconds; default 3600). It is clamped to S3's allowed range of 1–604800 seconds (7 days). Empty, unset, or non-numeric values fall back to the default.

Visiting the deployment in a browser shows a short landing page explaining how to point a Git client at it — try lfs.khws.io.

Connect a bucket

On any S3-compatible object store, create a bucket for your LFS assets and an access key with read/write permission. The credential is always a pair: an access key ID such as AKIAIOSFODNN7EXAMPLE and a secret such as wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY. URL-encode either value if it contains non-alphanumeric characters.

Pick a provider for setup links and the endpoint format. R2 has the most generous free tier and shares datacenters with the LFS client worker.

Build the server URL

The LFS server URL packs the credential, instance, endpoint, and bucket into one string:

https://<ACCESS_KEY_ID>:<SECRET_ACCESS_KEY>@<INSTANCE>/<ENDPOINT>/<BUCKET>

It's fiddly to assemble by hand. Fill the fields below and copy the result. Nothing leaves your browser — the builder runs entirely client-side.

Result

Install Git LFS

Check whether Git LFS is already present:

git lfs version

If the command reports git: 'lfs' is not a git command, follow the upstream installation instructions. Then set up the smudge and clean filters for your user account:

git lfs install

Point Git at the proxy

Two approaches, depending on who can push.

Separate read and write keys

The committed .lfsconfig carries a read-only URL; the read/write key reaches each clone out-of-band. Use this for public repos and most private repos with several collaborators. Create a read-only access key, build a URL with it, and commit it:

cd "$(git rev-parse --show-toplevel)"   # repository root
    git config -f .lfsconfig lfs.url 'https://<RO_KEY>:<RO_SECRET>@<INSTANCE>/<ENDPOINT>/<BUCKET>'
    git add .lfsconfig
    git commit -m "Add .lfsconfig"

Grant a clone write access by adding a read/write URL to its .git/config. Git never commits .git/config, so the key stays private:

git config lfs.url 'https://<RW_KEY>:<RW_SECRET>@<INSTANCE>/<ENDPOINT>/<BUCKET>'

Shared read/write key

Simpler, less secure: put the read/write URL directly in .lfsconfig. Anyone with a clone can then push.

git config -f .lfsconfig lfs.url 'https://<RW_KEY>:<RW_SECRET>@<INSTANCE>/<ENDPOINT>/<BUCKET>'
    git add .lfsconfig
    git commit -m "Add .lfsconfig"

Track files with Git LFS

Once Git points at the proxy, use Git LFS as usual. Route a file type through LFS for future commits:

git lfs track '*.iso'
    git add .gitattributes
    git commit -m "Add .iso files to Git LFS"

Move files already in history into LFS with git lfs migrate (this rewrites history):

# move all existing .iso files
    git lfs migrate import --everything --include='*.iso'

    # or move every file above 25 MiB
    git lfs migrate import --everything --above=25MiB

    git push --all --force-with-lease

Migrating existing objects

Only relevant if you already store objects on another LFS server. Fetch every object before changing the LFS URL, then push them to the proxy.

Show migration steps

Before changing the URL, fetch a local copy of everything:

git lfs fetch --all

After pointing Git at the proxy, push every fetched object:

git lfs push --all origin

Development

The worker logic lives in src/; _worker.js is the entry point Cloudflare bundles. Tests run offline because S3 signing is local crypto — no network.

npm install            # runtime + dev dependencies
    npm test               # offline test suite (node --test)
    npm run check:bundle   # verify the Cloudflare Pages bundle
    npm run dev            # serve locally with Wrangler
Serving files, not just storing them
To serve LFS objects directly from a Cloudflare Pages site — bypassing the 25 MiB file limit — pair git-lfs3 with its sibling, git-lfserve.