Git LFS server / Rust / MPL-2.0 / FerrLabs

Vos assets. Votre disque.

LFSX stocke les gros binaires d'un dépôt Git, assets de jeu, textures, modèles et vidéo, pour qu'ils ne touchent jamais au quota LFS de votre hébergeur.

Un pack d'assets de 3 Go cloné dix fois par mois par un job CI, cela fait 30 Go de trafic facturé.

GitHub facture le stockage et la bande passante LFS à part de votre offre, et un projet Unity ou Unreal épuise le palier gratuit en un seul push. L'auto-hébergement supprime le compteur : le coût devient un disque que vous possédez déjà.

Ce qui change vraiment

GitHub / GitLab LFS

Stockage et bande passante comptés, facturés par packs

Les objets vivent chez le même hébergeur que le dépôt

Le contrôle d'accès appartient à la forge, la facture aussi

Un job CI qui retire le même pack le paie chaque fois

LFSX

Un disque que vous possédez déjà, sans compteur dessus

Le dépôt est inchangé, seul le transfert est redirigé

Toujours les permissions de la forge, interrogées en direct, sans seconde liste d'utilisateurs

Un .lfsconfig commité, aucun plugin côté client

Démarrage rapide

Lancez-le

Un conteneur, un binaire lié statiquement depuis les releases, ou cargo install lfsx-server si vous préférez compiler.

docker run -d --name lfsx \
  -p 8080:8080 \
  -v lfsx-data:/var/lib/lfsx \
  -e LFSX_PUBLIC_URL=https://lfs.example.com \
  ghcr.io/ferrlabs/lfsx:latest

Pointez un dépôt vers lui

Les deux derniers segments du chemin sont l'organisation et le projet, et ensemble ils délimitent le stockage. Deux dépôts qui partagent une URL partagent leurs objets.

# .lfsconfig
[lfs]
	url = https://lfs.example.com/my-org/my-project

Puis utilisez Git LFS comme d'habitude

git lfs install
git lfs track "*.psd"
git add .gitattributes assets/hero.psd
git commit -m "add hero artwork"
git push

Exécutez git lfs install avant de cloner. Sans cela, les fichiers arrivent sous forme de pointeurs de 130 octets, et les outils qui les lisent, Unity, Unreal et les éditeurs d'images, échouent de façon déroutante.

Vérifiez que ça marche

Le doctor vérifie que le serveur répond, que son stockage est accessible en écriture, que votre jeton est accepté, et que l'URL annoncée pour les transferts est celle par laquelle vous l'avez joint, le décalage qui fait réussir la négociation et échouer tous les transferts.

npm install -g @ferrlabs/lfsx        # or: cargo install lfsx
lfsx --url https://lfs.example.com doctor --repo my-org/my-project

Aucun compte à gérer.

Le client présente le jeton qu'il utiliserait pour cloner en HTTPS, et LFSX demande à la forge ce que ce jeton est autorisé à faire. Chaque réponse est mise en cache : un push de deux cents objets coûte un appel d'API, pas deux cents.

GitHubGitLabGitea, ForgejoObjets
admin Maintainer, Owner admin téléchargement, envoi, et forcer l'ouverture d'un verrou
push Developer push téléchargement, envoi, et prise de verrous
pull only Reporter pull only téléchargement
none Guest, or none none rejeté

En CI, le jeton que le workflow détient déjà suffit, aucun secret à provisionner.

- run: |
    printf 'protocol=https\nhost=lfs.example.com\nusername=git\npassword=%s\n' "${{ secrets.GITHUB_TOKEN }}" \
      | git credential approve
    git lfs pull

Configuration

Toute la configuration passe par des variables d'environnement. Aucun fichier de config à générer, aucune base de données à migrer.

LFSX_BIND
adresse d'écoute, 0.0.0.0:8080
LFSX_STORAGE_ROOT
racine du stockage d'objets, /var/lib/lfsx
LFSX_PUBLIC_URL
URL publique servant à construire les liens de transfert
LFSX_AUTH
source des permissions, github ou gitlab ou gitea, ou disabled
LFSX_STORAGE
s3 pour garder les objets dans un bucket plutôt que sur le volume
LFSX_COMPRESSION
zstd:1 à zstd:19, pour compresser les objets au repos
LFSX_ENCRYPTION_KEY_FILE
des clés de 32 octets en hexadécimal, pour chiffrer les objets au repos
LFSX_REPO_QUOTA
octets qu'un seul dépôt peut détenir, illimité

LFSX_PUBLIC_URL est renvoyée dans la réponse batch et le client s'y reconnecte pour chaque objet. Si elle est fausse, la négociation réussit et chaque transfert échoue ensuite.

Toutes les variables

Auto-hébergement

Docker

docker run -d -p 8080:8080 -v lfsx-data:/var/lib/lfsx ghcr.io/ferrlabs/lfsx:latest

Kubernetes

helm install lfsx oci://ghcr.io/ferrlabs/charts/lfsx --set ingress.host=lfs.example.com

