How to Run MinIO S3 Storage in a Cloud Sandbox
Updated October 27, 2026 .md
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 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):
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"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.265sminio --version doesn't say which commit you got. go version -m does. On our run, @latest resolved to a commit from February 12, 2026:
sbx exec $id "go version -m ~/go/bin/minio | sed -n 2,3p" 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:
[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.targetCopy it in, generate a random root password into a root-only file, and start the service:
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'Created symlink /etc/systemd/system/multi-user.target.wants/minio.service → /etc/systemd/system/minio.service.
active
200Port 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 (unzip is already there), then make a profile from the MinIO credentials. AWS_ENDPOINT_URL sends every command to MinIO instead of AWS:
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'aws-cli/2.38.6 Python/3.14.8 Linux/6.18.56-sandboxer exe/x86_64.ubuntu.24Now create a bucket and time 1 GiB of random data in and out. Save this as s3test.sh:
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}'sbx scp ./s3test.sh $id:/home/user/
sbx exec $id --timeout 600 "bash s3test.sh"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:
aws --endpoint-url https://prompt-copper-marsh-9000.box.sandboxer.com s3 lsAn error occurred (401) when calling the ListBuckets operation: UnauthorizedOption 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 walks through it.
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/ --recursive2026-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.
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 listing200
<?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:
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.txtupload: ./note.txt to s3://demo/notes/note.txt
hello from my laptop
403The 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:
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#
sbx stop $id --wait
sbx resume $id
aws --endpoint-url $B s3 ls s3://demo/ --recursive --summarize | tail -2Total Objects: 5
Total Size: 3288334357The 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 enabled unit starts MinIO at boot. We didn't force a disk boot. We did kill the process to check Restart=always:
sbx exec $id "systemctl is-enabled minio; sudo pkill -9 -x minio; sleep 7; systemctl show minio -p NRestarts -p ActiveState"enabled
NRestarts=1
ActiveState=activeMoving 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#
sbx delete $id --yesA 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 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 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 rather than writing them into scripts.
Related#
More guides
For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.
For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.
The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.
If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.