DEV Community

DeployLark
DeployLark

Posted on Originally published at deploylark.com

S3 NoSuchBucket in Docker: Fix Bucket Not Found Errors

Your app talks to self-hosted S3 storage in Docker, and the upload fails with NoSuchBucket. The message says Bucket not found or The specified bucket does not exist. But the bucket is right there in the admin UI. Or the same call works from the AWS CLI and fails from your Node app.

This guide reproduces those failures in Docker Compose with Garage v2.5.0, a maintained open-source S3 server, and fixes each one. Every Garage and RustFS error below comes from that lab run on November 2, 2026. In several cases the bucket existed the whole time. The server read a different bucket name than the one you meant, because of a typo, the URL style, the endpoint or the access key.

The questions get steady traffic. On Stack Overflow, Code: NoSuchBucket had 78,316 views on November 2, 2026, and docker-compose: how to use minio in- and outside of the docker network had 57,838.

The short version:

  • Read the bucket name inside the error. Garage prints it, as in Bucket not found: cat.txt. If that isn't your bucket, the request was built wrong.
  • Set forcePathStyle: true (or the path-style option of your SDK) for self-hosted endpoints. The AWS SDK for JavaScript v3 puts the bucket in the hostname by default.
  • Set the region to the one the server expects. Garage uses garage unless you change s3_region.
  • Point the endpoint at the S3 API port (3900 for Garage) with no bucket in the URL.
  • AccessDenied means the bucket exists but your key can't use it. NoSuchBucket means the server found no bucket under that name for that key.

Fix NoSuchBucket: find the bucket name the server saw

Read the name in the error

The lab created one bucket, photos, and one access key with read and write rights on it. Then it ran aws 2.39.3 in a container on the same Compose network with AWS_ENDPOINT_URL=http://garage:3900 and AWS_DEFAULT_REGION=garage:

Terminal capture: garage bucket list shows photos and invoices, aws s3 ls s3://Photos and a copy to s3://pictures fail with NoSuchBucket Bucket not found, head-bucket returns 404 Not Found, the raw XML error body names the bucket, and aws s3 ls s3://photos lists cat.txt

Bucket names are case sensitive. Photos is not photos, so Garage answered NoSuchBucket with Bucket not found: Photos. The raw XML body carries the same name in <Message> and <Resource>:

<Error><Code>NoSuchBucket</Code><Message>Bucket not found: pictures</Message><Resource>/pictures</Resource><Region>garage</Region></Error>
Enter fullscreen mode Exit fullscreen mode

The Garage log shows the same thing with the method and path, for example error 404 Not Found, Bucket not found: pictures ... HEAD /pictures. Run docker compose logs garage and compare the path with the bucket you meant.

Other servers word it differently. RustFS 1.0.4 answered a missing bucket with NoSuchBucket: The specified bucket does not exist, the same text MinIO used, and rejected Photos with InvalidBucketName: The specified bucket is not valid. Supabase Storage wraps the error in JSON, {"statusCode":"404","error":"Bucket not found","message":"Bucket not found","code":"NoSuchBucket"}, which the Supabase self-hosting guide ran into before the bucket was created.

Check the bucket with head-bucket

aws s3api head-bucket --bucket pictures returned An error occurred (404) when calling the HeadBucket operation: Not Found and exit code 254. A HEAD response has no body, so you get a bare 404 instead of NoSuchBucket. That makes it a quick existence check in a script. On Garage, docker compose exec garage /garage bucket list shows every bucket name, called a global alias, and every per-key name, called a local alias. More on those below.

Fix Bucket not found caused by virtual-hosted URLs

Start with how the request is built. S3 clients can address a bucket in two ways, and the AWS docs on virtual hosting describe both. Path-style puts the bucket in the path. Virtual-hosted style puts it in the hostname.

Diagram: the same GetObject call for bucket photos and key cat.txt. With forcePathStyle true the request goes to http://garage:3900/photos/cat.txt and works. With the SDK default it goes to http://photos.garage:3900/cat.txt and fails with getaddrinfo ENOTFOUND photos.garage, or with NoSuchBucket Bucket not found cat.txt when that name resolves to Garage

Set forcePathStyle for self-hosted endpoints

