イントロ
お家サーバーのメリットといえばHDDやSSDを好き放題増設できることですよね?!
色々なOSSを立ち上げる際にも、S3互換ストレージがあるととても便利。
ということでご紹介するのが、GarageHQ!
なぜGarageHQなのかというと、minioの件があったからです・・・
その件については別記事でも書いてる(というかGarageHQの紹介をすでにしている)ので、そちらを参照してください。
とはいえ、RustFSはBeta版だったので、他のものを探しました。
SeaweedFSやCephなども検討しましたが、セットアップが簡単(と言われていたの)でGarageHQを選択。
GarageHQの使い方
環境情報
$ docker version
Client: Docker Engine - Community
Version: 29.1.3
API version: 1.52
Go version: go1.25.5
Git commit: f52814d
Built: Fri Dec 12 14:49:51 2025
OS/Arch: linux/amd64
Context: default
Server: Docker Engine - Community
Engine:
Version: 29.1.3
API version: 1.52 (minimum version 1.44)
Go version: go1.25.5
Git commit: fbf3ed2
Built: Fri Dec 12 14:49:51 2025
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: v2.2.0
GitCommit: 1c4457e00facac03ce1d75f7b6777a7a851e5c41
runc:
Version: 1.3.4
GitCommit: v1.3.4-0-gd6d73eb8
docker-init:
Version: 0.19.0
GitCommit: de40ad0
前提条件
- Docker がインストールされていること
- Traefik が導入されていること(リバースプロキシサーバー)
- HDDやSSDがマウントされていること(fstabなどの設定は省きます)
- 複数のノードやHDDを準備すること(シングルノードでも動きますが、GarageHQの推奨は3Way以上です)
- 今回は1サーバーに2つのノードと2つのストレージを割り当てる例を紹介します
手順
-
GarageHQ用のディレクトリを準備してください。
GarageHQの推奨では、metaデータはSSD、データ本体はHDDに保存することが推奨されています。
ここでは例として、ローカルストレージがSSD、外付けHDDが/mnt/garage1、/mnt/garage2にマウントされているとします。 -
compose.ymlを作成します。domain.tldは自分のドメインに置き換えてください。https://s3.domain.tldでS3互換ストレージ、https://*.web.domain.tldで公開したS3バケット、https://admin.domain.tldで管理APIにアクセスできるようにしています。
(管理APIは用意しましたが、今のところ使うことはなかったです)compose.ymlservices: garage-node1: image: dxflrs/garage:v2.1.0 container_name: garage-node1 restart: always ports: - 3901:3901 # rpc port(node sync) expose: - 3900 - 3902 - 3903 environment: GARAGE_RPC_SECRET: ${GARAGE_RPC_SECRET} GARAGE_ADMIN_TOKEN: ${GARAGE_ADMIN_TOKEN} GARAGE_METRICS_TOKEN: ${GARAGE_METRICS_TOKEN} volumes: - ./garage-node1.toml:/etc/garage.toml - ./storage1-meta:/var/lib/garage/meta - /mnt/garage1:/var/lib/garage/data labels: traefik.enable: true traefik.docker.network: traefik-network traefik.http.services.garage-s3.loadbalancer.server.port: 3900 traefik.http.routers.garage-s3.rule: Host(`s3.domain.tld`) || HostRegexp(`^.+\.s3\.domain\.tld$`) traefik.http.routers.garage-s3.service: garage-s3 traefik.http.routers.garage-s3.entrypoints: websecure traefik.http.routers.garage-s3.tls: true traefik.http.routers.garage-s3.tls.certResolver: cloudflare traefik.http.routers.garage-s3.tls.domains[0].main: "*.s3.domain.tld" traefik.http.routers.garage-s3.tls.domains[0].sans: "s3.domain.tld" traefik.http.services.garage-web.loadbalancer.server.port: 3902 traefik.http.routers.garage-web.rule: HostRegexp(`^.+\.web\.domain\.tld$`) traefik.http.routers.garage-web.service: garage-web traefik.http.routers.garage-web.entrypoints: websecure traefik.http.routers.garage-web.tls: true traefik.http.routers.garage-web.tls.certResolver: cloudflare traefik.http.routers.garage-web.tls.domains[0].main: "*.web.domain.tld" traefik.http.routers.garage-web.tls.domains[0].sans: web.domain.tld traefik.http.services.garage-admin.loadbalancer.server.port: 3903 traefik.http.routers.garage-admin.rule: Host(`admin.domain.tld`) traefik.http.routers.garage-admin.service: garage-admin traefik.http.routers.garage-admin.entrypoints: websecure traefik.http.routers.garage-admin.tls: true traefik.http.routers.garage-admin.tls.certResolver: cloudflare traefik.http.middlewares.garage-s3-cors.headers.accessControlAllowOriginList: "*" traefik.http.middlewares.garage-s3-cors.headers.accessControlAllowMethods: "GET,POST,PUT,DELETE,OPTIONS" traefik.http.middlewares.garage-s3-cors.headers.accessControlAllowHeaders: "*" traefik.http.middlewares.garage-s3-cors.headers.accessControlAllowCredentials: "true" traefik.http.middlewares.garage-s3-cors.headers.accessControlMaxAge: "3600" traefik.http.routers.garage-s3.middlewares: garage-s3-cors@docker networks: - traefik-network garage-node2: image: dxflrs/garage:v2.1.0 container_name: garage-node2 restart: always ports: - 3911:3911 # rpc port(node sync) expose: - 3900 - 3902 - 3903 environment: GARAGE_RPC_SECRET: ${GARAGE_RPC_SECRET} GARAGE_ADMIN_TOKEN: ${GARAGE_ADMIN_TOKEN} GARAGE_METRICS_TOKEN: ${GARAGE_METRICS_TOKEN} volumes: - ./garage-node2.toml:/etc/garage.toml - ./storage2-meta:/var/lib/garage/meta - /mnt/garage2:/var/lib/garage/data labels: traefik.enable: true traefik.docker.network: traefik-network traefik.http.services.garage-s3.loadbalancer.server.port: 3900 traefik.http.routers.garage-s3.rule: Host(`s3.domain.tld`) || HostRegexp(`^.+\.s3\.domain\.tld$`) traefik.http.routers.garage-s3.service: garage-s3 traefik.http.routers.garage-s3.entrypoints: websecure traefik.http.routers.garage-s3.tls: true traefik.http.routers.garage-s3.tls.certResolver: cloudflare traefik.http.routers.garage-s3.tls.domains[0].main: "*.s3.domain.tld" traefik.http.routers.garage-s3.tls.domains[0].sans: "s3.domain.tld" traefik.http.services.garage-web.loadbalancer.server.port: 3902 traefik.http.routers.garage-web.rule: HostRegexp(`^.+\.web\.domain\.tld$`) traefik.http.routers.garage-web.service: garage-web traefik.http.routers.garage-web.entrypoints: websecure traefik.http.routers.garage-web.tls: true traefik.http.routers.garage-web.tls.certResolver: cloudflare traefik.http.routers.garage-web.tls.domains[0].main: "*.web.domain.tld" traefik.http.routers.garage-web.tls.domains[0].sans: web.domain.tld traefik.http.services.garage-admin.loadbalancer.server.port: 3903 traefik.http.routers.garage-admin.rule: Host(`admin.domain.tld`) traefik.http.routers.garage-admin.service: garage-admin traefik.http.routers.garage-admin.entrypoints: websecure traefik.http.routers.garage-admin.tls: true traefik.http.routers.garage-admin.tls.certResolver: cloudflare traefik.http.routers.garage-s3.middlewares: garage-s3-cors@docker networks: - traefik-network networks: traefik-network: external: true -
.envファイルを作成します。GARAGE_RPC_SECRET="" # openssl rand -hex 32 で生成したランダムな文字列を設定, 各ノードで同じ値にすること GARAGE_ADMIN_TOKEN="" # openssl rand -base64 32 で生成したランダムな文字列を設定 GARAGE_METRICS_TOKEN="" # openssl rand -base64 32 で生成したランダムな文字列を設定, ただしPrometheusでメトリクスを取得する際にトークンを使う場合のみ必要なだけ -
各ノードの設定ファイルを作成します。
ここでは同じサーバー内に2つのノードを立てる例を紹介しますが、別々のサーバーに立てる場合は
rpc_public_addrをそれぞれのサーバーのIPアドレスに変更してください。garage-node1.toml# メタデータとデータの保存場所 metadata_dir = "/var/lib/garage/meta" data_dir = "/var/lib/garage/data" db_engine = "sqlite" # lmdbがデフォルトだが、停電などの不正なシャットダウンで壊れやすいためsqliteに変更 # 自動スナップショット(推奨) metadata_auto_snapshot_interval = "6h" # クラスター設定 replication_factor = 2 # 2Wayレプリケーション, 3Way以上が推奨 compression_level = 2 # RPC通信設定(ノード間通信) rpc_bind_addr = "[::]:3901" rpc_public_addr = "100.xxx.xxx.xxx:3901" # 自分のサーバーのIPアドレスに変更(Public用なら公開IP, Tailscale内での通信を想定しているならTailscaleのIPアドレス) # 他のノードとの接続設定 bootstrap_peers = [] # コマンドで追加する # S3 API設定 [s3_api] api_bind_addr = "[::]:3900" s3_region = "garage" root_domain = ".s3.domain.tld" # S3 Web(使用しない場合はコメントアウト可) [s3_web] bind_addr = "[::]:3902" root_domain = ".web.domain.tld" # 管理API設定 [admin] api_bind_addr = "[::]:3903"garage-node2.toml# メタデータとデータの保存場所 metadata_dir = "/var/lib/garage/meta" data_dir = "/var/lib/garage/data" db_engine = "sqlite" # lmdbがデフォルトだが、停電などの不正なシャットダウンで壊れやすいためsqliteに変更 # 自動スナップショット(推奨) metadata_auto_snapshot_interval = "6h" # クラスター設定 replication_factor = 2 # 2Wayレプリケーション, 3Way以上が推奨 compression_level = 2 # RPC通信設定(ノード間通信) rpc_bind_addr = "[::]:3911" rpc_public_addr = "100.xxx.xxx.xxx:3911" # 自分のサーバーのIPアドレスに変更(Public用なら公開IP, Tailscale内での通信を想定しているならTailscaleのIPアドレス) # 他のノードとの接続設定 bootstrap_peers = [] # コマンドで追加する # S3 API設定 [s3_api] api_bind_addr = "[::]:3900" s3_region = "garage" root_domain = ".s3.domain.tld" # S3 Web(使用しない場合はコメントアウト可) [s3_web] bind_addr = "[::]:3902" root_domain = ".web.domain.tld" # 管理API設定 [admin] api_bind_addr = "[::]:3903" -
GarageHQの起動
下記コマンドを実行したらhttps://s3.domain.tldにアクセスできるようになります。
docker compose up -d -
ノード間の接続
この辺が少し面倒なところです。当然ながら適当にノード2つを建てたからといって勝手に連携するわけがありません。
簡潔にいうと、docker compose exec garage-node2 /garage node idでノードIDを取得します。
その後、docker compose exec garage-node1 /garage node connect <ノード2のノードID>@<ノード2のIPアドレス>:3911をすることで互いを認識するようになります。
(connectする前に、docker compose exec garage-node1 /garage statusでノード2が認識されず、設定後は認識されていることを確認すると良いでしょう)2つのノードが認識されたら、
garage-node1.tomlのbootstrap_peersにノード2の情報を、garage-node2.tomlのbootstrap_peersにノード1の情報を追加しておけば再起動しても問題ありません。(設定しない場合、リセットされるかは未調査)
その後各ノード間のストレージのゾーンやサイズの割り当てを行うことで使えるようになります。
細かい部分についてはドキュメントをご確認くださいhttps://garagehq.deuxfleurs.fr/documentation/cookbook/real-world/#controlling-the-daemon
ただ、CLIは結構ややこしいので、garage-webuiというものを使うのがおすすめです。
こちらの使い方は別のアドベントカレンダーをご確認ください。
GarageHQの感想
- 良いところ
- シンプルでセットアップが簡単(と言われている気がする)
- S3互換ストレージとして使える
- 複数ノードでのレプリケーションが可能
- ハードウェア要件が低い
- 不満・使いこなせていないところ
- ドキュメントは少し慣れが必要
- GUIが公式で用意されていない(サードパーティ製のものを使う必要がある)
- 大規模運用には向いていない(あくまでホームユース向け)
- ライセンスがAGPLv3なので、商用利用には向いていない
- データ自体はGarageでは暗号化されない
以上、GarageHQの紹介でした!
私自身、minioの経験はシングルノードでの利用しかなかったので、マルチノードでのS3互換ストレージはGarageHQが初めてでした。他のOSSとの比較ができてない部分は申し訳ないです。
traefikを使うことで、サブドメイン形式のバケットへのアクセスも、パス形式のアクセスもできるため、traefikには感謝しかないです!