01 / The concept
One storage layer for documents, backups and models
Our document app processes scans and invoices on a Mac. Finished documents should move to the NAS instead of filling the Mac's disk. Its nightly backups need a home with their own retention rules. We also keep copies of the language models we run locally, with Model Library, our open-source model archive. That deserves its own post.
All of it goes through MinIO, an S3-compatible object store. The app only knows an endpoint, a bucket and a key. It does not care whether the bucket lives on our NAS or at a cloud provider, and that is the point. If MinIO stops being the right choice, we change an endpoint, not the app.
02 / The problem
The admin pages left the free console
In March 2025 MinIO merged a change that reduced the Community console to an object browser. Users, policies and access keys moved to the mc command line. Our console showed buckets and files. Nothing else.
A community fork, OpenMaxIO, set out to keep the full console. We hit two traps on the way.
The fork's main branch already contains MinIO's change. We built it first and got the same stripped object browser, just with a different logo. The last version with the admin pages is the tag v1.7.6, from February 2025.
That tag no longer builds as it is. Its front end pulls MinIO's design library from github.com/minio/mds, and that repository is gone. OpenMaxIO keeps a copy at github.com/openmaxio/mds with the exact commit v1.7.6 expects, so one line in the build points the dependency there.
A smaller one cost us a build too. Yarn 4 could not clone that dependency inside the Alpine-based Node image and failed with invalid key: core.autocrlf. The Debian-based image works.
03 / How it works
One console, one key per app, nothing deleted unchecked
The full console from v1.7.6
We build the console from source in a container. The two fixes are the base image and the redirected dependency.
FROM node:20-bookworm-slim AS web
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates
WORKDIR /src
RUN git clone https://github.com/OpenMaxIO/openmaxio-object-browser . && git checkout v1.7.6
WORKDIR /src/web-app
RUN sed -i 's#github.com/minio/mds.git#github.com/openmaxio/mds.git#g' package.json yarn.lock
RUN corepack enable && yarn install && yarn build
FROM golang:1.23-alpine AS go
COPY --from=web /src /src
WORKDIR /src
RUN CGO_ENABLED=0 go build -trimpath --tags=kqueue -o /console ./cmd/console
FROM alpine:3.20
COPY --from=go /console /usr/local/bin/console
ENTRYPOINT ["console", "server", "--port", "9090"]The console runs as its own container on MinIO's Docker network and talks to the S3 API internally. MinIO itself stays as it was. We log in with a separate admin user, not the root account, so the root credentials never pass through the console.
One bucket, one user, one key per app
For the document app we wrote a policy that allows listing one bucket and reading, writing and deleting objects inside it.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:ListBucket", "s3:GetBucketLocation"],
"Resource": ["arn:aws:s3:::documents"]
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": ["arn:aws:s3:::documents/*"]
}
]
}A user gets that policy, and the app gets an access key belonging to that user. Before handing the key over we tested it both ways. It lists, writes, reads and deletes in its bucket. Listing another bucket, calling the admin API and creating a bucket all return Access Denied.
Test the denials. A key that can do everything passes the positive test too.
Versioning instead of a cleanup script
With versioning on, MinIO keeps the previous version when the app overwrites or deletes a file. A lifecycle rule removes those old versions after 30 days, so the bucket does not grow forever.
Backups go to a second bucket that the app key cannot reach, with a rule that deletes objects 30 days after upload. A bug in the app, or a leaked app key, cannot delete its own backups.
mc version enable nas/documents
mc ilm rule add nas/documents --noncurrent-expire-days 30
mc ilm rule add nas/documents-backups --expire-days 30Keys in a file nobody reads
While setting this up we found the keys for the app's offsite backup to Wasabi. They had been in .env.local since June. Docker Compose only reads .env, and the compose file never passed them to the containers. The backup job existed and looked configured, and it had never uploaded a single document.
So the MinIO key went into the macOS Keychain instead of a file. Containers cannot read the Keychain, so the next step is a start command that reads the key and hands it to the containers as environment variables. That part is not built yet.
Read it back before you delete
We moved 17 GB of the app's backups into the backup bucket. Before deleting a local copy, we downloaded each file again and compared SHA-256 checksums. All 42 files matched.
One trap if you take a shortcut. For files uploaded in parts, the ETag MinIO reports is not the MD5 of the file. Comparing ETags with local MD5 sums flags those files as different even when they are identical.
The risk we accept
v1.7.6 is from February 2025, and the fork has not moved since June 2025. We get no new console features, so the console stays on our private network and never faces the internet.
The server has the same problem. MinIO archived its open-source repository in April 2026. The last release, from October 2025, fixes a high-severity privilege escalation for access keys, and no further fixes will come. Run that release, not an older one.
Build the last server release yourself
MinIO no longer publishes images, so we build that release from its source tag, the same way as the console. Copy the environment defaults from MinIO's own Dockerfile.release at that tag, so the server behaves as before. Back up .minio.sys in the data directory first. It holds users, policies and keys.
FROM golang:1.24-alpine AS build
RUN apk add --no-cache git
WORKDIR /src
RUN git clone --depth 1 --branch RELEASE.2025-10-15T17-29-55Z https://github.com/minio/minio .
RUN CGO_ENABLED=0 go build -tags kqueue -trimpath -ldflags "$(go run buildscripts/gen-ldflags.go)" -o /minio
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
COPY --from=build /minio /usr/bin/minio
COPY --from=build /src/dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
CMD ["minio"]One trap along the way. We had set a custom region on the server, and the console could no longer log in. The server answered with the region is wrong; expecting 'us-east-1', although it expected our custom region. With a region set, MinIO only accepts requests signed for that region. Leave it empty unless you need it, or set CONSOLE_MINIO_REGION on the console to match.
We accept this for now because our apps only use the S3 API. If we have to leave, Garage and SeaweedFS speak the same API, and the apps keep their code.
04 / Installation
Run the console next to your MinIO
You need MinIO in Docker, the mc client, and 10 to 15 minutes for the first build.
- Create an admin user for the console with a policy that allows
admin:*ands3:*. - Build the image from the Dockerfile above and start it on MinIO's Docker network, with
CONSOLE_MINIO_SERVER,CONSOLE_PBKDF_PASSPHRASEandCONSOLE_PBKDF_SALTset. - For each app, create a policy for its bucket, a user with that policy and an access key. Test what the key may do and what it may not.
- Turn on versioning and add lifecycle rules.
services:
console:
build: .
environment:
CONSOLE_MINIO_SERVER: http://minio:9000
CONSOLE_PBKDF_PASSPHRASE: ${CONSOLE_PBKDF_PASSPHRASE}
CONSOLE_PBKDF_SALT: ${CONSOLE_PBKDF_SALT}
ports: ["9102:9090"]
networks: [minio]
networks:
minio: { external: true, name: minio_default }mc admin policy create nas consoleAdmin console-admin.json
mc admin user add nas console-admin "$CONSOLE_ADMIN_PASSWORD"
mc admin policy attach nas consoleAdmin --user console-adminDelegate this guide
Give this to your agent
Copy this prompt into your coding agent. It should check the prerequisites, carry out this guide and report what it verified.
Help me run the OpenMaxIO admin console next to my existing MinIO and give one app its own bucket and key, as described in this article.
First check that MinIO already runs in Docker, find its Docker network and confirm that the mc client reaches it with an admin account. If there is no MinIO yet, stop and tell me. This guide does not cover a fresh install, because MinIO's open-source project is archived. If the server is older than RELEASE.2025-10-15T17-29-55Z, tell me before going further. Do not change the MinIO server or its data.
Build OpenMaxIO from the tag v1.7.6 with the two fixes from the article: a Debian-based Node image, and the mds dependency redirected to github.com/openmaxio/mds. Do not build the main branch, it lacks the admin pages. Generate CONSOLE_PBKDF_PASSPHRASE and CONSOLE_PBKDF_SALT, keep them in a local .env that stays out of git, and start the console on MinIO's Docker network. Create a separate console admin user instead of using the root account.
Then ask me which app and bucket to set up. Create a policy limited to that bucket, a user with that policy and an access key for the user. Test the key both ways. It must list, write, read and delete a test object in its bucket, and it must be denied on another bucket, on the admin API and when creating a bucket. Remove the test object afterwards.
Ask before enabling versioning, adding lifecycle rules or deleting anything. Never print secrets in full, and store them where I tell you. Report the console URL, what passed, what failed and how to stop the console.05 / Takeaways
Why you might care
If you already run MinIO
The admin pages are not gone for good. OpenMaxIO v1.7.6 brings back users, policies and access keys, once you apply the two fixes above. Move your server to the last MinIO release from October 2025 as well. Nothing newer will come.
If you build systems that produce a lot of data
You know how it goes. Generated files pile up on the laptop until the disk is full, and every project invents its own way to copy them somewhere else. An S3 store on your NAS gives every project the same interface. The code talks to a bucket with a key, and versioning and expiry become settings instead of scripts.
For a new setup today we would look at Garage or SeaweedFS first, because MinIO's open-source project is archived. The code you write against the S3 API stays the same either way.
