Skip to content

MinIO S3 Setup

MinIO is a high-performance distributed object store with native S3 API compatibility, widely used for on-premise infrastructure, private enterprise clouds, and local CI testing.

NOTE

Maintenance & Compatibility Notice: While upstream open-source MinIO changed licensing (AGPLv3) and standalone community releases are no longer actively maintained with free public security patches, many development teams and enterprise clusters continue to rely on existing MinIO infrastructure or use ephemeral containers in CI. cloud-cache-action maintains 100% interoperability with all MinIO versions.


Best Practices: Bucket & Cluster Setup

Follow these recommendations when setting up MinIO for CI/CD caching:

1. Bucket Privacy & Quotas

  • Bucket Access Policy: Ensure the bucket access policy is strictly private (none). Never set download or public access policies on cache buckets.
  • Hard Storage Quotas: CI builds can rapidly generate hundreds of gigabytes of dependency archives. Use the MinIO Client (mc) to set a hard storage quota on your cache bucket:
    bash
    # Set a 100 GB hard limit on the cache bucket
    mc quota set --hard 100GB myminio/ci-cache

2. Information Lifecycle Management (ILM) Auto-Expiration

MinIO includes native ILM lifecycle management. Configure automatic expiration so old caches are purged automatically:

bash
# Automatically expire and delete cache archives older than 30 days
mc ilm rule add --expire-days 30 myminio/ci-cache

# Automatically abort incomplete multipart uploads after 7 days
mc ilm rule add --expire-delete-marker --abort-incomplete-multipart-upload-days 7 myminio/ci-cache

3. Server-Side Encryption

Enable transparent server-side encryption with MinIO-managed keys:

bash
# Enable auto-encryption on the bucket
mc encrypt set sse-s3 myminio/ci-cache

Credentials & Least-Privilege Service Accounts

WARNING

Never use root credentials (MINIO_ROOT_USER / MINIO_ROOT_PASSWORD) in your GitHub Actions workflows. Always create a dedicated Service Account restricted strictly to the CI cache bucket.

1. Create a Restricted Policy (ci-cache-policy.json)

Save the following minimal policy JSON:

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket"
      ],
      "Resource": "arn:aws:s3:::ci-cache"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:AbortMultipartUpload"
      ],
      "Resource": "arn:aws:s3:::ci-cache/*"
    }
  ]
}

Add the policy to MinIO:

bash
mc admin policy create myminio ci-cache-policy ci-cache-policy.json

2. Generate Service Account Credentials

bash
# Create a service account restricted by the policy
mc admin user svcacct add --policy ci-cache-policy myminio ci-runner

MinIO will print the Access Key and Secret Key.

3. Configure GitHub Secrets

Store the credentials in your repository's Settings > Secrets and variables > Actions:

Secret NameDescription
MINIO_ACCESS_KEYMinIO Service Account Access Key
MINIO_SECRET_KEYMinIO Service Account Secret Key

Example 1: GitHub Actions CI with Ephemeral MinIO Container

You can spin up an ephemeral MinIO instance directly inside your GitHub Actions runner job using Docker or Docker Compose for zero-cost, isolated CI caching:

yaml
name: Build with Local MinIO Cache

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

      # 1. Start ephemeral MinIO container
      - name: Start ephemeral MinIO
        run: |
          docker run -d --name ci-minio -p 9000:9000 \
            -e MINIO_ROOT_USER=minioadmin \
            -e MINIO_ROOT_PASSWORD=minioadmin \
            minio/minio:RELEASE.2025-09-07T16-13-09Z server /data
          
          # Wait for MinIO readiness
          for i in {1..30}; do
            if curl -s http://127.0.0.1:9000/minio/health/live > /dev/null 2>&1; then
              echo "MinIO ready."
              break
            fi
            sleep 1
          done
          
          # Create cache bucket
          docker exec ci-minio mkdir -p /data/ci-cache

      # 2. Cache dependencies with Cloud Cache Action
      - name: Cache dependencies using local MinIO
        uses: xSAVIKx/cloud-cache-action@v1
        with:
          bucket: ci-cache
          endpoint: http://127.0.0.1:9000
          access-key: minioadmin
          secret-key: minioadmin
          force-path-style: true
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-
          path: ~/.npm

      - name: Install dependencies
        run: npm ci

Example 2: Self-Hosted / On-Prem MinIO Cluster

For organizations hosting a persistent MinIO server or cluster on private infrastructure (e.g., bare-metal or Kubernetes):

yaml
- name: Cache dependencies using self-hosted MinIO
  uses: xSAVIKx/cloud-cache-action@v1
  with:
    bucket: ci-cache
    endpoint: https://minio.internal.mycompany.com:9000
    access-key: ${{ secrets.MINIO_ACCESS_KEY }}
    secret-key: ${{ secrets.MINIO_SECRET_KEY }}
    force-path-style: true
    key: ${{ runner.os }}-build-${{ hashFiles('**/lock') }}
    path: build/

TIP

Path-Style Addressing: Always set force-path-style: true for MinIO unless you have configured wildcard DNS subdomains (virtual-host style) for your buckets.


Running MinIO Locally for Integration Testing

You can spin up MinIO locally using the repository's docker-compose.test.yml:

bash
docker compose -f docker-compose.test.yml up -d minio
docker-compose.test.yml (Click to view Compose definition)
yaml
name: cloud-cache-test

