Skip to content

Commit 8873b7f

Browse files
authored
feat(local-dev): use garage for s3 (#1996)
* fix: configuration * fix: use correct container name from core and add issue about region * fix: remove mentions of localstack * fix: make requested changes in the readme about garage
1 parent 41a0834 commit 8873b7f

4 files changed

Lines changed: 141 additions & 43 deletions

File tree

.devcontainer/docker-compose.yml

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@ services:
1515
environment:
1616
# sane default to allow the server to bind to any interface in the container
1717
HOSTNAME: localhost
18-
# temporary storage for files inside the container
19-
FILE_STORAGE_ROOT_PATH: /tmp/graasp-file-item-storage
20-
# the localfile config host points to the static file server defined below
21-
FILE_STORAGE_HOST: http://localfile:1081
2218
# H5P local storage host used in local only
2319
H5P_FILE_STORAGE_HOST: http://localfile:1081
2420
# endpoint of the nudenet model
@@ -35,11 +31,12 @@ services:
3531
REDIS_CONNECTION: redis://redis:6379
3632
# the Mailer config is set by the "mailer" service below
3733
MAILER_CONNECTION: smtp://docker:docker@mailer:1025
38-
# the Localstack config is set by the "localstack" service below
39-
S3_FILE_ITEM_HOST: http://localstack:4566
34+
# the Garage config is set by the "garage" service below
35+
S3_FILE_ITEM_HOST: http://s3.garage.localhost:3900
4036
# the Iframely config is set by the "iframely" service below
4137
EMBEDDED_LINK_ITEM_IFRAMELY_HREF_ORIGIN: http://iframely:8061
42-
38+
links:
39+
- garage:s3.garage.localhost
4340
volumes:
4441
- ..:/workspace:cached
4542
- ../tmp:/tmp/graasp-file-item-storage
@@ -135,20 +132,18 @@ services:
135132
- 8000:3000
136133

137134
# Localstack is used to test aws services locally
138-
localstack:
139-
hostname: localstack
140-
image: localstack/localstack
135+
garage:
136+
hostname: garage
137+
image: dxflrs/garage:v2.0.0
141138
volumes:
142-
- ../tmp:/tmp/graasp-localstack
143-
- './localstack/init.sh:/etc/localstack/init/ready.d/init-aws.sh'
144-
- /var/run/docker.sock:/var/run/docker.sock
145-
environment:
146-
- SERVICES=s3
147-
- DEBUG=1
148-
- DOCKER_HOST=unix:///var/run/docker.sock
149-
- AWS_DEFAULT_REGION=us-east-1
139+
- ../tmp/garage/data:/var/lib/garage/data
140+
- ../tmp/garage/meta:/var/lib/garage/meta
141+
- ./garage/garage.toml:/etc/garage.toml
150142
ports:
151-
- 4566-4583:4566-4583
143+
- "3900:3900"
144+
- "3901:3901"
145+
- "3902:3902"
146+
- "3903:3903"
152147

153148
localfile:
154149
hostname: localfile

.devcontainer/garage/garage.toml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
metadata_dir = "/var/lib/garage/meta"
2+
data_dir = "/var/lib/garage/data"
3+
db_engine = "sqlite"
4+
5+
replication_factor = 1
6+
7+
rpc_bind_addr = "[::]:3901"
8+
rpc_public_addr = "127.0.0.1:3901"
9+
# generate with: $(openssl rand -hex 32)
10+
rpc_secret = "8481e1b792f7ea90045de86aa89c2c3590c69270c212a079638772c15af59cbf"
11+
12+
[s3_api]
13+
s3_region = "garage"
14+
api_bind_addr = "[::]:3900"
15+
root_domain = ".s3.garage.localhost"
16+
17+
[s3_web]
18+
bind_addr = "[::]:3902"
19+
root_domain = ".web.garage.localhost"
20+
index = "index.html"
21+
22+
[k2v_api]
23+
api_bind_addr = "[::]:3904"
24+
25+
[admin]
26+
api_bind_addr = "[::]:3903"
27+
# generate with: $(openssl rand -base64 32)
28+
admin_token = "gerE+wyOwMubhx73da8UnQMG2X/1Vhd76HZM9Zk/dcA="
29+
# generate with: $(openssl rand -base64 32)
30+
metrics_token = "5V9umZxH5RDVEl77GCK/t+Yq8ehkBNYgtEcs+q6b7B0="

README.md

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,14 @@ This will create 11 containers :
5656
- `etherpad` : Container for the etherpad service
5757
- `meilisearch` : Container for the meilisearch service
5858
- `redis` : Redis instance to enable websockets
59-
- `localstack` : Localstack instance use to fake S3 storage locally
60-
- `localfile` : Simple static file server to get files stored in graasp when using the `local` storage option (see the [Utilities section](#utilities))
59+
- `garage` : An s3-compatible service that works locally
60+
- `localfile` : Simple static file server alternative to s3. Used currently for H5P in local development
6161
- `iframely` : Iframely instance used to get embeds for links
6262
- `mailer` : Simple mailer instance used to receive emails locally (see the [Utilities section](#utilities))
6363
- `umami`: An analytics service used instead of Google Analytics
6464

6565
> **Important**
66-
> To use localstack with the Docker installation, it is necessary to edit your `/etc/hosts` with the following line `127.0.0.1 localstack`. This is necessary because the backend creates signed urls with the localstack container hostname. Without changing the hosts, the development machine cannot resolve the `http://localstack` hostname.
66+
> To use garage with the Docker installation, it is necessary to edit your `/etc/hosts` with the following line `127.0.0.1 .s3.garage.localhost`. This is necessary because the backend creates signed urls pointing to this subdomain. Without changing the hosts, the development machine cannot resolve urls like `http://s3.garage.localhost:3900` .
6767
6868
> **Troubleshoot**
6969
> If during setup of the devcontainer you get an error like `nudenet Error pull access denied for public.ecr.aws/g...`
@@ -94,7 +94,7 @@ First a running and accessible instance of PostgreSQL is required.
9494

9595
To enable websockets capabilities, it is required to have a running instance of [Redis](https://redis.io).
9696

97-
To use the backend with S3, it is required to have a running instance of [localstack](https://github.com/localstack/localstack).
97+
To use the backend with S3, it is required to have a running instance of [garage](https://git.deuxfleurs.fr/Deuxfleurs/garage).
9898

9999
Then open the folder locally and run the following command to install the required npm packages.
100100

@@ -150,9 +150,9 @@ EMAIL_CHANGE_JWT_SECRET=<secret-key>
150150

151151
### File storages configuration
152152

153-
# If you are using a local installation of localstack replace by http://localhost:4566
153+
# If you are using a different service than garage for s3-compatible operations update this value.
154154
# Otherwise this value is already set by ./.devcontainer/docker-compose.yml
155-
# S3_FILE_ITEM_HOST=http://graasp-localstack:4566
155+
# S3_FILE_ITEM_HOST=http://s3.garage.localhost:3900
156156

157157
# Graasp file item file storage path
158158
# File item storage is set by ./.devcontainer/docker-compose.yml
@@ -161,10 +161,10 @@ EMAIL_CHANGE_JWT_SECRET=<secret-key>
161161

162162
# Graasp s3 file item
163163
FILE_STORAGE_TYPE=s3
164-
S3_FILE_ITEM_REGION=us-east-1
165-
S3_FILE_ITEM_BUCKET=graasp
166-
S3_FILE_ITEM_ACCESS_KEY_ID=graasp-user
167-
S3_FILE_ITEM_SECRET_ACCESS_KEY=graasp-pwd
164+
S3_FILE_ITEM_REGION=garage
165+
S3_FILE_ITEM_BUCKET=file-items
166+
S3_FILE_ITEM_ACCESS_KEY_ID=<your bucket key>
167+
S3_FILE_ITEM_SECRET_ACCESS_KEY=<your bucket secret>
168168

169169
# Graasp H5P
170170
H5P_FILE_STORAGE_TYPE=local
@@ -225,6 +225,79 @@ OPENAI_API_KEY=<openai-api-key>
225225
GEOLOCATION_API_KEY=
226226
```
227227

228+
### Garage
229+
230+
You will need to configure the garage instance so you can use the s3 buckets with the proper access keys.
231+
232+
To simplify the commands you can create an alias to the docker exec command:
233+
234+
Run this on the host machine
235+
```sh
236+
# get the container name for the garage service
237+
docker ps
238+
239+
# complete the command with the container name of the garage service (something like `core_devcontainer-garage-1`)
240+
alias garage="docker exec -it <container-name> /garage"
241+
```
242+
243+
You should now be able to run commands against the garage executable running inside the container. Check that it works by running:
244+
245+
```sh
246+
garage status
247+
```
248+
249+
You should see an output similar to:
250+
251+
```
252+
2025-09-11T05:42:45.393828Z INFO garage_net::netapp: Connected to 127.0.0.1:3901, negotiating handshake...
253+
2025-09-11T05:42:45.436392Z INFO garage_net::netapp: Connection established to fca7df6b0fe8115c
254+
==== HEALTHY NODES ====
255+
ID Hostname Address Tags Zone Capacity DataAvail Version
256+
fca7df6b0fe8115c garage 127.0.0.1:3901 [] dc1 1000.0 MB 365.8 GB (36.8%) v2.0.0
257+
```
258+
259+
#### Config
260+
261+
Now for the real configuration part.
262+
263+
We will:
264+
- setup the layout for the storage (this is required by garage to know how it allocates the capacity)
265+
- create the file-items bucket (h5p bucket can be configured too, guide does not do it currently)
266+
- create an access key for the bucket
267+
- make the correct configurations to be able to access the bucket
268+
269+
Layout setup
270+
```sh
271+
# get the node id
272+
garage status
273+
274+
# -z defines the zone for the node, -c defines the capacity, in single node this has no impact
275+
garage layout assign -z dc1 -c 1G <node-id>
276+
277+
# apply the layout
278+
garage layout apply --version 1
279+
```
280+
281+
Create a bucket
282+
```sh
283+
garage bucket create file-items
284+
285+
garage bucket list
286+
287+
garage bucket info file-items
288+
```
289+
290+
Create an access key. Make not of the secret key as it will not be shown again !
291+
```sh
292+
garage key create core-s3-key
293+
294+
# allow the key to access the bucket
295+
garage bucket allow --read --write --owner file-items --key core-s3-key
296+
```
297+
298+
299+
300+
228301
### Umami
229302

230303
To log into umami in your local instance: [Umami login documentation](https://umami.is/docs/login)
@@ -241,7 +314,7 @@ You can also run `yarn seed` to feed the database with predefined mock data.
241314

242315
The development [docker-compose.yml](.devcontainer/docker-compose.yml) provides an instance of [mailcatcher](https://mailcatcher.me/), which emulates a SMTP server for sending e-mails. When using the email authentication flow, the mailbox web UI is accessible at [http://localhost:1080](http://localhost:1080).
243316

244-
The development [docker-compose.yml](.devcontainer/docker-compose.yml) provides a [static file server](https://static-web-server.net/) for serving files when using the `local` storage option (alternative to the `s3` option). This option has the added benefit of being persistent when used locally in opposition to localstack (see the [known issues section](#known-issues) for more informations). The server is available at `http://localhost:1081`.
317+
The development [docker-compose.yml](.devcontainer/docker-compose.yml) provides a [s3-compatible service](https://garagehq.deuxfleurs.fr/) for serving files. Ensure you have setup your /etc/hosts so that it works.
245318

246319
## Testing
247320

@@ -285,23 +358,23 @@ Each migration should have its own test to verify the `up` and `down` procedures
285358

286359
Up tests start from the previous migration state, insert mock data and apply the up procedure. Then each table should still contain the inserted data with necessary changes. The down tests have a similar approach.
287360

288-
## Known issues
361+
## Troubleshooting
289362

290-
### Data persistence
363+
### Nudenet Container can not be pulled
291364

292-
The development environnement uses `localstack` as a local alternative to AWS s3 storage. But persistence accross restarts is not supported without the premium license.
293-
This means that it is expected that you see 404 on uploaded files after a restart of your computer.
294-
In details:
365+
It is possible that the nudenet container pull fails with a 403 status code. This is likely because you are authenticated to the public AWS ECR and trying to pull a public image. Log out of the public ECR with `docker logout public.ecr.aws` and try building the devContainer again.
295366

296-
- the items are persisted in the DB
297-
- the files stored on the fake s3 are not.
367+
### Uploading files results in "AuthorizationHeaderMalformed: Authorization header malformed, unexpected scope"
298368

299-
In the future we might investigate different solutions to mocking s3 storage, or improve the local storage to provide a durable local storage option.
369+
This upload error occurs when we try to upload a file to s3 (mocked by garage on local dev setup).
300370

301-
### Container installation
371+
You need to check that you:
372+
- have access and secret keys in your env
373+
- have set the region to the same value as the ".devcontainer/garage/garage.toml" file (look under the `s3.api` section for the `s3_region` value.) By default it should be `garage` and not `us-east-1`. Update the value in your `.env.development` file.
302374

303-
It is possible that the nudenet container pull fails with a 403 status code. This is likely because you are authenticated to the public AWS ECR and trying to pull a public image. Log out of the public ECR with `docker logout public.ecr.aws` and try building the devContainer again.
375+
### Uploading files throws with "Invalid signature"
304376

377+
This error indicated that the keys to access the buckets are not correct, ensure that you have copied the full key (beware line breaks) and that they are available from the process env.
305378

306379
## Openapi
307380

src/services/file/repositories/s3.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ export class S3FileRepository implements FileRepository {
4747
region,
4848
useAccelerateEndpoint,
4949
credentials: { accessKeyId, secretAccessKey },
50-
// this is necessary because localstack doesn't support hostnames eg: <bucket>.s3.<region>.amazonaws.com/<key>
51-
// so it we must use pathStyle buckets eg: localhost:4566/<bucket>/<key>
50+
// This was required when we used localstack in development, now it is legacy.
51+
// Previously localstack did not allow the use of subdomains for bucket names and instead we had to use path-style urls: localhost:4566/<bucket>/<key> Instead of <bucket>.s3.<region>.amazonaws.com/<key>
5252
forcePathStyle: true,
53-
// this is necessary to use the localstack instance running on graasp-localstack or localhost
53+
// this is necessary to use the garage instance
5454
// this overrides the default endpoint (amazonaws.com) with S3_FILE_ITEM_HOST
5555
endpoint: S3_FILE_ITEM_HOST,
5656
});

0 commit comments

Comments
 (0)