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:
# 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-cache3. Server-Side Encryption
Enable transparent server-side encryption with MinIO-managed keys:
# Enable auto-encryption on the bucket
mc encrypt set sse-s3 myminio/ci-cacheCredentials & 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:
{
"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:
mc admin policy create myminio ci-cache-policy ci-cache-policy.json2. Generate Service Account Credentials
# Create a service account restricted by the policy
mc admin user svcacct add --policy ci-cache-policy myminio ci-runnerMinIO 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 Name | Description |
|---|---|
MINIO_ACCESS_KEY | MinIO Service Account Access Key |
MINIO_SECRET_KEY | MinIO 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:
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 ciExample 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):
- 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:
docker compose -f docker-compose.test.yml up -d miniodocker-compose.test.yml (Click to view Compose definition)
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-stoppedOnce 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)
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"