services:
  # Garage S3 compatible storage (https://garagehq.deuxfleurs.fr)
  garage:
    image: dxflrs/garage:v2.4.1
    container_name: cloud-cache-garage
    ports:
      - '3900:3900' # S3 API
      - '3902:3902' # Admin API
    volumes:
      - ./tests/fixtures/garage.toml:/etc/garage.toml:ro
    environment:
      - RUST_LOG=info
    restart: unless-stopped

  # SeaweedFS S3 compatible storage (https://github.com/seaweedfs/seaweedfs)
  seaweedfs:
    image: chrislusf/seaweedfs:4.46
    container_name: cloud-cache-seaweedfs
    ports:
      - '8333:8333' # S3 API
      - '9333:9333' # Master
    command: 'server -s3 -s3.port=8333'
    restart: unless-stopped

  # MinIO S3 compatible storage (Alternative / Local testing)
  minio:
    image: minio/minio:RELEASE.2025-09-07T16-13-09Z
    container_name: cloud-cache-minio
    ports:
      - '9000:9000'
      - '9001:9001'
    environment:
      - MINIO_ROOT_USER=minioadmin
      - MINIO_ROOT_PASSWORD=minioadmin
    command: server /data --console-address ":9001"
    restart: unless-stopped

Once running:

  • S3 API: http://localhost:9000
  • Web Console: http://localhost:9001 (Default credentials: minioadmin / minioadmin)

Live CI Dogfooding Workflow

The repository dogfoods MinIO directly inside the continuous integration test suite: .github/workflows/test.yml.

.github/workflows/test.yml (Click to view test workflow)
yaml
name: CI Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    name: Unit, Contract & Integration Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

      - name: Setup Node.js
        uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
        with:
          node-version: 24

      - name: Start local test container (MinIO)
        run: |
          docker compose -f docker-compose.test.yml up -d minio
          for i in {1..30}; do
            if curl -s http://127.0.0.1:9000/minio/health/live > /dev/null 2>&1; then
              echo "MinIO is ready."
              break
            fi
            sleep 1
          done
          docker exec cloud-cache-minio mkdir -p /data/test-bucket
        continue-on-error: true

      - name: Ensure npm cache directory exists
        run: mkdir -p ~/.npm

      - name: Restore npm cache (local S3 + GitHub dual-cache)
        uses: ./
        with:
          path: ~/.npm
          key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-npm-
          bucket: test-bucket
          endpoint: http://127.0.0.1:9000
          access-key: minioadmin
          secret-key: minioadmin
          force-path-style: true
          dual-cache: true
          restore-priority: s3-first

      - name: Install dependencies
        run: npm ci

      - name: Code style and lint check
        run: |
          npm run format:check
          npm run lint

      - name: Type check
        run: npx tsc --noEmit

      - name: Run Jest test suite
        run: npm test

      - name: Build action bundles
        run: npm run build

      - name: Verify dist is clean and tracked
        run: |
          if [ -n "$(git status --porcelain dist)" ]; then
            echo "::error::Uncommitted dist changes detected. Run npm run build and commit."
            git status --porcelain dist
            exit 1
          fi

      - name: Build documentation
        run: npm run docs:build

  dogfood-verify:
    name: Dedicated Dogfood Verification
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

      - name: Setup Node.js
        uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
        with:
          node-version: 24

      - name: Start local test container (MinIO)
        run: |
          docker compose -f docker-compose.test.yml up -d minio
          for i in {1..30}; do
            if curl -s http://127.0.0.1:9000/minio/health/live > /dev/null 2>&1; then
              echo "MinIO is ready."
              break
            fi
            sleep 1
          done
          docker exec cloud-cache-minio mkdir -p /data/test-bucket

      - name: Generate synthetic test payload
        run: |
          mkdir -p /tmp/dogfood-cache-payload
          echo "Dogfood artifact $(date +%s%N) for run ${{ github.run_id }}" > /tmp/dogfood-cache-payload/artifact.txt
          sha256sum /tmp/dogfood-cache-payload/artifact.txt > /tmp/dogfood-expected-sha.txt
          cat /tmp/dogfood-expected-sha.txt

      - name: Save cache via ./ (save-only)
        uses: ./save
        with:
          path: /tmp/dogfood-cache-payload
          key: dogfood-e2e-${{ github.run_id }}-${{ github.run_attempt }}
          bucket: test-bucket
          endpoint: http://127.0.0.1:9000
          access-key: minioadmin
          secret-key: minioadmin
          force-path-style: true

      - name: Purge local test payload
        run: rm -rf /tmp/dogfood-cache-payload

      - name: Restore cache via ./ (restore-only)
        id: restore-step
        uses: ./restore
        with:
          path: /tmp/dogfood-cache-payload
          key: dogfood-e2e-${{ github.run_id }}-${{ github.run_attempt }}
          bucket: test-bucket
          endpoint: http://127.0.0.1:9000
          access-key: minioadmin
          secret-key: minioadmin
          force-path-style: true
          fail-on-cache-miss: true

      - name: Verify dogfood cache restoration & checksum
        run: |
          echo "Asserting cache hit..."
          if [ "${{ steps.restore-step.outputs.cache-hit }}" != "true" ]; then
            echo "::error::Expected cache-hit to be 'true', got '${{ steps.restore-step.outputs.cache-hit }}'"
            exit 1
          fi

          echo "Verifying file integrity..."
          if [ ! -f /tmp/dogfood-cache-payload/artifact.txt ]; then
            echo "::error::Restored file /tmp/dogfood-cache-payload/artifact.txt not found!"
            exit 1
          fi

          ACTUAL_SHA=$(sha256sum /tmp/dogfood-cache-payload/artifact.txt | awk '{print $1}')
          EXPECTED_SHA=$(awk '{print $1}' /tmp/dogfood-expected-sha.txt)
          if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
            echo "::error::Checksum mismatch! Expected $EXPECTED_SHA, got $ACTUAL_SHA"
            exit 1
          fi

          echo "Dogfood verification passed! Checksum: $ACTUAL_SHA"