Le chart encode ce qui se rate facilement : l'URL publique dérivée de l'hôte d'ingress, les annotations nginx qui évitent le rejet des gros envois, les probes sur /health et /ready, et le refus de rendre plus d'un replica tant que les objets ne sont pas dans un bucket.

Binary

curl -fsSL .../lfsx-server-x86_64-unknown-linux-musl.tar.gz | tar xz && ./lfsx-server

x86_64 ou aarch64, musl ou gnu. Chaque archive est accompagnée de son .sha256.

Cargo

cargo install lfsx-server

Stockage

Les objets vivent par défaut sur le système de fichiers, adressés par empreinte. Un bucket, la compression et le chiffrement sont chacun une variable d'environnement, et ils se combinent.

Objets dans un bucket

LFSX_STORAGE=s3

Un bucket compatible S3, MinIO, Garage, Backblaze ou AWS, au lieu du volume, ce qui détache la capacité d'une seule machine.

Les verrous suivent les objets, posés par écriture conditionnelle pour que le stockage lui-même décide qui est arrivé premier, et c'est ce qui rend un second replica possible. Le serveur vérifie au démarrage que le stockage l'applique vraiment. Les transferts passent par le serveur par défaut ; LFSX_S3_PRESIGN=true les redirige vers le bucket.

Compression

LFSX_COMPRESSION=zstd

On croit qu'un store LFS est déjà compressé, et c'est vrai pour le PNG, le MP3 et l'OGG. C'est très faux pour les maillages.

Les objets sont compressés en trames de quatre mégaoctets avec un index : les ranges fonctionnent toujours et la mémoire reste plate. Ce qui ne se compresse pas est stocké tel qu'arrivé.

.tga

10.4×

.fbx

2.9-6.7×

.png

1%

Mesuré sur deux vrais projets Unity : 71% de moins au total.

Chiffrement au repos

ChaCha20-Poly1305

Pour la plupart des déploiements auto-hébergés, la meilleure réponse reste le volume : LUKS, un volume EBS chiffré, une storage class qui le fait de façon transparente. Choisissez ceci quand c'est le stockage lui-même que vous ne voulez pas croire : un NAS partagé, un bucket opéré par quelqu'un d'autre, un disque que vous renverrez un jour sous garantie.

La clé est un chemin de fichier, jamais la clé elle-même : une clé dans une variable d'environnement se retrouve dans le pod spec, dans docker inspect, et dans chaque log qui vide l'environnement. La rotation est une nouvelle ligne en haut du fichier. La compression passe en premier quand les deux sont actifs.

Cela ne protège pas de qui détient le serveur en marche, car ce processus porte la clé par construction.

LFSX_STORAGE=s3
LFSX_S3_ENDPOINT=https://s3.example.com
LFSX_S3_BUCKET=assets
LFSX_S3_ACCESS_KEY=…
LFSX_S3_SECRET_KEY=…
LFSX_COMPRESSION=zstd        # level 3
LFSX_COMPRESSION=zstd:9      # slower, smaller

head -c 32 /dev/urandom | xxd -p -c 64 > /etc/lfsx/key
LFSX_ENCRYPTION_KEY_FILE=/etc/lfsx/key
Comment fonctionne un déploiement en bucket

FAQ

Mes clients ont-ils besoin d'un plugin ?

Non. Git LFS 3.0.2 et suivants sont testés sur Linux, macOS et Windows, y compris les copies embarquées par Git for Windows, GitHub Desktop, Sourcetree, Rider et Unity.

L'authentification peut-elle vivre dans le proxy ?

Non, et cela écarte l'approche que la plupart tentent d'abord. Derrière un proxy authentifiant, git-lfs appelle les URLs de transfert sans aucune identification et boucle. LFSX ne déclare jamais qu'un transfert passant par lui est pré-authentifié : le client authentifie chacun d'eux.

Quelles forges sont supportées ?

GitHub, GitLab, et Gitea avec Forgejo, y compris les instances Enterprise et auto-gérées via leur URL d'API. Sur GitLab, les droits hérités d'un groupe comptent comme ceux posés sur le projet.

Gère-t-il les verrous de fichiers ?

Oui, pour les assets qui ne peuvent pas être fusionnés, la réouverture forcée étant réservée aux niveaux qu'une forge traite comme administratifs : admin sur GitHub et Gitea, Maintainer ou Owner sur GitLab. Dans un bucket, le serveur vérifie au démarrage que le stockage refuse bien une écriture conditionnelle, et renonce au verrouillage plutôt que de donner le même verrou à deux personnes.

Puis-je contribuer une forge ?

Trois fournisseurs existent, donc la forme est établie plutôt que devinée : un module sous server/src/auth/ exposant trois fonctions, plus une variante de config et trois branches dans auth.rs. Le cache, la gestion du challenge et le comptage des refus sont partagés et agnostiques du fournisseur.

L'auto-hébergement supprime le compteur.

LFSXMPL-2.0FerrLabslfsx.dev