Git LFS のストレージをセルフホストしたかったので、rudolfs(S3バックエンドのLFSサーバー)と Nginx Proxy Manager(以下NPM)を組み合わせて構築しました。
セルフホストに踏み切った理由のひとつがストレージコストです。GitHub LFS はストレージとダウンロード帯域幅それぞれに課金されますが、S3 に自前でホストするとストレージ単価を大きく抑えられます。
| GitHub LFS | S3 Standard | S3 Intelligent-Tiering (低頻度アクセス層) |
S3 Intelligent-Tiering (Deep Archive層) |
|
|---|---|---|---|---|
| ストレージ | $0.07/GB/月 | $0.025/GB/月 (約1/3) |
$0.0138/GB/月 (約1/5) |
$0.00099/GB/月 (約1/70) |
| ダウンロード帯域幅 | $0.0875/GB | $0.09/GB | $0.09/GB | $0.09/GB |
アクセス頻度の低いファイルが多い用途であれば、Intelligent-Tiering を有効にしてアーカイブ階層まで移行させることでストレージコストをさらに大幅に抑えられます。
今回の構成のポイントは以下のとおりです。
- リポジトリの追加がUIだけで完結 — NginxやrudolfsのコンテナをいじらずNPMのUI操作だけで増やせる
- S3バックエンドで容量を気にしなくてよい — ローカルストレージと違い、ディスク管理が不要
- リポジトリ間のアクセス分離 — サブドメイン+Basic認証+パス制限の組み合わせで、認証済みユーザーが他リポジトリに触れない
構成概要
NPMはNginxのリバースプロキシ設定をWeb UIだけで管理できるツールです。Basic認証やSSL証明書の設定もUIから操作できるため、Nginxの設定ファイルを直接編集する手間がなく、リポジトリの追加もUIの操作だけで完結します。
S3バケットは全リポジトリで共有し、rudolfsがリポジトリごとのプレフィックスでオブジェクトを分けて保存します。S3バケット自体にアクセス制御は設けず、NPMがアクセス制御を担います。
リポジトリごとにサブドメインを割り当て、NPMのBasic認証とパス制限を組み合わせることで、認証を通過した利用者が別リポジトリのパスにアクセスできないようにしています。
前提 / 必要なもの
- Docker / Docker Compose
- S3バケット(AWSなど)
- ドメイン(Cloudflare Tunnelなどで外部公開する場合)
手順
Step 1: Docker Composeの用意
以下の compose.yaml を用意します。
name: git-lfs
x-common-logging: &x-common-logging
driver: json-file
options:
max-size: 10m
max-file: 3
services:
npm:
image: jc21/nginx-proxy-manager:latest
restart: unless-stopped
volumes:
- npm-data:/data
- npm-letsencrypt:/etc/letsencrypt
networks:
default:
cloudflared:
aliases:
- lfs-npm
logging: *x-common-logging
lfs:
image: jasonwhite0/rudolfs:latest
restart: unless-stopped
command: s3 --bucket=${S3_BUCKET}
environment:
RUDOLFS_KEY: "${RUDOLFS_KEY}"
AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}"
AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}"
AWS_DEFAULT_REGION: "${AWS_REGION}"
logging: *x-common-logging
volumes:
npm-data:
npm-letsencrypt:
networks:
cloudflared:
external: true
cloudflared ネットワークはCloudflare TunnelのコンテナがいるDockerネットワークです。Cloudflare Tunnelを使わない場合はネットワーク設定を適宜変更してください。
.env ファイルも作成します。
RUDOLFS_KEY= # openssl rand -hex 32 で生成
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
S3_BUCKET=
RUDOLFS_KEY は openssl rand -hex 32 で生成した値を設定します。S3オブジェクトの暗号化キーです。
docker compose up -d
Step 2: S3バケット・IAMの設定
IAMポリシーを作成します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::{バケット名}",
"arn:aws:s3:::{バケット名}/*"
]
}
]
}
IAMユーザーを作成し、このポリシーをアタッチします。アクセスキーを発行して .env に設定してください。
コストを抑えたい場合、S3バケットにライフサイクルルールを設定しておくのがおすすめです。以下は設定例です。アクセス頻度に応じて日数は調整してください。
| 設定 | 値(例) |
|---|---|
| Intelligent-Tiering へ移行 | 0日(即時) |
| Archive Access 移行 | 360日 |
| Deep Archive Access 移行 | 720日 |
Step 3: NPMでAccess Listを作成
NPMの管理UI(81番ポート)にアクセスし、リポジトリ用のAccess Listを作成します。
アクセスリスト → アクセスリストを追加
- 名前: プロジェクト名
- 認証タブ: ユーザー名・パスワードを追加
- ルール:
allow/allを追加
Step 4: NPMでProxy Hostを追加
プロキシホスト → プロキシホストを追加
| 項目 | 値 |
|---|---|
| ドメイン名 | lfs-{プロジェクト名}.example.com |
| 転送ホスト名/IP | lfs |
| 転送ポート | 8080 |
| アクセスリスト | 手順3で作成したもの |
カスタムNginx設定(右上の歯車アイコン)に以下を設定します。{org} と {repo} は実際の値に置き換えてください。
client_max_body_size 0;
location ~ ^/api/{org}/{repo}/object/ {
auth_basic off;
proxy_pass http://lfs:8080;
}
location ~ ^/api/{org}/{repo}/objects/verify {
auth_basic off;
proxy_pass http://lfs:8080;
}
location ~* ^(?!/api/{org}/{repo}/) {
return 403;
}
auth_basic off が必要かrudolfsはバッチAPIのレスポンスに
"authenticated": true を含めます。これはgit LFSプロトコルの仕様で「URLが自己認証済み」を示すフラグで、クライアントはアップロード(PUT)・verifyリクエストに認証情報を付けなくなります。NPMのAccess Listは全リクエストにBasic認証を要求するため、そのままではPUT・verifyが401になってしまいます。そのためアップロード・verifyのエンドポイントは
auth_basic off で個別に無効化します。アップロードURLの末尾はSHA256ハッシュ(OID)なので、URLが推測されても任意のコンテンツをアップロードすることは不可能です。
Custom LocationsタブのAdvancedフィールドはAccess Listの
auth_basic 設定が継承されるため、auth_basic 系のディレクティブを追記するとnginxの設定生成がエラーになります(エラー表示はなし)。auth_basic のカスタム設定は必ずSettingsボタン(カスタムNginx設定)から行ってください。
Step 5: リポジトリに .lfsconfig を追加
[lfs]
url = https://{user}:{password}@lfs-{プロジェクト名}.example.com/api/{org}/{repo}
locksverify = false
locksverify = false はrudolfsがLFSロックAPIに非対応なため必要です。
この例はアクセス制御されたプライベートなリポジトリを想定しているため、Basic認証の情報を
.lfsconfig に直接記載しています。パブリックなリポジトリや認証情報をリポジトリに含めたくない場合は、.lfsconfig にはURLのみ記載し、認証情報は ~/.netrc やgit credentialで別管理にしてください。
rudolfsのLFS APIエンドポイントは
/api/{org}/{project}/ プレフィックスが必須です。/objects/batch や /{org}/objects/batch のように省略すると404になります。.lfsconfig の url には必ず2階層のパスを含めてください。
リポジトリの追加
2つ目以降のリポジトリを追加する場合は、Step 3〜5を繰り返すだけです。
- Cloudflare Tunnelを使っている場合はサブドメインのルート追加も必要です
- S3バケットは共有のまま使い回せます
- rudolfsのコンテナは追加不要です
おわりに
NPMのAccess Listとカスタムエンドポイント制限の組み合わせで、わりとシンプルにマルチリポジトリのLFSサーバーが作れました。
Amazon S3でなくとも、互換のストレージが使用できますので、MinIO や RustFS などでサーバーのディスク領域を活用することもできますね。
なお、auth_basic off が必要な部分はハマりポイントだったので、ややモヤっとしますが、同じ構成を試す方は気を付けてください。
以上!





コメント