Bonjour bonjour ! Bon, je dégouline actuellement, comme tout le monde en France, j’ai envie de dire, mais ça n’empêche pas de continuer à vous informer du mieux que je peux. Aujourd’hui, il s’agit de vous présenter la configuration initiale d’un cluster Garage S3, un outil dédié à la construction et à l’exploitation de clusters de stockage objet de type S3, comme RustFS ou MinIO.

Garage S3 se positionne comme une nouvelle alternative open source très légère (un seul binaire !), pensée pour les infrastructures modernes et les environnements conteneurisés. Ce projet se distingue par sa philosophie minimaliste : offrir une compatibilité totale avec l’API S3 d’AWS, avec deux objectifs en tête : la sécurité et la simplicité de déploiement.

Contrairement à certaines autres solutions du même type, Garage est entièrement open source, ce qui en fait un choix idéal pour les organisations soucieuses de maîtriser leurs données ou de réduire leur empreinte cloud. Ses fonctionnalités clés incluent la réplication multi-sites, le chiffrement de bout en bout, une gestion fine des permissions via IAM, et une scalabilité horizontale optimisée pour les workloads distribués. Garage est natif des environnements conteneurisés, avec une architecture pensée pour Docker et Kubernetes.

Son déploiement se fait en quelques commandes (ce que nous allons faire ensemble justement). Son API S3 native permet une migration transparente depuis des solutions comme MinIO ou même AWS S3. Que ce soit pour des sauvegardes, du stockage objet ou comme backend pour des applications cloud-native, Garage S3 incarne une approche vraiment tournée vers les ingénieurs infrastructures qui veulent rester maîtres des briques qu’ils mettent en production.

Bref, trêve de blabla, trêve de blablaaaaaa !

La première chose à faire est de déployer les conteneurs avec suffisamment d’espace pour chacun. Pour se faire on va utiliser ce manisfest docker-compose.yml :

services:
  garage:
    image: dxflrs/garage:v2.3.0
    container_name: garage
    restart: unless-stopped
    ports:
      - "3900:3900"
      - "3901:3901"
      - "3902:3902"
      - "3903:3903"
    command: ["/garage", "server"]
    volumes:
      - ./garage.toml:/etc/garage.toml:ro
      - ./meta:/var/lib/garage/meta
      - ./garagedata:/var/lib/garage/data

… couplé avec un fichier de configuration garage.toml relativement simple :

metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "lmdb"
metadata_auto_snapshot_interval = "6h"

replication_factor = 3

compression_level = 2

rpc_bind_addr = "[::]:3901"
rpc_public_addr = "192.168.200.102:3901"
rpc_secret = "b6511022195d5ea063f6a1efc454885ce2850d1935c5b0ae07fb686df107ae8e"

