Set Up MinIO Object Storage for Self-Hosted Apps with Docker Compose

Stand up a small S3-compatible storage service with MinIO, persistent storage, a clear console/API split, and a safer first-run workflow than “docker up and hope.”

MinIO Docker Compose Object storage
Illustrated guide cover for Set Up MinIO Object Storage for Self-Hosted Apps with Docker Compose
MinIO • Docker Compose • Object storage
What you learn

How to deploy a small MinIO service, persist its data, create a bucket, generate an access key, and verify that the service survives restart.

Best for

Backups, app uploads, artifact storage, and other self-hosted workloads that need an S3-compatible endpoint you control.

Risk to watch

The fastest path to trouble is exposing MinIO directly on the public internet before you have TLS, intentional auth, and a recovery plan.

Before you begin

  • A Linux host with Docker Engine and Compose V2.
  • A directory where you can store persistent MinIO data.
  • A plan for whether MinIO stays private behind a tunnel or proxy, or will later sit behind HTTPS deliberately.
  • Enough disk space for both live data and future backups.

MinIO's current docs still make two important basics obvious: the S3 API commonly lives on port 9000, and the web console commonly lives on port 9001. Keep those roles separate in your head from the start. The API is for applications. The console is for human administration.

Expected outcome: By the end, you should have a single-node MinIO deployment that persists data, serves an S3-compatible API, and exposes a console you can reach intentionally.

Step 1: Create a clean project layout

Start with a small dedicated directory:

mkdir -p ~/minio-compose/data
cd ~/minio-compose

Keeping MinIO in its own Compose project makes later upgrades, restarts, and backup planning clearer than burying it inside a giant unrelated stack.

Step 2: Write the Compose file and env file

Create a local environment file first:

cat > .env <<'EOF'
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=replace-this-with-a-long-random-password
EOF

Then create compose.yaml:

services:
  minio:
    image: minio/minio:latest
    container_name: minio
    restart: unless-stopped
    env_file:
      - .env
    command: server /data --console-address ":9001"
    ports:
      - "9000:9000"
      - "9001:9001"
    volumes:
      - ./data:/data

The important part is not the YAML formatting trophy. It is the intent: persistent storage under /data, a console bound to 9001, and credentials stored outside the Compose file itself.

Warning: Replace the default-looking root password before you treat the service as anything more than a throwaway test. Root credentials are for bootstrap, not long-term application use.

Step 3: Launch and verify MinIO

Validate the rendered config first, then start the service:

docker compose config
docker compose up -d
docker compose ps
docker compose logs --tail=50

After startup, open http://localhost:9001 if you are on the same machine, or use a private tunnel if you are managing the host remotely. Sign in with the root credentials from the env file.

From MinIO's docs, the console and API are separate endpoints by design. Do not point application clients at the console port by mistake. Normal S3-compatible client traffic belongs on 9000.

Step 4: Create a bucket and application credentials

You can manage MinIO through the console, but the mc client makes repeatable setup easier. Once installed, add an alias:

mc alias set localminio http://127.0.0.1:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"

Create a first bucket:

mc mb localminio/app-data

Then create an application-specific access key through the console or the MinIO admin workflow. Use that key for apps and backup tools instead of normalizing the root user everywhere.

This is also the right moment to write down which bucket names are for uploads, backups, or artifacts so future services do not pile into one vague storage bucket.

Step 5: Decide how clients should reach it

You have three sane early patterns:

  • Local-only or same-host use, where the app talks to http://minio:9000 inside a Compose network.
  • Private admin access through SSH port forwarding or a tailnet.
  • A deliberate reverse proxy and TLS path if outside clients truly need object access.

If MinIO is only serving containers in the same stack, do not publish ports publicly just because a tutorial somewhere did. Internal networking is cleaner and safer.

Backup and restore notes

MinIO is storage infrastructure, not a backup strategy by itself. You still need to protect the underlying data directory and any configuration or credential material required to rebuild access.

At a minimum, back up:

  • the MinIO data path
  • the Compose project files
  • the credential records or admin recovery procedure

Restore drills matter here. A backup that only copies raw files but never gets tested against a fresh container is still partly a guess.

Troubleshooting

The console loads but applications cannot connect.
Check that the app is using port 9000 for the S3 API, not the console on 9001.

Compose starts but nothing persists after restart.
Re-check the volume mapping and confirm you are writing to the mounted /data path.

The service is reachable but the credentials fail.
Inspect the env file, then recreate the container if the wrong bootstrap credentials were baked into the first run.

You want outside access immediately.
Slow down. Put TLS and access control in front of it first, or keep it private until the need is real.