The lab used a small Node.js script with @aws-sdk/client-s3 3.1161.0 on node:24.22.2-alpine. It set endpoint: "http://garage:3900" and region: "garage" and nothing else. The SDK built http://photos.garage:3900/cat.txt. No DNS record exists for photos.garage, so the call failed before it reached Garage:

Error: getaddrinfo ENOTFOUND photos.garage
Enter fullscreen mode Exit fullscreen mode

The box this ran on resolves unknown names to a proxy, so the lab pointed the client at a CoreDNS 1.15.0 container that answers "no such name" for everything, the way a normal network does. With forcePathStyle: true, the same script fetched http://garage:3900/photos/cat.txt and printed OK hello garage.

The worse case is when photos.garage does resolve, for example through a wildcard DNS record or a proxy that sends every subdomain to Garage. The lab simulated that with a Compose network alias. Garage did not recognize the hostname, so it treated the request as path-style and read the object key as the bucket name:

Terminal capture: the AWS SDK for JavaScript v3 with its default URL style fails with getaddrinfo ENOTFOUND photos.garage, then with NoSuchBucket Bucket not found cat.txt once the name resolves, the Garage log shows GET /cat.txt, and forcePathStyle true or a matching root_domain endpoint works

NoSuchBucket: Bucket not found: cat.txt is the giveaway. cat.txt is a file, not a bucket. An upload failed in a stranger way. Garage read PUT /from-node.txt as a request to create a bucket called from-node.txt and returned InvalidRequest: Bad request: Invalid create bucket XML query. Here is the client config that fixed both:

import { S3Client } from "@aws-sdk/client-s3";

const s3 = new S3Client({
  endpoint: "http://garage:3900",
  region: "garage",
  forcePathStyle: true, // http://garage:3900/<bucket>/<key>
});
Enter fullscreen mode Exit fullscreen mode

Other SDKs have their own path-style option, so check their docs. The AWS CLI 2.39.3 in the lab used path-style on its own with a custom endpoint. Its debug log showed http://garage:3900/photos?list-type=2.... That is why the CLI worked while the Node app failed with the same keys.

Or configure root_domain and wildcard DNS

If an app can't do path-style, give Garage a domain for virtual-hosted requests. The lab config had root_domain = ".s3.garage.localhost" under [s3_api]. With the endpoint set to http://s3.garage.localhost:3900 and a network alias for photos.s3.garage.localhost, the script worked without forcePathStyle. In production that alias is a wildcard DNS record such as *.s3.example.com, plus a wildcard TLS certificate if you serve HTTPS. The Garage configuration reference notes: "Note path-style requests are always enabled, whether or not vhost-style is configured." So path-style keeps working after you add the domain. Garage picks the bucket from the Host header, so a reverse proxy in front has to pass that header through unchanged. The Traefik labels guide covers routing by host name.

Fix the region and the endpoint URL

Match the region to s3_region

Garage only accepts requests signed for its own region. The Garage configuration reference says: "Garage will accept S3 API calls that are targeted to the S3 region defined here. API calls targeted to other regions will fail with a AuthorizationHeaderMalformed error message that redirects the client to the correct region."

Terminal capture: the SDK with region us-east-1 fails with AuthorizationHeaderMalformed unexpected scope, the AWS CLI debug log shows a us-east-1 request followed by a retry signed for garage, port 3902 returns 404, port 3903 returns 400, and an endpoint that includes the bucket makes Garage look up /photos/photos/cat.txt

With region: "us-east-1", the Node SDK failed with AuthorizationHeaderMalformed: Authorization header malformed, unexpected scope: '20261102/us-east-1/s3/aws4_request', expected: '20261102/garage/s3/aws4_request'. The AWS CLI hid the same mistake. With --region us-east-1 it listed the bucket fine, and its debug log showed why: one request signed for us-east-1, the AuthorizationHeaderMalformed answer, then a retry signed for garage. So set the region in your app to the s3_region value from garage.toml, which is garage in the Garage quick start. RustFS accepted us-east-1 in the lab.

Point the endpoint at the S3 port, without the bucket

Garage listens on several ports. Only 3900 speaks the S3 API. The lab tried the others:

  • http://garage:3902 (the static website port) returned (404) ... Not Found.
  • http://garage:3903 (the admin API) returned (400) ... Bad Request.
  • http://garage:3900/photos, with the bucket inside the endpoint, made the client add the bucket a second time. Garage looked up /photos/photos/cat.txt, and aws s3 ls failed with NoSuchKey: Key not found.
  • A wrong secret key returned AccessDenied: Forbidden: Invalid signature.

