# How to Run MinIO S3 Storage in a Cloud Sandbox

> MinIO's community edition is now source-only, so build it with go install inside a Sandboxer sandbox (93 seconds on default), run it as a systemd service and point the AWS CLI at it with --endpoint-url. A 1 GiB upload took 2.2 to 2.5 seconds inside the VM, and the S3 port worked on a hosted HTTPS URL.

Source: https://sandboxer.com/guides/run-minio-s3-storage-in-a-sandbox. Updated 2026-10-27.

**Short answer:** MinIO's community edition is now source-only, so build it with go install inside a Sandboxer sandbox (93 seconds on default), run it as a systemd service and point the AWS CLI at it with --endpoint-url. A 1 GiB upload took 2.2 to 2.5 seconds inside the VM, and the S3 port worked on a hosted HTTPS URL.

## What we ran

Everything below ran on production on October 27, 2026, in one `default` sandbox (4 vCPU, 8 GB, 50 GB disk). The laptop side used AWS CLI 1.46 and plain `curl`. Timings are single runs or three back-to-back runs; yours will differ.

| Step | Result |
| - | - |
| `sbx new` until ready | 3.2 s |
| `go install github.com/minio/minio@latest` | 1 min 33 s, 147 MB binary |
| Install AWS CLI v2 in the sandbox | 3.1 s |
| `aws s3 cp` 1 GiB into MinIO, in the VM | 2.45 s, 2.17 s, 2.44 s |
| `aws s3 cp` 1 GiB out of MinIO, in the VM | 1.56 s, 2.19 s, 1.99 s |
| Presigned GET from the laptop over the hosted URL | 190 ms median over 20 requests, each a new TLS connection |
| 64 MiB upload / download from the laptop over the hosted URL | 3.3 s / 5.5 s |
| `sbx stop --wait`, then `sbx resume` | 2.2 s and 1.4 s, memory restored, all objects intact |

## Why build from source