[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
root_domain = ".s3.garage"

[s3_web]
bind_addr = "[::]:3902"
root_domain = ".web.garage"
index = "index.html"

Ce fichier définit les chemins locaux de stockage des métadonnées metadata_dir et des données brutes data_dir. Le paramètre replication_factor = 3 impose une réplication des données sur 3 nœuds, pour constituer un cluster S3. Les adresses RPC (rpc_bind_addr et rpc_public_addr) et le secret assurent une communication sécurisée entre les nœuds du cluster.

J’ai créé trois VMs pour l’occasion, camelot, avalon et orcanie, qui vont chacune héberger un des noeuds S3, en full Docker, donc.

une fois chacun des noeuds lancer avec un docker compose up -d, voici ce que ça donne :

cidou@avalon:~/garage$ docker compose up
[+] up 2/2
 ✔ Network garage_default Created                                                                                                                                               0.0s
 ✔ Container garage       Created                                                                                                                                               0.1s
Attaching to garage
garage  | 2026-06-25T05:33:53.733301Z  INFO garage::server: Loading configuration from /etc/garage.toml...
garage  | 2026-06-25T05:33:53.734198Z  INFO garage::server: Initializing Garage main data store...
garage  | 2026-06-25T05:33:53.734256Z  INFO garage_model::garage: Opening database...
garage  | 2026-06-25T05:33:53.734493Z  INFO garage_db::lmdb_adapter: Opening LMDB database at: /var/lib/garage/meta/db.lmdb
garage  | 2026-06-25T05:33:53.734853Z  INFO garage_model::garage: Initializing RPC...
garage  | 2026-06-25T05:33:53.735005Z  INFO garage_model::garage: Initialize background variable system...
garage  | 2026-06-25T05:33:53.735175Z  INFO garage_model::garage: Initialize membership management system...
garage  | 2026-06-25T05:33:53.735757Z  INFO garage_rpc::system: Generating new node key pair.
garage  | 2026-06-25T05:33:53.736813Z  INFO garage_rpc::system: Node ID of this node: 5f7d4eb5c1ee7b0b
garage  | 2026-06-25T05:33:53.736919Z  INFO garage_rpc::layout::manager: No valid previous cluster layout stored (IO error: No such file or directory (os error 2)), starting fresh.
garage  | 2026-06-25T05:33:53.737035Z  INFO garage_rpc::layout::helper: ack_until updated to 0
garage  | 2026-06-25T05:33:53.737216Z  INFO garage_model::garage: Initialize block manager...
garage  | 2026-06-25T05:33:53.737701Z  INFO garage_model::garage: Initialize admin_token_table...
garage  | 2026-06-25T05:33:53.737901Z  INFO garage_model::garage: Initialize bucket_table...
garage  | 2026-06-25T05:33:53.738011Z  INFO garage_model::garage: Initialize bucket_alias_table...
garage  | 2026-06-25T05:33:53.738098Z  INFO garage_model::garage: Initialize key_table_table...
garage  | 2026-06-25T05:33:53.738193Z  INFO garage_model::garage: Initialize block_ref_table...
garage  | 2026-06-25T05:33:53.738267Z  INFO garage_model::garage: Initialize version_table...
garage  | 2026-06-25T05:33:53.738352Z  INFO garage_model::garage: Initialize multipart upload counter table...
garage  | 2026-06-25T05:33:53.738433Z  INFO garage_model::garage: Initialize multipart upload table...
garage  | 2026-06-25T05:33:53.738527Z  INFO garage_model::garage: Initialize object counter table...
garage  | 2026-06-25T05:33:53.738636Z  INFO garage_model::garage: Initialize object_table...
garage  | 2026-06-25T05:33:53.738791Z  INFO garage_model::garage: Load lifecycle worker state...
garage  | 2026-06-25T05:33:53.738823Z  INFO garage_model::garage: Initialize K2V counter table...
garage  | 2026-06-25T05:33:53.738926Z  INFO garage_model::garage: Initialize K2V subscription manager...
garage  | 2026-06-25T05:33:53.738944Z  INFO garage_model::garage: Initialize K2V item table...
garage  | 2026-06-25T05:33:53.739117Z  INFO garage_model::garage: Initialize K2V RPC handler...
garage  | 2026-06-25T05:33:53.739238Z  INFO garage::server: Initializing background runner...
garage  | 2026-06-25T05:33:53.739431Z  INFO garage::server: Spawning Garage workers...
garage  | 2026-06-25T05:33:53.740107Z  INFO garage_model::s3::lifecycle_worker: Starting lifecycle worker for 2026-06-25
garage  | 2026-06-25T05:33:53.745520Z  INFO garage_model::s3::lifecycle_worker: Lifecycle worker finished for 2026-06-25, objects expired: 0, mpu aborted: 0
garage  | 2026-06-25T05:33:53.747759Z  INFO garage::server: Initialize Admin API server and metrics collector...
garage  | 2026-06-25T05:33:53.747842Z  INFO garage::server: Launching internal Garage cluster communications...
garage  | 2026-06-25T05:33:53.747868Z  INFO garage::server: Initializing S3 API server...
garage  | 2026-06-25T05:33:53.747901Z  INFO garage::server: Initializing web server...
garage  | 2026-06-25T05:33:53.747972Z  INFO garage_web::web_server: Web server listening on http://[::]:3902
garage  | 2026-06-25T05:33:53.748046Z  INFO garage_net::netapp: Listening on [::]:3901
garage  | 2026-06-25T05:33:53.747968Z  INFO garage_api_common::generic_server: S3 API server listening on http://[::]:3900

Vous noterez que le noeud semble déjà opérationnel : listening on http://[::]:3900

On va l’interroger en exécutant un docker exec <le conteneur> <la ligne de commande>, tout simplement. la seule interface de commande des noeud est l’usage du fameux binaire à la racine du conteneur /garage :

cidou@avalon:~/garage$ docker exec garage /garage status
2026-06-25T05:38:04.559300Z  INFO garage_net::netapp: Connected to 192.168.200.102:3901, negotiating handshake...
2026-06-25T05:38:04.603276Z  INFO garage_net::netapp: Connection established to 5f7d4eb5c1ee7b0b
==== HEALTHY NODES ====
ID                Hostname      Address               Tags  Zone  Capacity          DataAvail  Version
5f7d4eb5c1ee7b0b  2ba65e113559  192.168.200.102:3901              NO ROLE ASSIGNED             v2.3.0
cidou@avalon:~/garage$

… et c’est le cas pour les 3 noeuds que je viens de créer, pour le moment. Notre cluster est prêt à être configuré ! Le port 3900 est le port d’écoute de l’interface S3, les port 3901 est celui de l’interface de l’API garage, utilisée pour le pilotage de garage. On va récupérer le node id de chaque noeud. Pour avalon :

cidou@avalon:~/garage$ docker exec garage /garage node id

To instruct a node to connect to this node, run the following command on that node:
    garage [-c <config file path>] node connect 5f7d4eb5c1ee7b0bbe15970da04c73d8288afcfb28982bce14067e176e573504@192.168.200.102:3901

This node identifier can also be added as a bootstrap node in other node's garage.toml files:
    bootstrap_peers = [
        "5f7d4eb5c1ee7b0bbe15970da04c73d8288afcfb28982bce14067e176e573504@192.168.200.102:3901",
        ...
    ]

Security notice: Garage's intra-cluster communications are secured primarily by the shared
secret value rpc_secret.  However, an attacker that knows rpc_secret (for example if it
leaks) cannot connect if they do not know any of the identifiers of the nodes in the
cluster. It is thus a good security measure to try to keep them secret if possible.

5f7d4eb5c1ee7b0bbe15970da04c73d8288afcfb28982bce14067e176e573504@192.168.200.102:3901
cidou@avalon:~/garage$

… et même chose pour les deux autre noeuds, orcanie et camelot. Au final on a donc trois noeuds autonome (pour le moment), on va donc connecter orcanie et camelot a avalon :

cidou@orcanie:~/garage$ docker exec garage /garage status
2026-06-25T05:52:20.983842Z  INFO garage_net::netapp: Connected to 192.168.200.117:3901, negotiating handshake...
2026-06-25T05:52:21.028307Z  INFO garage_net::netapp: Connection established to 8789375f3ad9cbac
==== HEALTHY NODES ====
ID                Hostname      Address               Tags  Zone  Capacity          DataAvail  Version
8789375f3ad9cbac  5eacf91f4e9d  192.168.200.117:3901              NO ROLE ASSIGNED             v2.3.0
cidou@orcanie:~/garage$ docker exec garage /garage node connect 5f7d4eb5c1ee7b0bbe15970da04c73d8288afcfb28982bce14067e176e573504@192.168.200.102:3901
2026-06-25T05:53:47.725365Z  INFO garage_net::netapp: Connected to 192.168.200.117:3901, negotiating handshake...
2026-06-25T05:53:47.768323Z  INFO garage_net::netapp: Connection established to 8789375f3ad9cbac
Success.
cidou@orcanie:~/garage$

Au final, après avoir connecté les deux autre nodes au premier :

cidou@avalon:~/garage$ docker exec garage /garage status
2026-06-25T05:55:23.695686Z  INFO garage_net::netapp: Connected to 192.168.200.102:3901, negotiating handshake...
2026-06-25T05:55:23.739255Z  INFO garage_net::netapp: Connection established to 5f7d4eb5c1ee7b0b
==== HEALTHY NODES ====
ID                Hostname      Address               Tags  Zone  Capacity          DataAvail  Version
5f7d4eb5c1ee7b0b  2ba65e113559  192.168.200.102:3901              NO ROLE ASSIGNED             v2.3.0
6ea329a6501483db  6705927b21be  192.168.200.129:3901              NO ROLE ASSIGNED             v2.3.0
8789375f3ad9cbac  5eacf91f4e9d  192.168.200.117:3901              NO ROLE ASSIGNED             v2.3.0
cidou@avalon:~/garage$

On va maintenant déclarer l’organisation de notre cluster, basé sur les 3 instances que l’on vient de créer et connecter ensemble. Je vais partir sur ce principe d’organisation :

cidou@avalon:~/garage$ docker exec garage /garage layout assign 5f7d4eb5c1ee7b0b -c 50G -z zone1 -t avalon
2026-06-25T06:02:32.502518Z  INFO garage_net::netapp: Connected to 192.168.200.102:3901, negotiating handshake...
2026-06-25T06:02:32.551303Z  INFO garage_net::netapp: Connection established to 5f7d4eb5c1ee7b0b
Role changes are staged but not yet committed.
Use garage layout show to view staged role changes,
and garage layout apply to enact staged changes.
cidou@avalon:~/garage$ docker exec garage /garage layout assign 6ea329a6501483db -c 50G -z zone2 -t camelot
2026-06-25T06:05:33.684081Z  INFO garage_net::netapp: Connected to 192.168.200.102:3901, negotiating handshake...
2026-06-25T06:05:33.727225Z  INFO garage_net::netapp: Connection established to 5f7d4eb5c1ee7b0b
Role changes are staged but not yet committed.
Use garage layout show to view staged role changes,
and garage layout apply to enact staged changes.
cidou@avalon:~/garage$ docker exec garage /garage layout assign 8789375f3ad9cbac -c 50G -z zone3 -t orcanie
2026-06-25T06:06:04.626000Z  INFO garage_net::netapp: Connected to 192.168.200.102:3901, negotiating handshake...
2026-06-25T06:06:04.667307Z  INFO garage_net::netapp: Connection established to 5f7d4eb5c1ee7b0b
Role changes are staged but not yet committed.
Use garage layout show to view staged role changes,
and garage layout apply to enact staged changes.

Désormais, nous avons préparé une topologie que vous pouvez vérifier :

cidou@avalon:~/garage$ docker exec garage /garage layout show
2026-06-25T06:18:43.888094Z  INFO garage_net::netapp: Connected to 192.168.200.102:3901, negotiating handshake...
2026-06-25T06:18:43.931182Z  INFO garage_net::netapp: Connection established to 5f7d4eb5c1ee7b0b
==== CURRENT CLUSTER LAYOUT ====
No nodes currently have a role in the cluster.
See garage status to view available nodes.

Current cluster layout version: 0

==== STAGED ROLE CHANGES ====
ID                Tags       Zone   Capacity
5f7d4eb5c1ee7b0b  [avalon]   zone1  46.6 GiB
6ea329a6501483db  [camelot]  zone2  46.6 GiB
8789375f3ad9cbac  [orcanie]  zone3  46.6 GiB


==== NEW CLUSTER LAYOUT AFTER APPLYING CHANGES ====
ID                Tags       Zone   Capacity  Usable capacity
5f7d4eb5c1ee7b0b  [avalon]   zone1  46.6 GiB  46.6 GiB (100.0%)
6ea329a6501483db  [camelot]  zone2  46.6 GiB  46.6 GiB (100.0%)
8789375f3ad9cbac  [orcanie]  zone3  46.6 GiB  46.6 GiB (100.0%)

Zone redundancy: maximum

==== COMPUTATION OF A NEW PARTITION ASSIGNATION ====

Partitions are replicated 3 times on at least 3 distinct zones.

Optimal partition size:                     186.3 MiB
Usable capacity / total cluster capacity:   139.7 GiB / 139.7 GiB (100.0 %)
Effective capacity (replication factor 3):  46.6 GiB

zone1               Tags       Partitions        Capacity  Usable capacity
  5f7d4eb5c1ee7b0b  [avalon]   256 (256 new)     46.6 GiB  46.6 GiB (100.0%)
  TOTAL                        256 (256 unique)  46.6 GiB  46.6 GiB (100.0%)

zone2               Tags       Partitions        Capacity  Usable capacity
  6ea329a6501483db  [camelot]  256 (256 new)     46.6 GiB  46.6 GiB (100.0%)
  TOTAL                        256 (256 unique)  46.6 GiB  46.6 GiB (100.0%)

zone3               Tags       Partitions        Capacity  Usable capacity
  8789375f3ad9cbac  [orcanie]  256 (256 new)     46.6 GiB  46.6 GiB (100.0%)
  TOTAL                        256 (256 unique)  46.6 GiB  46.6 GiB (100.0%)


To enact the staged role changes, type:

    garage layout apply --version 1

You can also revert all proposed changes with: garage layout revert
cidou@avalon:~/garage$

Allez, tout me parait correct, on va commit tout cela via un layout apply :

cidou@avalon:~/garage$ docker exec garage /garage layout apply --version 1
2026-06-25T06:21:57.823909Z  INFO garage_net::netapp: Connected to 192.168.200.102:3901, negotiating handshake...
2026-06-25T06:21:57.867251Z  INFO garage_net::netapp: Connection established to 5f7d4eb5c1ee7b0b
==== COMPUTATION OF A NEW PARTITION ASSIGNATION ====

Partitions are replicated 3 times on at least 3 distinct zones.

Optimal partition size:                     186.3 MiB
Usable capacity / total cluster capacity:   139.7 GiB / 139.7 GiB (100.0 %)
Effective capacity (replication factor 3):  46.6 GiB

zone1               Tags       Partitions        Capacity  Usable capacity
  5f7d4eb5c1ee7b0b  [avalon]   256 (256 new)     46.6 GiB  46.6 GiB (100.0%)
  TOTAL                        256 (256 unique)  46.6 GiB  46.6 GiB (100.0%)

zone2               Tags       Partitions        Capacity  Usable capacity
  6ea329a6501483db  [camelot]  256 (256 new)     46.6 GiB  46.6 GiB (100.0%)
  TOTAL                        256 (256 unique)  46.6 GiB  46.6 GiB (100.0%)

zone3               Tags       Partitions        Capacity  Usable capacity
  8789375f3ad9cbac  [orcanie]  256 (256 new)     46.6 GiB  46.6 GiB (100.0%)
  TOTAL                        256 (256 unique)  46.6 GiB  46.6 GiB (100.0%)


New cluster layout with updated role assignment has been applied in cluster.
Data will now be moved around between nodes accordingly.
cidou@avalon:~/garage$

Notre cluster est prêt ! On peut maintenant configurer notre premier bucket pour le consommer :

cidou@avalon:~/garage$ docker exec garage /garage bucket create bucket1
2026-06-25T06:38:19.053031Z  INFO garage_net::netapp: Connected to 192.168.200.102:3901, negotiating handshake...
2026-06-25T06:38:19.095361Z  INFO garage_net::netapp: Connection established to 5f7d4eb5c1ee7b0b
==== BUCKET INFORMATION ====
Bucket:          7bebd57ac20f7392dd788c1bba4736f3c54ca345d96b910478ec3a6de05191df
Created:         2026-06-25 06:38:19.096 +00:00

Size:            0 B (0 B)
Objects:         0

Website access:  false

Global alias:    bucket1

==== KEYS FOR THIS BUCKET ====
Permissions  Access key    Local aliases
cidou@avalon:~/garage$ docker exec garage /garage key create key-bucket1
2026-06-25T06:39:04.411568Z  INFO garage_net::netapp: Connected to 192.168.200.102:3901, negotiating handshake...
2026-06-25T06:39:04.459278Z  INFO garage_net::netapp: Connection established to 5f7d4eb5c1ee7b0b
==== ACCESS KEY INFORMATION ====
Key ID:              GKb17f39dc74d189dfe0253f14
Key name:            key-bucket1
Secret key:          232f78a4e1fab22b8cb78afd7f1f7d1273ca98b2726f7b59db348239b464df1b
Created:             2026-06-25 06:39:04.459 +00:00
Validity:            valid
Expiration:          never

Can create buckets:  false

==== BUCKETS FOR THIS KEY ====
Permissions  ID  Global aliases  Local aliases
cidou@avalon:~/garage$ docker exec garage /garage bucket allow --read --write --owner bucket1 --key key-bucket1
2026-06-25T06:41:41.327799Z  INFO garage_net::netapp: Connected to 192.168.200.102:3901, negotiating handshake...
2026-06-25T06:41:41.375234Z  INFO garage_net::netapp: Connection established to 5f7d4eb5c1ee7b0b
==== BUCKET INFORMATION ====
Bucket:          7bebd57ac20f7392dd788c1bba4736f3c54ca345d96b910478ec3a6de05191df
Created:         2026-06-25 06:38:19.096 +00:00

Size:            0 B (0 B)
Objects:         0

Website access:  false

Global alias:    bucket1

==== KEYS FOR THIS BUCKET ====
Permissions  Access key                               Local aliases
RWO          GKb17f39dc74d189dfe0253f14  key-bucket1

On créé le bucket, ensuite une nouvelle clef l’accès, et enfin on donne le droit à cette clef d’accéder au bucket.

Attention comme quasiment toujours dans les solutions S3, lorsque la clef est créé, le système ne vous affiche qu’une seule fois la secret key 🙂

Le cluster est désormais configuré et accessible depuis n’importe quelle machine (avalon, camelot ou orcanie dans notre exemple). Charge à vous de rajouter un proxy/load balancer, comme, au hasard mon chouchou Traefik pour distribuer les accès sur un ensemble de clients. J’adore l’approche certes technique mais surtout parfaitement fonctionnelle de Garage S3. C’est un produit qui ne s’embarrasse pas belles interfaces web ni d’outil visuel sujet mécaniquement à des bugs ou des vulnérabilités potentielles. Il se concentre sur l’essentiel : fournir un accès robuste et potentiellement distribué sur de multiples points d’accès et noeuds à un stockage objet.

Je vous laisse découvrir la suite car c’est un système très riche !

Pour référence :
– Garage S3 de DeuxFleurs (joli nom ^^) : https://garagehq.deuxfleurs.fr

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *