Skip to content

Deployment

Plugwerk Server is published as a multi-architecture container image and as a standalone JAR. The container path — run via Docker Compose or directly with docker run — is the recommended one for production. The JAR is supported for environments without Docker.

The Plugwerk Server image is published on Docker Hub at plugwerk/plugwerk-server for linux/amd64 and linux/arm64. Every v* Git tag publishes a matching image tag.

Terminal window
# Pin a specific version (recommended for production)
docker pull plugwerk/plugwerk-server:1.1.0
# Latest stable release
docker pull plugwerk/plugwerk-server:latest
# Pre-release (e.g. alpha, beta) — never aliased to :latest
docker pull plugwerk/plugwerk-server:1.0.0-beta.3
Tag When published Example
{{version}} Every release (stable & pre-release) 1.1.0, 1.0.0-beta.3
{{major}}.{{minor}} Every release 1.1
latest Stable releases only (semver-aware) latest

Pre-releases (alpha, beta, rc) never update :latest. For reproducible deployments always pin a full {{version}} tag.

The image is multi-arch — docker pull resolves the correct manifest for linux/amd64 and linux/arm64 automatically. The bundled JVM means no host JVM is required.

The image needs a PostgreSQL database and a small set of environment variables. Pick one of the run options below.

Aspect Docker Compose Standalone Docker JAR Execution
Setup effort Minimal Low Medium
Third-party services Included (PostgreSQL) External External
Docker required Yes Yes No
JVM required No (bundled in image) No (bundled in image) Yes (JVM 21+)

All three options default to local-filesystem artifact storage under a Docker volume / mounted path. For multi-replica or cloud-managed deployments where a persistent volume is impractical, switch to S3-compatible object storage — see Storage backends.

Bundles Plugwerk Server with PostgreSQL into a single stack.

The compose snippet below uses ${PLUGWERK_VERSION:-latest}. Export an explicit version for reproducible deployments:

Terminal window
export PLUGWERK_VERSION=1.1.0
Terminal window
export PLUGWERK_AUTH_JWT_SECRET="$(openssl rand -base64 32)"
export PLUGWERK_AUTH_ENCRYPTION_KEY="$(openssl rand -base64 32)"

This file is auto-synced from the canonical compose file that ships with each Plugwerk release. Download it directly: /deploy/docker-compose.yml.

docker-compose.yml
# ---------------------------------------------------------------------------
# Plugwerk server — minimal local/dev compose template.
#
# This file is bundled in the server distribution as a starting point. It is
# NOT production-ready as-is:
#
# - The Postgres credentials (POSTGRES_PASSWORD, PLUGWERK_DB_PASSWORD) are
# placeholder values. Replace them with strong secrets before any non-
# local-dev use.
# - PLUGWERK_AUTH_JWT_SECRET and PLUGWERK_AUTH_ENCRYPTION_KEY are passed through from
# the host environment (`${VAR:?…}`) and fail fast if missing.
# - The admin bootstrap password is auto-generated on first start and
# printed to `docker compose logs plugwerk-server`; `passwordChangeRequired`
# is set, so the operator is forced to change it on first login. To pin
# a fixed admin password for CI/smoke-test environments, export
# `PLUGWERK_AUTH_ADMIN_PASSWORD` in your own shell or `.env` before
# running `docker compose up` — never commit that value.
#
# See the Docker Hub README for a guided setup (strong secret generation,
# login instructions, operational caveats).
# ---------------------------------------------------------------------------
services:
postgres:
image: postgres:18-alpine
environment:
POSTGRES_DB: plugwerk
POSTGRES_USER: plugwerk
POSTGRES_PASSWORD: plugwerk
ports:
- "127.0.0.1:5432:5432"
volumes:
# postgres:18+ requires the volume at /var/lib/postgresql (not /var/lib/postgresql/data)
# so PGDATA can live in a major-version-specific subdirectory alongside config.
# See https://github.com/docker-library/postgres/pull/1259.
- postgres-data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U plugwerk"]
interval: 5s
timeout: 5s
retries: 5
plugwerk-server:
image: plugwerk/plugwerk-server:${PLUGWERK_VERSION:-latest}
depends_on:
postgres:
condition: service_healthy
environment:
PLUGWERK_DB_URL: jdbc:postgresql://postgres:5432/plugwerk
PLUGWERK_DB_USERNAME: plugwerk
PLUGWERK_DB_PASSWORD: plugwerk
PLUGWERK_AUTH_JWT_SECRET: "${PLUGWERK_AUTH_JWT_SECRET:?Set PLUGWERK_AUTH_JWT_SECRET (min 32 chars)}"
PLUGWERK_AUTH_ENCRYPTION_KEY: "${PLUGWERK_AUTH_ENCRYPTION_KEY:?Set PLUGWERK_AUTH_ENCRYPTION_KEY (min 16 chars, 32+ recommended)}"
# PLUGWERK_AUTH_ADMIN_PASSWORD is intentionally NOT declared here.
# If unset, the server generates a random admin password on first start
# and forces a change on first login. Export it only for CI / smoke-test
# environments where a deterministic password is required; the process
# environment is picked up by Spring Boot without needing a compose entry.
volumes:
- plugwerk-artifacts:/var/plugwerk/artifacts
ports:
- "8080:8080"
volumes:
postgres-data:
plugwerk-artifacts:
Terminal window
docker compose up -d
curl http://localhost:8080/actuator/health
Terminal window
docker compose down # stop, keep data
docker compose down -v # stop and delete database volume

The published image reads the standard JAVA_OPTS environment variable. Override it from the host shell or from docker-compose.override.yml:

