From 9b3fc0ce81964f184ad138b717e3f1d175d83c9a Mon Sep 17 00:00:00 2001 From: Michael Bunsen Date: Tue, 29 Sep 2026 16:27:12 -0700 Subject: [PATCH 1/4] feat: create MINIO_DEFAULT_BUCKETS at startup and report health once they exist The entrypoint starts the server, waits until it is ready, then creates each listed bucket and applies its anonymous-access policy. The health check only passes once that setup has finished, so compose can wait on service_healthy instead of running a separate init container. Without the variable the server is exec'd directly, as before. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FA1nFdyB4WmWTw4syt89Yz --- Dockerfile | 7 ++- docker-entrypoint.sh | 117 +++++++++++++++++++++++++++++++++++++++++++ minio-healthcheck | 17 +++++++ 3 files changed, 140 insertions(+), 1 deletion(-) create mode 100755 docker-entrypoint.sh create mode 100755 minio-healthcheck diff --git a/Dockerfile b/Dockerfile index 688097f..edd0da3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -75,7 +75,12 @@ LABEL org.opencontainers.image.title="MinIO server and mc client (community buil org.insectai.mc.commit="${MC_COMMIT}" COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=build /out/ / +# The entrypoint creates MINIO_DEFAULT_BUCKETS at startup; the health check reports healthy only once the +# server is ready and those buckets exist. See README "Creating buckets at startup". +COPY --chmod=0755 docker-entrypoint.sh /usr/bin/docker-entrypoint.sh +COPY --chmod=0755 minio-healthcheck /usr/bin/minio-healthcheck EXPOSE 9000 9001 VOLUME ["/data"] -ENTRYPOINT ["/usr/bin/minio"] +HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=12 CMD ["/usr/bin/minio-healthcheck"] +ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"] CMD ["server", "/data", "--console-address", ":9001"] diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..2e6bbac --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,117 @@ +#!/bin/sh +# Container entrypoint: runs the MinIO server and, when MINIO_DEFAULT_BUCKETS is set, creates those +# buckets (and their anonymous-access policies) once the server is ready. This removes the need for a +# separate init container in compose stacks. +# +# MINIO_DEFAULT_BUCKETS="media:public,backups" # name[:policy], comma-separated +# +# The policy is one of mc's anonymous-access policies: none, download, upload, public. A bucket without a +# policy is created private. The variable name and format match the old Bitnami MinIO image. +# +# Anything other than `server ...` (for example `--version`) is passed straight to the minio binary. +# Without MINIO_DEFAULT_BUCKETS the server is exec'd directly, exactly as before this script existed. +# The API port is read from `--address` (default 9000) and shared with minio-healthcheck via a file. +set -eu + +MARKER=/tmp/minio-default-buckets.ready +PORT_FILE=/tmp/minio-api-port +READY_TIMEOUT="${MINIO_DEFAULT_BUCKETS_TIMEOUT:-120}" + +log() { echo "minio-default-buckets: $*"; } + +if [ "${1:-}" != "server" ]; then + exec /usr/bin/minio "$@" +fi + +# Find the API port from `--address HOST:PORT` or `--address=HOST:PORT`; the last occurrence wins. +address="" +prev="" +for arg in "$@"; do + case "$prev" in --address) address="$arg" ;; esac + case "$arg" in --address=*) address="${arg#--address=}" ;; esac + prev="$arg" +done +port=9000 +if [ -n "$address" ]; then + port="${address##*:}" +fi +case "$port" in + '' | *[!0-9]*) echo "docker-entrypoint.sh: cannot read a port from --address '$address'" >&2; exit 64 ;; +esac + +# A restarted container keeps /tmp, so clear the marker before the buckets are (re)checked. +rm -f "$MARKER" +echo "$port" > "$PORT_FILE" + +if [ -z "${MINIO_DEFAULT_BUCKETS:-}" ]; then + exec /usr/bin/minio "$@" +fi + +/usr/bin/minio "$@" & +server_pid=$! +trap 'kill -TERM "$server_pid" 2>/dev/null || true' TERM INT + +# Returns the server's exit status once it has exited, surviving waits interrupted by a trapped signal. +wait_for_server() { + status=0 + while :; do + wait "$server_pid" && status=0 || status=$? + kill -0 "$server_pid" 2>/dev/null || break + done + return "$status" +} + +fail() { + echo "minio-default-buckets: $*; stopping the server" >&2 + kill -TERM "$server_pid" 2>/dev/null || true + wait_for_server || true + exit 1 +} + +# A private mc config directory keeps credentials out of any ~/.mc the user may have mounted. +MC_CONFIG_DIR="$(mktemp -d /tmp/minio-entrypoint-mc.XXXXXX)" +export MC_CONFIG_DIR +endpoint="http://127.0.0.1:${port}" + +# `mc ready` retries forever, so bound each attempt and check the server is still running between them. +elapsed=0 +until MC_HOST_local="$endpoint" timeout 2 mc ready local >/dev/null 2>&1; do + if ! kill -0 "$server_pid" 2>/dev/null; then + wait_for_server && exit 0 || exit $? + fi + elapsed=$((elapsed + 3)) + [ "$elapsed" -lt "$READY_TIMEOUT" ] || fail "server not ready after ${READY_TIMEOUT}s" + sleep 1 +done + +mc alias set local "$endpoint" "${MINIO_ROOT_USER:-minioadmin}" "${MINIO_ROOT_PASSWORD:-minioadmin}" >/dev/null \ + || fail "could not authenticate to the server" + +old_ifs="$IFS" +IFS=, +for entry in $MINIO_DEFAULT_BUCKETS; do + IFS="$old_ifs" + entry="$(echo "$entry" | tr -d '[:space:]')" + [ -n "$entry" ] || continue + name="${entry%%:*}" + policy="" + case "$entry" in *:*) policy="${entry#*:}" ;; esac + case "$policy" in + '' | none | download | upload | public) ;; + *) fail "bucket '$name' has unknown policy '$policy' (use none, download, upload or public)" ;; + esac + mc mb --ignore-existing "local/$name" >/dev/null || fail "could not create bucket '$name'" + if [ -n "$policy" ]; then + mc anonymous set "$policy" "local/$name" >/dev/null || fail "could not set policy '$policy' on '$name'" + log "bucket '$name' ready (anonymous access: $policy)" + else + log "bucket '$name' ready" + fi +done +IFS="$old_ifs" + +rm -rf "$MC_CONFIG_DIR" +touch "$MARKER" +log "all buckets ready" + +wait_for_server && exit 0 || exit $? diff --git a/minio-healthcheck b/minio-healthcheck new file mode 100755 index 0000000..6cb7af1 --- /dev/null +++ b/minio-healthcheck @@ -0,0 +1,17 @@ +#!/bin/sh +# Health check for the image: succeeds only when the server answers its readiness probe and, if +# MINIO_DEFAULT_BUCKETS is set, the entrypoint has finished creating those buckets. Compose files can +# therefore use `condition: service_healthy` to mean "server up and buckets in place". +set -eu + +port="$(cat /tmp/minio-api-port 2>/dev/null || echo 9000)" + +# A throwaway config directory works whichever uid the container runs as. +export MC_CONFIG_DIR=/tmp/minio-healthcheck-mc + +# `mc ready` retries forever on its own, so bound it; the health check runs again on the next interval. +MC_HOST_local="http://127.0.0.1:${port}" timeout 3 mc ready local >/dev/null 2>&1 + +if [ -n "${MINIO_DEFAULT_BUCKETS:-}" ] && [ ! -f /tmp/minio-default-buckets.ready ]; then + exit 1 +fi From ba893b99f9ce98a0d79f72bfc62c362cc3584569 Mon Sep 17 00:00:00 2001 From: Michael Bunsen Date: Tue, 29 Sep 2026 16:27:12 -0700 Subject: [PATCH 2/4] test: add a smoke test for startup buckets, health and shutdown Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FA1nFdyB4WmWTw4syt89Yz --- test/smoke.sh | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100755 test/smoke.sh diff --git a/test/smoke.sh b/test/smoke.sh new file mode 100755 index 0000000..564ab85 --- /dev/null +++ b/test/smoke.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env sh +# Smoke test for a built image: startup bucket creation, anonymous access policies, the health check, +# clean shutdown, and unchanged behaviour when MINIO_DEFAULT_BUCKETS is not set. +# +# ./build.sh && test/smoke.sh insectai/minio:dev +# +# Needs only Docker. Every container and network it creates is removed on exit. +set -eu + +IMAGE="${1:?usage: test/smoke.sh IMAGE_REF}" +cd "$(dirname "$0")/.." +EXPECTED_TAG="$(sed -n 's/^MINIO_TAG=//p' versions.env)" + +run_id="minio-smoke-$$" +net="$run_id-net" +user=smokeadmin +password=smoke-secret-password + +cleanup() { + docker rm -f "$run_id-buckets" "$run_id-plain" "$run_id-badpolicy" >/dev/null 2>&1 || true + docker network rm "$net" >/dev/null 2>&1 || true +} +trap cleanup EXIT + +pass() { echo "PASS: $*"; } +fail() { echo "FAIL: $*" >&2; exit 1; } + +# Waits until the container's health status is `healthy`; fails on `unhealthy`, exit, or a 90s timeout. +wait_healthy() { + i=0 + while [ "$i" -lt 90 ]; do + state="$(docker inspect -f '{{.State.Status}} {{.State.Health.Status}}' "$1")" + case "$state" in + "running healthy") return 0 ;; + *unhealthy | exited* | dead*) docker logs "$1" >&2; fail "$1 is '$state'" ;; + esac + i=$((i + 1)) + sleep 1 + done + docker logs "$1" >&2 + fail "$1 not healthy after 90s (last state '$state')" +} + +# Runs a shell snippet in a throwaway client container on the test network. +client() { + docker run --rm --network "$net" --entrypoint sh \ + -e SMOKE_USER="$user" -e SMOKE_PASSWORD="$password" "$IMAGE" -c "$1" +} + +docker network create "$net" >/dev/null + +echo "== version" +version="$(docker run --rm "$IMAGE" --version)" +echo "$version" | head -1 +echo "$version" | grep -q "${EXPECTED_TAG}" || fail "minio --version does not report $EXPECTED_TAG" +pass "minio --version reports $EXPECTED_TAG through the entrypoint" +docker run --rm --entrypoint mc "$IMAGE" --version >/dev/null || fail "--entrypoint mc no longer works" +pass "--entrypoint mc still works" + +echo "== startup buckets" +srv="$run_id-buckets" +docker run -d --name "$srv" --network "$net" --network-alias minio \ + -e MINIO_ROOT_USER="$user" -e MINIO_ROOT_PASSWORD="$password" \ + -e MINIO_DEFAULT_BUCKETS="smoke-public:public, smoke-private" \ + "$IMAGE" >/dev/null +wait_healthy "$srv" +pass "container with MINIO_DEFAULT_BUCKETS became healthy" +docker logs "$srv" 2>&1 | grep "minio-default-buckets:" +docker logs "$srv" 2>&1 | grep -q "$password" && fail "the root password appears in the container log" +pass "the root password is not in the container log" + +# shellcheck disable=SC2016 # expanded inside the client container, not here +client ' +set -eu +mc alias set s http://minio:9000 "$SMOKE_USER" "$SMOKE_PASSWORD" >/dev/null +mc ls s +mc ls s | grep -q "smoke-public/" +mc ls s | grep -q "smoke-private/" +echo public-object > /tmp/o && mc cp -q /tmp/o s/smoke-public/o.txt >/dev/null && mc cp -q /tmp/o s/smoke-private/o.txt >/dev/null +mc anonymous get s/smoke-public | grep -q "is .public.$" +mc anonymous get s/smoke-private | grep -q "is .private.$" +' || fail "buckets missing or policies wrong" +pass "both buckets exist with the expected policies" + +# shellcheck disable=SC2016 # expanded inside the client container, not here +client ' +set -eu +test "$(wget -qO- http://minio:9000/smoke-public/o.txt)" = public-object +if wget -qO- http://minio:9000/smoke-private/o.txt >/dev/null 2>&1; then exit 1; fi +' || fail "anonymous access does not match the policies" +pass "anonymous GET works on smoke-public and is denied on smoke-private" + +echo "== restart keeps buckets and re-runs setup" +docker restart "$srv" >/dev/null +wait_healthy "$srv" +pass "container became healthy again after a restart" + +echo "== clean shutdown" +start="$(date +%s)" +docker stop "$srv" >/dev/null +elapsed=$(($(date +%s) - start)) +code="$(docker inspect -f '{{.State.ExitCode}}' "$srv")" +echo "docker stop took ${elapsed}s, exit code $code" +[ "$elapsed" -lt 10 ] || fail "docker stop needed the kill timeout (SIGTERM not forwarded)" +[ "$code" = 0 ] || fail "server exited with $code after SIGTERM" +pass "SIGTERM reaches the server and the container exits 0" + +echo "== bad policy fails fast" +bad="$run_id-badpolicy" +docker run -d --name "$bad" --network "$net" \ + -e MINIO_ROOT_USER="$user" -e MINIO_ROOT_PASSWORD="$password" \ + -e MINIO_DEFAULT_BUCKETS="smoke-bad:everyone" "$IMAGE" >/dev/null +timeout 60 docker wait "$bad" >/dev/null || fail "container with a bad policy kept running" +code="$(docker inspect -f '{{.State.ExitCode}}' "$bad")" +[ "$code" != 0 ] || fail "container with a bad policy exited 0" +docker logs "$bad" 2>&1 | grep "unknown policy" || fail "no error message for the bad policy" +pass "a bad policy stops the container with exit code $code" + +echo "== no MINIO_DEFAULT_BUCKETS (unchanged behaviour)" +plain="$run_id-plain" +docker run -d --name "$plain" --network "$net" \ + -e MINIO_ROOT_USER="$user" -e MINIO_ROOT_PASSWORD="$password" "$IMAGE" >/dev/null +wait_healthy "$plain" +pass "container without MINIO_DEFAULT_BUCKETS became healthy" +[ "$(docker exec "$plain" cat /proc/1/comm)" = minio ] || fail "PID 1 is not the minio server" +pass "the server is PID 1 (entrypoint exec'd it directly)" + +echo "All smoke tests passed for $IMAGE" From 3b0da186a6e2ec2cb692afc0717b7673a316ab1f Mon Sep 17 00:00:00 2001 From: Michael Bunsen Date: Tue, 29 Sep 2026 16:27:12 -0700 Subject: [PATCH 3/4] ci: run the smoke test on an amd64 build before the multi-arch build Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FA1nFdyB4WmWTw4syt89Yz --- .github/workflows/build.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7224cce..ab56eb6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,7 @@ name: Build and push image # Builds insectai/minio for linux/amd64 and linux/arm64 from the sources pinned in versions.env. +# Every run first builds linux/amd64 into the runner's Docker daemon and runs test/smoke.sh against it. # Pull requests only build (no push). Pushes to main and manual runs build and push. on: @@ -9,11 +10,17 @@ on: paths: - Dockerfile - versions.env + - docker-entrypoint.sh + - minio-healthcheck + - test/** - .github/workflows/build.yml pull_request: paths: - Dockerfile - versions.env + - docker-entrypoint.sh + - minio-healthcheck + - test/** - .github/workflows/build.yml workflow_dispatch: inputs: @@ -56,6 +63,31 @@ jobs: - uses: docker/setup-buildx-action@v3 + - name: Build for the smoke test (linux/amd64, loaded locally) + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64 + load: true + push: false + tags: insectai/minio:smoke + build-args: | + GO_IMAGE=${{ env.GO_IMAGE }} + RUNTIME_IMAGE=${{ env.RUNTIME_IMAGE }} + MINIO_REPO=${{ env.MINIO_REPO }} + MINIO_TAG=${{ env.MINIO_TAG }} + MINIO_COMMIT=${{ env.MINIO_COMMIT }} + MC_REPO=${{ env.MC_REPO }} + MC_TAG=${{ env.MC_TAG }} + MC_COMMIT=${{ env.MC_COMMIT }} + SOURCE_REVISION=${{ github.sha }} + provenance: false + sbom: false + cache-from: type=gha + + - name: Smoke test + run: test/smoke.sh insectai/minio:smoke + - name: Log in to Docker Hub if: steps.mode.outputs.push == 'true' uses: docker/login-action@v3 From a66037c783df0ed566ba86b1a7ccf2ee6f7422a5 Mon Sep 17 00:00:00 2001 From: Michael Bunsen Date: Tue, 29 Sep 2026 16:27:12 -0700 Subject: [PATCH 4/4] docs: document MINIO_DEFAULT_BUCKETS and the built-in health check Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FA1nFdyB4WmWTw4syt89Yz --- README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7291c7d..358b4f3 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,9 @@ The `insectai` organisation is a Docker-Sponsored Open Source namespace, so anon |---|---|---| | `/usr/bin/minio` | [pgsty/silo](https://github.com/pgsty/silo), the community-maintained fork of `minio/minio` | Built with the fork's own `gen-ldflags.go`, so `minio --version` reports the pinned release. | | `/usr/bin/mc` | [pgsty/mc](https://github.com/pgsty/mc), the matching fork of `minio/mc` | Same command set as upstream `mc` (`alias set`, `mb`, `anonymous set`, `ready`). | -| `/bin/sh` and BusyBox | Alpine base | Lets an init container run a shell script with `mc`. | +| `/usr/bin/docker-entrypoint.sh` | This repository | Starts the server and creates the buckets listed in `MINIO_DEFAULT_BUCKETS`. | +| `/usr/bin/minio-healthcheck` | This repository | The image's health check: healthy once the server is ready and those buckets exist. | +| `/bin/sh` and BusyBox | Alpine base | Runs the two scripts above, and lets you run your own shell scripts with `mc`. | | `/licenses/` | Copied from the source checkouts | AGPL-3.0 licence, NOTICE and CREDITS files. | The binaries keep their original names (`minio`, `mc`) so existing compose files and scripts work unchanged. The image runs as root by default, like the historical official image, and `/data` is world-writable so a non-root `user:` also works on a fresh volume. @@ -35,16 +37,49 @@ docker buildx imagetools inspect insectai/minio:RELEASE.2026-09-16T00-00-00Z services: minio: image: insectai/minio:RELEASE.2026-09-16T00-00-00Z@sha256: - command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: change-me-please + MINIO_DEFAULT_BUCKETS: media:public,backups healthcheck: - test: ["CMD", "mc", "ready", "local"] - minio-init: - image: insectai/minio:RELEASE.2026-09-16T00-00-00Z@sha256: - entrypoint: ["/bin/sh", "/etc/minio/init.sh"] + test: ["CMD", "minio-healthcheck"] + interval: 5s + retries: 12 + + app: + depends_on: + minio: + condition: service_healthy ``` +The default command is `server /data --console-address ":9001"`, so the `command:` line can be left out. The `healthcheck:` block above only repeats the image's built-in health check with a shorter interval; it can also be left out. + The image tag is the server release tag. The client version is recorded in the `org.insectai.mc.tag` label. +## Creating buckets at startup + +Set `MINIO_DEFAULT_BUCKETS` to a comma-separated list of buckets, each optionally followed by a colon and an anonymous-access policy: + +```sh +MINIO_DEFAULT_BUCKETS=media:public,uploads:upload,backups +``` + +The policy is one of the values `mc anonymous set` accepts: `none`, `download` (anonymous read), `upload` (anonymous write) or `public` (anonymous read and write). A bucket without a policy is created private. The variable name and format are the same as in the Bitnami MinIO image, so this setting carries over from compose files written for that image. Bitnami's other variables (such as its port settings) are not supported. + +When the variable is set, the entrypoint starts the server, waits until it is ready, creates each missing bucket, applies its policy, and logs one line per bucket. Existing buckets and their contents are left alone, and the policy is applied again on every start. If any step fails (an invalid bucket name, an unknown policy, wrong credentials), the entrypoint stops the server and the container exits with a non-zero status, so a misconfiguration is visible immediately instead of surfacing later as missing buckets. The server stays the container's main process: `docker stop` is forwarded to it and the container's exit code is the server's. + +When the variable is not set, the entrypoint hands straight over to the server, so the image behaves exactly as it did before this feature existed. Any command other than `server ...` (for example `--version`) is passed to the `minio` binary unchanged, and `--entrypoint mc` still runs the client. + +### Health check + +The image declares a Docker `HEALTHCHECK` that runs `/usr/bin/minio-healthcheck`. It reports healthy only when the server answers its readiness probe and, if `MINIO_DEFAULT_BUCKETS` is set, the entrypoint has finished setting up the buckets (it writes a marker file, `/tmp/minio-default-buckets.ready`, as its last step). In compose, `depends_on: {minio: {condition: service_healthy}}` therefore means "the server is up and the buckets are in place", which replaces a separate init container. + +### Limits + +- The scripts talk to the server over plain HTTP on `127.0.0.1`. The API port is taken from the server's `--address` argument (for example `--address :9100`) and defaults to 9000. TLS on the API port is not supported by the bucket setup. +- The root credentials come from `MINIO_ROOT_USER` and `MINIO_ROOT_PASSWORD`. Credentials supplied only through files or other mechanisms are not read. +- The entrypoint waits up to 120 seconds for the server to become ready before giving up. Set `MINIO_DEFAULT_BUCKETS_TIMEOUT` (in seconds) to change this. + ## Bumping versions 1. Find the new release tags on [pgsty/silo/releases](https://github.com/pgsty/silo/releases) and [pgsty/mc/releases](https://github.com/pgsty/mc/releases), and read their release notes for behaviour changes. @@ -60,10 +95,11 @@ The image tag is the server release tag. The client version is recorded in the ` ```sh ./build.sh - docker run --rm insectai/minio:dev --version - docker run --rm --entrypoint mc insectai/minio:dev --version + test/smoke.sh insectai/minio:dev ``` + The smoke test starts the image with and without `MINIO_DEFAULT_BUCKETS`, checks bucket creation, anonymous access, the health check, `--version` output and clean shutdown, and removes everything it created. The workflow runs the same script on every pull request. + 5. Open a pull request. The workflow builds both platforms without pushing. 6. Merge to `main`. The workflow pushes `insectai/minio:` and `insectai/minio:latest`, and prints the digest pin line in the run summary. 7. Update the digest pins in the consuming repositories (for Antenna: `docker-compose.yml` and `docker-compose.ci.yml`).