As of October 27, 2026, the [MinIO repository](https://github.com/minio/minio) README says the repository is no longer maintained and that the community edition "is now distributed as source code only", with no new pre-built binaries. Its documented install is `go install github.com/minio/minio@latest`, which needs Go 1.24 or later. A sandbox ships Go 1.27, so that path needs nothing extra. Older binaries and Docker images still exist, but the README says they won't get updates. Check the README for the current state before you rely on either. MinIO is AGPLv3; read the license page in the repo if you plan to ship it inside a product.

## 1. Build MinIO in the sandbox

The build runs longer than the default 30-second `sbx exec` timeout, so pass `--timeout` (the maximum is 600; use `--detach` for anything longer):

```bash
sbx new --name minio --type default --no-env
id=sb_km3bx8u9ha    # the id that sbx new printed

sbx exec $id --timeout 600 "go version; time go install github.com/minio/minio@latest; ~/go/bin/minio --version"
```

```text
go version go1.27.2 linux/amd64
go: downloading cel.dev/expr v0.23.1
…
minio version DEVELOPMENT.GOGET (commit-id=DEVELOPMENT.GOGET)
Runtime: go1.27.2 linux/amd64
License: GNU AGPLv3 - https://www.gnu.org/licenses/agpl-3.0.html

real	1m32.817s
user	3m53.265s
```

`minio --version` doesn't say which commit you got. `go version -m` does. On our run, `@latest` resolved to a commit from February 12, 2026:

```bash
sbx exec $id "go version -m ~/go/bin/minio | sed -n 2,3p"
```

```text
	path	github.com/minio/minio
	mod	github.com/minio/minio	v0.0.0-20260212201848-7aac2a2c5b7c	h1:…
```

Pin that pseudo-version instead of `@latest` (`go install github.com/minio/minio@v0.0.0-20260212201848-7aac2a2c5b7c`) if you want the same build every time.

## 2. Run it as a systemd service

MinIO's default login is `minioadmin:minioadmin`. Don't keep it, because step 4 puts the S3 port on the internet. Save this unit as `minio.service` on your laptop:

```ini
[Unit]
Description=MinIO object storage
After=network-online.target
Wants=network-online.target

[Service]
User=user
EnvironmentFile=/etc/default/minio
ExecStart=/usr/local/bin/minio server /home/user/minio-data --address :9000 --console-address :9001
Restart=always
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
```

Copy it in, generate a random root password into a root-only file, and start the service:

```bash
sbx scp ./minio.service $id:/home/user/
sbx exec $id --timeout 120 '
sudo install -m 755 ~/go/bin/minio /usr/local/bin/minio
mkdir -p ~/minio-data
printf "MINIO_ROOT_USER=admin\nMINIO_ROOT_PASSWORD=%s\n" $(openssl rand -hex 16) | sudo tee /etc/default/minio >/dev/null
sudo chmod 600 /etc/default/minio
sudo mv minio.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now minio
sleep 3; systemctl is-active minio
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:9000/minio/health/live'
```

```text
Created symlink /etc/systemd/system/multi-user.target.wants/minio.service → /etc/systemd/system/minio.service.
active
200
```

Port 9000 is the S3 API. Port 9001 is the web console. `--console-address` fixes the console port; without it, MinIO picks a random one.

## 3. Use the AWS CLI in the sandbox

Install AWS CLI v2 with [AWS's own installer](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) (`unzip` is already there), then make a profile from the MinIO credentials. `AWS_ENDPOINT_URL` sends every command to MinIO instead of AWS:

```bash
sbx exec $id --timeout 300 '
cd /tmp && curl -sSL https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip -o awscliv2.zip \
  && unzip -q awscliv2.zip && sudo ./aws/install >/dev/null && aws --version
. <(sudo cat /etc/default/minio)
aws configure set aws_access_key_id "$MINIO_ROOT_USER" --profile minio
aws configure set aws_secret_access_key "$MINIO_ROOT_PASSWORD" --profile minio
aws configure set region us-east-1 --profile minio'
```

```text
aws-cli/2.38.6 Python/3.14.8 Linux/6.18.56-sandboxer exe/x86_64.ubuntu.24
```

Now create a bucket and time 1 GiB of random data in and out. Save this as `s3test.sh`:

```bash
export AWS_PROFILE=minio AWS_ENDPOINT_URL=http://127.0.0.1:9000
TIMEFORMAT='%R s'
aws s3 mb s3://demo
head -c 1G /dev/urandom > /tmp/big.bin
for i in 1 2 3; do echo -n "upload 1 GiB: "; { time aws s3 cp --no-progress /tmp/big.bin s3://demo/big-$i.bin >/dev/null; } 2>&1; done
for i in 1 2 3; do echo -n "download 1 GiB: "; { time aws s3 cp --no-progress s3://demo/big-$i.bin /tmp/back.bin >/dev/null; } 2>&1; done
cmp /tmp/big.bin /tmp/back.bin && echo "checksum match"
aws s3api head-object --bucket demo --key big-1.bin --query '{ETag:ETag,Size:ContentLength}'
```

```bash
sbx scp ./s3test.sh $id:/home/user/
sbx exec $id --timeout 600 "bash s3test.sh"
```

```text
make_bucket: demo
upload 1 GiB: 2.453 s
upload 1 GiB: 2.171 s
upload 1 GiB: 2.435 s
download 1 GiB: 1.558 s
download 1 GiB: 2.191 s
download 1 GiB: 1.988 s
checksum match
{
    "ETag": "\"dc6b68ab07b203cdca0a0afada7bc801-128\"",
    "Size": 1073741824
}
```

That is roughly 440 to 495 MB/s up and 490 to 690 MB/s down, with client and server on the same 4 vCPU VM. The `-128` on the ETag means the CLI split the file into 128 multipart chunks of 8 MiB, its default. That is useful to check if your code compares ETags with MD5 sums: multipart ETags aren't MD5s of the file.

## 4. Reach the S3 API from your laptop

There are two ways. Pick by who needs access.

| You want | Do this | Who can reach the bucket |
| - | - | - |
| The AWS CLI or an SDK on your own machine only | `ssh -L` tunnel to port 9000, keep it unhosted | Only you, through SSH with your key |
| Any S3 client anywhere, or presigned links to share | `sbx host $id 9000 --public` | Anyone with valid S3 credentials or a presigned URL; anonymous requests get `403` |

**A private hosted URL doesn't work for S3 clients.** We hosted 9000 privately first. The AWS CLI can't attach the URL's `_token`, so it got the proxy's `401`:

```bash
aws --endpoint-url https://prompt-copper-marsh-9000.box.sandboxer.com s3 ls
```

```text
An error occurred (401) when calling the ListBuckets operation: Unauthorized
```

**Option A: SSH tunnel.** Connect once with `sbx ssh` so the CLI authorizes its key, then forward a local port. `sbx ssh-config > ~/.ssh/sbx.conf` saves the `Host *.sbx` stanza that `-F` reads; the [MongoDB guide](/guides/run-mongodb-in-a-sandbox) walks through it.

```bash
sbx ssh $id true
ssh -F ~/.ssh/sbx.conf -f -N -o ExitOnForwardFailure=yes -L 9100:127.0.0.1:9000 $id.sbx
aws --endpoint-url http://127.0.0.1:9100 s3 ls s3://demo/ --recursive
```

```text
2026-10-27 09:57:58 1073741824 big-1.bin
2026-10-27 09:58:01 1073741824 big-2.bin
2026-10-27 09:58:03 1073741824 big-3.bin
…
```

**Option B: a public hosted URL.** Hosted URLs serve HTTPS, and MinIO's own SigV4 check still guards every request, so a public S3 port is as safe as your MinIO credentials. That is why step 2 replaced the default password.

```bash
sbx host $id 9000 --public
B=https://prompt-copper-marsh-9000.box.sandboxer.com
curl -s -o /dev/null -w '%{http_code}\n' $B/minio/health/live      # health check
curl -s $B/demo/ | head -c 120; echo                               # anonymous listing
```

```text
200
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied.</Message><BucketName>demo</BucketName>
```

With the credentials in `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` on the laptop, the normal CLI works through the URL. Signatures survive the proxy:

```bash
echo "hello from my laptop" > note.txt
aws --endpoint-url $B s3 cp note.txt s3://demo/notes/note.txt
url=$(aws --endpoint-url $B s3 presign s3://demo/notes/note.txt --expires-in 300)
curl -s "$url"
curl -s -o /dev/null -w '%{http_code}\n' $B/demo/notes/note.txt
```

```text
upload: ./note.txt to s3://demo/notes/note.txt
hello from my laptop
403
```

The presigned link works for anyone for 5 minutes. The same object without a signature gets `403`. To size the network cost, we fetched the presigned URL 20 times with `curl` (`-w '%{time_total}'`): median 190 ms, min 189 ms, max 234 ms. Each request opened a new TLS connection, so an SDK that reuses connections will do better. A 64 MiB file took 3.3 s up and 5.5 s down from our laptop. That is the laptop's link, not MinIO, so put bulk jobs in the sandbox next to the data.

## 5. The web console on a private URL

The console is a browser app, so a private hosted URL fits it:

```bash
sbx host $id 9001 --title "MinIO console"
```

It prints `https://prompt-copper-marsh-9001.box.sandboxer.com?_token=…`. Treat it like a password. Without the token the proxy answered `401`. With it, `curl -L` with a cookie jar got `200` and a page titled `MinIO Console`. Log in with `admin` and the password in `/etc/default/minio`. Read it with `sudo cat` in an `sbx ssh` session, not in a shared log. We checked the HTTP status and page title only, not what the console can do.

## 6. What survives a stop and resume

```bash
sbx stop $id --wait
sbx resume $id
aws --endpoint-url $B s3 ls s3://demo/ --recursive --summarize | tail -2
```

```text
Total Objects: 5
   Total Size: 3288334357
```

The stop took 2.2 s and the resume 1.4 s, with `memoryRestored: true`. `systemctl show minio -p ActiveEnterTimestamp` still showed the original start time, so MinIO never restarted. Both hosted URLs answered again with the same token. In this run, the laptop's `ssh -L` tunnel also kept working after the resume. Don't count on it: if yours has dropped, run the same `ssh -L` line again.

A resume boots from disk instead when no machine with the same CPU type is free, or when you move to a bigger size. Then processes are gone, but the objects in `~/minio-data` are on the disk, and the `enable`d unit starts MinIO at boot. We didn't force a disk boot. We did kill the process to check `Restart=always`:

```bash
sbx exec $id "systemctl is-enabled minio; sudo pkill -9 -x minio; sleep 7; systemctl show minio -p NRestarts -p ActiveState"
```

```text
enabled
NRestarts=1
ActiveState=active
```

Moving to a smaller size isn't supported yet. Create a new sandbox and copy the bucket across with `aws s3 sync` between the two endpoints.

## 7. Clean up

```bash
sbx delete $id --yes
```

A `default` sandbox costs $0.036/hour while it runs and nothing while it is stopped.

## FAQ

### Can I use the official MinIO Docker image instead?

The [minio/minio repository](https://github.com/minio/minio) says the community edition is now source-only and older pre-built releases won't be updated. Docker is preinstalled, so an existing image will run, but we built from source as the README recommends and didn't test an image here.

### Do AWS SDKs need path-style addressing for MinIO?

Use path-style. Virtual-host style puts the bucket in the hostname (`demo.prompt-copper-marsh-9000…`), a name that `sbx host` never gives you, so we didn't try it. The AWS CLI used path-style requests against our custom endpoint without any setting. In an SDK, set the path-style option, for example `forcePathStyle: true` in the AWS SDK for JavaScript v3. We tested only the CLI.

### Why does `aws s3 ls` get 401 on a private hosted URL?

The `401` comes from Sandboxer's proxy, before MinIO sees the request. Private URLs need the `_token` query parameter or the cookie it sets, and S3 clients send neither. Use an SSH tunnel, or host the port with `--public` and rely on MinIO's credentials.

### Where are the objects stored, and how much fits?

On the sandbox disk under `/home/user/minio-data`. A `default` sandbox has a 50 GB disk; `df -h` showed 43 GB free after the build and three 1 GiB objects. [Machine sizes](/docs/machines) lists the disk for each type.

### Can a sandbox upload to real Amazon S3 instead?

Yes. Outbound HTTPS is open, so drop `--endpoint-url` and use AWS credentials. Pass them as described in [passing secrets to a sandbox](/guides/pass-secrets-to-a-sandbox-safely) rather than writing them into scripts.

## Related

- [Run MongoDB in a sandbox](/guides/run-mongodb-in-a-sandbox)
- [Give a sandbox port an HTTPS URL](/guides/give-a-sandbox-port-an-https-url)
- [Move files in and out of a sandbox](/guides/move-files-in-and-out-of-a-sandbox)
- [Hosting](/docs/hosting)
