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 :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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 :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
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 :
|
1 2 3 4 5 6 7 |
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 :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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 :
|
1 2 3 4 5 6 7 8 9 10 11 |
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 :
|
1 2 3 4 5 6 7 8 9 |
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 :

|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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 :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
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 :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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 :
On créé le bucket, ensuite une nouvelle clef d’acces, et enfin on donne le droit à cette clef d’accéder au bucket.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
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 |
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