The endpoint is the server address only, such as http://garage:3900. The bucket goes in the bucket parameter. From another container, use the Compose service name. From the host, use the published port. In the lab, --endpoint-url http://127.0.0.1:3900 listed the bucket from the host. localhost inside a container is that container, the same trap the Redis in Docker guide shows. And if you put a TLS proxy in front, match http and https with what the proxy serves. The private registry guide shows what a scheme mismatch looks like.

AccessDenied or NoSuchBucket: check the access key

Grant the key access to the bucket

In Garage, a key sees nothing until you run garage bucket allow for it. The lab created report-key with read access to photos only:

Terminal capture: report-key lists only photos, gets AccessDenied on invoices, NoSuchBucket on a missing bucket and on another key's local alias media, AccessDenied when creating a bucket, and RustFS returns The specified bucket does not exist and InvalidBucketName for the same mistakes

The difference matters. invoices exists, so the key got AccessDenied: Forbidden: Operation is not allowed for this key. A bucket that didn't exist got NoSuchBucket. aws s3 ls only listed photos, the one bucket the key could use. Creating a bucket failed with AccessDenied: Forbidden: Access key GK8a04... is not allowed to create buckets, because the key had no create right. Grant access with:

docker compose exec garage /garage bucket allow --read --write photos --key report-key
Enter fullscreen mode Exit fullscreen mode

The Garage quick start also adds --owner. Run garage key info <key> to see which buckets a key can use.

Local aliases are per key

Garage lets a key give a bucket its own private name, a local alias. The lab ran garage bucket alias --local app-key photos media. For app-key, s3://media listed the files in photos. For report-key, which had read access to the same bucket, s3://media returned NoSuchBucket: Bucket not found: media. If two services share a bucket, use the global alias, or create the same local alias for each key.

MinIO images are gone: a Garage Compose file that works

What happened to minio/minio

Many older tutorials pin minio/minio. On November 2, 2026, the Docker Hub API answered object not found for that repository. docker pull minio/minio:RELEASE.2025-09-07T16-13-09Z failed with pull access denied for minio/minio, repository does not exist, and the same tag on quay.io/minio/minio returned unauthorized: access to the requested resource is not authorized. The MinIO GitHub repository was archived on April 25, 2026, and its README says "The MinIO community edition is now distributed as source code only." StableBuild's write-up dates the Docker Hub removal to around September 12, 2026.

So a fresh docker compose up with a MinIO image now fails before any bucket error can happen. The lab moved to Garage. RustFS 1.0.4 also ran fine, and its error messages are closer to MinIO's. The fixes above apply to both, since they come from the S3 protocol and the client.

compose.yaml, garage.toml and the app

This setup ran verbatim in the lab. It uses the --single-node and --default-bucket flags from the Garage quick start, which create the layout, one access key and one bucket from environment variables:

metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "sqlite"
replication_factor = 1

rpc_bind_addr = "[::]:3901"
rpc_public_addr = "127.0.0.1:3901"