docker-compose.override.yml
services:
plugwerk-server:
environment:
JAVA_OPTS: "-Xms512m -Xmx2g"

Run Plugwerk Server as a single container against your own PostgreSQL, S3-compatible storage, or identity provider.

Terminal window
docker run -d \
--name plugwerk-server \
-p 8080:8080 \
-e PLUGWERK_DB_URL=jdbc:postgresql://your-db-host:5432/plugwerk \
-e PLUGWERK_DB_USERNAME=plugwerk \
-e PLUGWERK_DB_PASSWORD=changeme \
-e PLUGWERK_AUTH_JWT_SECRET="$(openssl rand -base64 32)" \
-e PLUGWERK_AUTH_ENCRYPTION_KEY="$(openssl rand -base64 32)" \
-v plugwerk-artifacts:/var/plugwerk/artifacts \
plugwerk/plugwerk-server:${PLUGWERK_VERSION:-latest}

Pass JAVA_OPTS the same way as in Compose:

Terminal window
docker run -e JAVA_OPTS="-Xms512m -Xmx2g" plugwerk/plugwerk-server:latest

Run Plugwerk Server directly on a JVM (21+) using the provided start script. Download the distribution ZIP from GitHub Releases, extract it, and start:

Terminal window
unzip plugwerk-server-*.zip
cd plugwerk-server-*/
chmod +x start.sh
./start.sh

This requires an external PostgreSQL database and optionally external S3 storage.

Customize the JVM with JAVA_OPTS:

Terminal window
export JAVA_OPTS="-Xms512m -Xmx1g -XX:+UseZGC"
./start.sh

Plugwerk Server’s per-IP rate limiting (login brute-force protection on /api/v1/auth/login and /api/v1/auth/refresh) keys off the client IP that the server observes. When a reverse proxy sits in front of Plugwerk, every request appears to come from the proxy’s IP — every client lands in the same rate-limit bucket and the protection collapses.

To restore correct per-client buckets, the server reads the original client IP from the leftmost X-Forwarded-For value — but only when the request actually came from a proxy you have explicitly trusted. Trust is configured via:

Variable Default Description
PLUGWERK_AUTH_TRUSTED_PROXY_CIDRS (empty) Comma-separated CIDR ranges. X-Forwarded-For is honoured only when the connecting peer’s IP falls inside one of these.

With the variable left empty, X-Forwarded-For is ignored entirely and Plugwerk uses the connection’s remoteAddr. This is the correct default for a server reachable directly by clients (no proxy in front), and prevents a forged header from rotating fake IPs to bypass rate limiting.

If you run with no proxy, do nothing — the default is safe.

Set PLUGWERK_AUTH_TRUSTED_PROXY_CIDRS to the egress IP range(s) of the proxy that fronts Plugwerk. Each entry must be valid CIDR syntax; both IPv4 and IPv6 are supported. The list is validated at startup — an invalid entry fails fast with a clear error.

Terminal window
# Single nginx on the same host as the Plugwerk container
export PLUGWERK_AUTH_TRUSTED_PROXY_CIDRS=127.0.0.1/32
# Internal LAN (Kubernetes pod CIDR, Docker bridges, internal load balancers)
export PLUGWERK_AUTH_TRUSTED_PROXY_CIDRS=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
# AWS ALB egress range plus a backup proxy
export PLUGWERK_AUTH_TRUSTED_PROXY_CIDRS=10.0.0.0/16,52.95.245.0/24

Identifying the egress IP for common stacks

Section titled “Identifying the egress IP for common stacks”
Stack Suggested CIDR Notes
nginx on the same host 127.0.0.1/32 Plugwerk in Docker with nginx on the host: use the Docker bridge gateway (typically 172.17.0.1/32) instead.
Kubernetes (nginx / Traefik) Pod CIDR (e.g. 10.244.0.0/16) Look up your cluster’s pod CIDR with kubectl cluster-info dump | grep -m1 cluster-cidr.
AWS ALB The VPC CIDR (e.g. 10.0.0.0/16) ALB egress IPs are ENIs inside the VPC. Pinning the VPC CIDR is simpler and equivalent in practice.
Cloudflare in front of nginx The local proxy’s CIDR, not Cloudflare’s Plugwerk only sees the immediate hop. Configure set_real_ip_from on nginx for Cloudflare ranges, then trust nginx’s IP from Plugwerk.
/etc/nginx/sites-available/plugwerk.conf
server {
listen 443 ssl http2;
server_name plugwerk.example.com;
# ... TLS config omitted ...
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
# Forward the original client IP. Plugwerk reads the leftmost value.
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
}
}

Then on the Plugwerk side:

Terminal window
export PLUGWERK_AUTH_TRUSTED_PROXY_CIDRS=127.0.0.1/32

The connection from nginx to Plugwerk arrives on 127.0.0.1, matches the trusted CIDR, and Plugwerk reads the real client IP from X-Forwarded-For. Per-IP rate limiting now applies per real client.

To verify, hit the server from two different external IPs and confirm that each is throttled independently — the rate limit (default: 10 attempts / 15 min on /auth/login) should not be shared between them.

Plugwerk parses every entry in PLUGWERK_AUTH_TRUSTED_PROXY_CIDRS through Spring Security’s IpAddressMatcher at startup. A bare IP without a prefix is accepted and treated as /32 (IPv4) or /128 (IPv6); blank entries and unparseable values abort the boot with an indexed message identifying the bad entry, for example:

plugwerk.auth.trusted-proxy-cidrs[2] is not a valid CIDR range ('10.0.0.300/24'): ...

See Configuration for the full environment variable reference.