Skip to content

MinIO

dev.rightsize.modules.MinIOContainer — a single-node MinIO container, an S3-compatible object store. Defaults to a testuser/testpassword root credential pair.

Defaults

Default image minio/minio:latest — this image's floating reference (see below)
Exposed ports 9000 (S3 API — what the helpers use), 9001 (console, exposed but not wrapped by a helper)
Command server /data --console-address :9001
Env MINIO_ROOT_USER=testuser, MINIO_ROOT_PASSWORD=testpassword
Wait strategy Wait.forHttp("/minio/health/live").forPort(9000)

With no image given, this module tracks upstream's latest tag rather than a version this library pins, so the version moves with MinIO's own releases instead of this library's release cycle. The facts below were verified against minio/minio:RELEASE.2025-09-07T16-13-09Z specifically — pass that image explicitly to pin it:

MinIOContainer("minio/minio:RELEASE.2025-09-07T16-13-09Z")

Helpers

Member Returns
endpointUrl: String The S3 API's base URI — point any S3-compatible client at this with username/password
username / password: String The configured root credentials (default testuser/testpassword)
withUsername(username: String): MinIOContainer Overrides MINIO_ROOT_USER
withPassword(password: String): MinIOContainer Overrides MINIO_ROOT_PASSWORD — must be at least 8 characters

Call the withX overrides before start().

Example

package dev.rightsize.modules

import dev.rightsize.modules.MinIOContainer
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

class MinIOContainerTest {
    private val http = HttpClient.newHttpClient()

    @Test
    fun `bundled mc round-trips a bucket write and read`() {
        val minio = MinIOContainer()
        minio.start()
        try {
            val mcHost = "MC_HOST_local=http://${minio.username}:${minio.password}@127.0.0.1:9000"
            minio.execInContainer("sh", "-c", "$mcHost mc mb local/roundtrip")
            minio.execInContainer("sh", "-c", "printf 'hello minio' > /srv/hello.txt && $mcHost mc cp /srv/hello.txt local/roundtrip/hello.txt")
            val cat = minio.execInContainer("sh", "-c", "$mcHost mc cat local/roundtrip/hello.txt")
            assertEquals(0, cat.exitCode)
            assertEquals("hello minio", cat.stdout.trim())

            val live = http.send(
                HttpRequest.newBuilder(URI("${minio.endpointUrl}/minio/health/live")).GET().build(),
                HttpResponse.BodyHandlers.discarding(),
            )
            assertEquals(200, live.statusCode())
        } finally {
            minio.stop()
        }
    }
}

Backend notes

The default entrypoint does not serve — a command is required. Booting this image with no command produces no listening S3 API at all; server /data --console-address :9001 is required to make it actually serve. This module sets that command unconditionally.

Credentials default to testuser/testpassword, not the house test/test pair used by ClickHouse and friends. MinIO rejects a root password shorter than 8 characters at startup, so the two-character test/test pair can't be reused here. testuser/testpassword is the shortest pair that both clears that floor and stays obviously a test credential.

Readiness is protocol-aware and answers correctly on the first poll. Wait.forHttp("/minio/health/live").forPort(9000) returned 200 on the first poll after boot in verification — no restart/double-boot race to work around, unlike the Postgres/MySQL/MariaDB entrypoints.

Round-trip and auth were verified with the bundled mc client — no S3 SDK in this repo. mc mb (make bucket) followed by mc cp (write) and mc cat (read back) against the configured credentials returned the exact bytes written, all run through exec on the started container using the mc binary the image already bundles. mc cp uploads a file written into the guest first rather than piping bytes into mc pipe over stdin: an exec'd mc pipe under this backend either dumps its goroutines and exits non-zero or hangs outright, both observed directly, while mc cp needs no stdin and round-trips reliably. Separately, an anonymous GET / against the S3 API returned AccessDenied rather than serving, confirming auth is actually enforced rather than merely configured.

Memory: a verification spike ran this image at 1024 MB with no issues. Whether any floor is needed at all under this backend's default allocation is not yet established, so this module sets no withMemoryLimit override; callers who hit memory pressure can call it themselves.

Compatibility checking

Passing an explicit image checks its repository against the one this module understands (minio/minio) before any port, wait-strategy, or backend work runs — a mismatched image fails fast with a typed IncompatibleImageException naming both repositories, rather than degrading into a bare wait-strategy timeout. To use a differently-named image on purpose (a private mirror, a hardened rebuild), wrap it with the escape hatch:

MinIOContainer(
    DockerImageName.parse("mycorp/minio-hardened:RELEASE.2025-09-07T16-13-09Z")
        .asCompatibleSubstituteFor("minio/minio"))

See Core Concepts for DockerImageName itself.