Skip to content

Storage backends

Plugwerk persists every uploaded plugin artifact via a pluggable storage backend. Two backends ship today:

  • fs — local filesystem under PLUGWERK_STORAGE_ROOT. Default. Single-node, no extra moving parts.
  • s3 — S3-compatible object storage. Enables horizontal scaling, cloud-managed hosting (no persistent volume), DR / geographic replication, and SaaS multi-tenant setups. Tested against AWS S3, MinIO, Cloudflare R2, and Hetzner Object Storage — every other S3-compatible target should work the same way.

The backend is selected via PLUGWERK_STORAGE_TYPE. Switching it on a running deployment requires a manual sync — see Migration between backends.

| Use case | Pick | | -------------------------------------------------------------- | ------ | | Single-node Docker Compose / single VM | fs | | Existing deployment, no horizontal-scaling requirement | fs | | Kubernetes / multi-replica deployment without a shared volume | s3 | | Cloud-managed (serverless container, no persistent disk) | s3 | | Cross-region disaster recovery / replication | s3 | | SaaS multi-tenant where one bucket per tenant or shared bucket | s3 |

The fs default needs no configuration beyond the standard Docker volume mount in Deployment — a fresh install starts there and stays there until the operator decides otherwise.

| Variable | Default | Description | | ----------------------- | ------------------------- | --------------------------------------- | | PLUGWERK_STORAGE_TYPE | fs | Storage backend selector | | PLUGWERK_STORAGE_ROOT | /var/plugwerk/artifacts | Directory for uploaded plugin artifacts |

The default directory is created and owned by the non-root plugwerk user in the official Docker image. Operators who override PLUGWERK_STORAGE_ROOT to a custom path are responsible for ensuring the target directory is writable by the container user.

Artifacts are stored as flat files named <namespace-uuid>:<plugin-id>:<version>:<extension>. The colons are deliberate — see Migration between backends for why this matters when copying to or from S3.

Setting PLUGWERK_STORAGE_TYPE=s3 switches Plugwerk to the S3 backend. The S3 client is the AWS SDK for Java v2; any endpoint that speaks the S3 API works.

| Variable | Required when | Default | Purpose | | ------------------------------------------------- | ----------------------- | ------- | ---------------------------------------------------------------------------------------------------- | | PLUGWERK_STORAGE_TYPE | always | fs | Set to s3 to switch backend | | PLUGWERK_STORAGE_S3_BUCKET | type=s3 | — | Must already exist — Plugwerk does NOT create buckets | | PLUGWERK_STORAGE_S3_REGION | type=s3 | — | AWS region (eu-central-1) or vendor-required placeholder (auto for R2, us-east-1 for MinIO) | | PLUGWERK_STORAGE_S3_ENDPOINT | non-AWS | empty | Override URL. Leave blank for AWS S3 | | PLUGWERK_STORAGE_S3_ACCESS_KEY | static credentials | empty | Set together with SECRET_KEY. Both blank → AWS DefaultCredentialsProvider chain | | PLUGWERK_STORAGE_S3_SECRET_KEY | static credentials | empty | See ACCESS_KEY | | PLUGWERK_STORAGE_S3_KEY_PREFIX | optional | empty | Prepended to every artifact key. For shared buckets across environments (prod/plugwerk/, staging/plugwerk/). Must not start with / | | PLUGWERK_STORAGE_S3_PATH_STYLE_ACCESS | MinIO | false | true for MinIO and other endpoints without virtual-hosted URLs. AWS S3 / R2 / Hetzner all work with the default | | PLUGWERK_STORAGE_S3_FAIL_FAST_ON_BUCKET_MISSING | production | false | When true, the server refuses to start if the startup HeadBucket probe fails. Default keeps the server running with an ERROR log line. Set true in container orchestrators that prefer fail-closed semantics |

The four endpoint variants Plugwerk treats as first-class are below. All three knobs that vary by provider — endpoint, region, path-style-access — are highlighted in each block.

Terminal window
PLUGWERK_STORAGE_TYPE=s3
PLUGWERK_STORAGE_S3_BUCKET=plugwerk-artifacts
PLUGWERK_STORAGE_S3_REGION=eu-central-1
# Leave creds + endpoint blank → DefaultCredentialsProvider picks them up

For the canonical defaults and inline operator notes shipped with the server, see the full .env.example reference on the Configuration page.

Plugwerk does not ship a built-in migration command. The key layout is identical between backends — every artifact is a flat file or object named <namespace-uuid>:<plugin-id>:<version>:<extension>, no nested directories — so a plain sync works in either direction.

Terminal window
# 1. Maintenance window — stop the server.
docker compose stop plugwerk-server
# 2. Sync the artifact directory to the bucket. Key layout is identical:
# every file becomes an object with the same name.
aws s3 sync /var/plugwerk/artifacts s3://plugwerk-artifacts/
# 3. Flip the env vars and start the server.
cat >> .env <<'EOF'
PLUGWERK_STORAGE_TYPE=s3
PLUGWERK_STORAGE_S3_BUCKET=plugwerk-artifacts
PLUGWERK_STORAGE_S3_REGION=eu-central-1
EOF
docker compose up -d plugwerk-server
# 4. Verify the server log:
# "S3 bucket 'plugwerk-artifacts' is reachable"
# 5. Smoke-test by downloading one pre-switch plugin via the Admin UI.

Symmetric:

Terminal window
docker compose stop plugwerk-server
aws s3 sync s3://plugwerk-artifacts/ /var/plugwerk/artifacts
# Flip PLUGWERK_STORAGE_TYPE=fs (the S3_* vars can stay — they are ignored
# when type=fs).
docker compose up -d plugwerk-server

Open Admin → Storage Consistency in the web UI and trigger a scan. The check reports two diffs: artifact files in storage that no longer have a matching plugin_release row (orphans, harmless), and DB rows whose artifact is missing from storage (broken — fix before declaring the migration done).

For a quick CLI sanity check without opening the UI:

Terminal window
# Row count
psql "$PLUGWERK_DB_URL" -c "SELECT COUNT(*) FROM plugin_release;"
# File count in target backend
find /var/plugwerk/artifacts -type f | wc -l # fs
aws s3 ls s3://plugwerk-artifacts/ --recursive | wc -l # s3

The numbers should match. Storage may legitimately carry slightly more entries (orphans from manual operations), but it must never carry fewer than the number of plugin releases.

  • Pre-signed URLs for direct client uploads — every upload still flows through the server.
  • Google Cloud Storage and Azure Blob Storage native APIs — both vendors offer S3-compatible facades that work, but their native SDKs are not wired in.
  • Multiple buckets per Plugwerk instance — one backend, one bucket. Use PLUGWERK_STORAGE_S3_KEY_PREFIX to share a single bucket across environments.

These are deliberate scope boundaries, not bugs.

  • Configuration — the full environment-variable reference
  • Deployment — Docker Compose, standalone container, JAR run options
  • Monitoring — health endpoint, Prometheus scrape configuration