[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"

[admin]
api_bind_addr = "[::]:3903"
Enter fullscreen mode Exit fullscreen mode
services:
  garage:
    image: dxflrs/garage:v2.5.0
    command: ["/garage", "server", "--single-node", "--default-bucket"]
    environment:
      GARAGE_RPC_SECRET: ${GARAGE_RPC_SECRET}
      GARAGE_ADMIN_TOKEN: ${GARAGE_ADMIN_TOKEN}
      GARAGE_DEFAULT_ACCESS_KEY: ${S3_ACCESS_KEY}
      GARAGE_DEFAULT_SECRET_KEY: ${S3_SECRET_KEY}
      GARAGE_DEFAULT_BUCKET: ${S3_BUCKET}
    volumes:
      - ./garage.toml:/etc/garage.toml:ro
      - garage-meta:/var/lib/garage/meta
      - garage-data:/var/lib/garage/data
    ports:
      - "127.0.0.1:3900:3900"
    healthcheck:
      test: ["CMD", "/garage", "status"]
      interval: 10s
      timeout: 5s
      retries: 5

  app:
    image: node:24.22.2-alpine
    working_dir: /app
    command: ["node", "app.mjs"]
    depends_on:
      garage:
        condition: service_healthy
    environment:
      S3_ENDPOINT: http://garage:3900
      S3_REGION: garage
      S3_BUCKET: ${S3_BUCKET}
      AWS_ACCESS_KEY_ID: ${S3_ACCESS_KEY}
      AWS_SECRET_ACCESS_KEY: ${S3_SECRET_KEY}
    volumes:
      - ./app:/app:ro

volumes:
  garage-meta:
  garage-data:
Enter fullscreen mode Exit fullscreen mode

Fill .env with your own random values. The access key ID starts with GK, as in the quick start:

cat > .env <<EOF
GARAGE_RPC_SECRET=$(openssl rand -hex 32)
GARAGE_ADMIN_TOKEN=$(openssl rand -base64 32)
S3_ACCESS_KEY=GK$(openssl rand -hex 16)
S3_SECRET_KEY=$(openssl rand -hex 32)
S3_BUCKET=uploads
EOF
Enter fullscreen mode Exit fullscreen mode

./app/app.mjs stands in for your real service. It needs npm install @aws-sdk/client-s3@3.1161.0 in ./app first:

import { S3Client, PutObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";

const s3 = new S3Client({
  endpoint: process.env.S3_ENDPOINT,   // http://garage:3900
  region: process.env.S3_REGION,       // must match s3_region in garage.toml
  forcePathStyle: true,                // http://garage:3900/<bucket>/<key>
});
const Bucket = process.env.S3_BUCKET;

await s3.send(new PutObjectCommand({ Bucket, Key: "hello.txt", Body: "hello" }));
const { Contents } = await s3.send(new ListObjectsV2Command({ Bucket }));
console.log(`${Bucket}: ${Contents.map((o) => o.Key).join(", ")}`);
Enter fullscreen mode Exit fullscreen mode

Check it

<code>text
$ docker compose up -d
Container s3final-garage-1 Healthy
Container s3final-app-1 Started
$ docker compose ps -a --format '&#123;&#123;.Service&#125;&#125; &#123;&#123;.Status&#125;&#125;'
app Exited (0) 1 second ago
garage Up 12 seconds (healthy)
$ docker compose logs --no-log-prefix app
uploads: hello.txt
$ docker compose restart garage
$ docker compose run --rm app
uploads: hello.txt
</code>

On the first start, Garage logged Created initial layout for single-node configuration, Creating default access key and Creating default bucket uploads. The SDK reads the key from AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY on its own. Changing S3_BUCKET later behaves differently from database images. With S3_BUCKET=assets, Garage created a second bucket, assets, on the next start and kept uploads with its files. The default key kept its rights on uploads, and the app still read it after switching back. Keep the image pinned, here dxflrs/garage:v2.5.0, so a restart never pulls a new release onto your data.

FAQ

Errors

What does NoSuchBucket mean?

The server found no bucket with the name in the request, for the key you used. Check the name in the error message first. If it shows a file name or a word you never used as a bucket, fix the URL style or the endpoint. If it shows your bucket, check the spelling, the case, and whether the bucket was created on this server.

Why does the AWS CLI work but my app gets NoSuchBucket or ENOTFOUND?

The AWS CLI 2.39.3 used path-style URLs with a custom endpoint, and it retried a wrong region on its own. The AWS SDK for JavaScript v3 did neither. Set forcePathStyle: true and the server's region in the app.

What does "The authorization header is malformed" mean with self-hosted S3?

The client signed the request for a different region than the server uses. Garage answered AuthorizationHeaderMalformed with the expected scope in the message. Set the client region to the server's s3_region.

Setup

How do I check if a bucket exists?

Run aws s3api head-bucket --bucket <name>. In the lab, exit code 0 meant the bucket existed and the key could use it. A missing bucket returned (404) ... Not Found and a bucket the key had no rights on returned (403) ... Forbidden, both with exit code 254.

How do I connect to S3 storage inside and outside the Docker network?

Inside the Compose network, use the service name, such as http://garage:3900. From the host, publish the port and use http://127.0.0.1:3900. Use path-style in both cases, so the bucket never ends up in a hostname.

Can I still pull the MinIO Docker image?

Not from Docker Hub. On November 2, 2026, minio/minio no longer existed there, and the Quay copy returned unauthorized. The community edition is source only. Use a maintained server such as Garage or RustFS, or build MinIO from source at your own risk.

Top comments (0)