Self-hosted Pluma — multi-tenant PDF template builder + renderer
10K+
Multi-tenant PDF template builder + renderer in one container. Design templates in a browser, fill them via a REST API, get back rendered PDFs. Stateless renderer, durable builder, no SaaS dependency.
This is the self-hosted image. It runs offline against a license JWT you paste into one env var.
services:
pluma:
image: mertdalbudak/pluma:latest
container_name: pluma
restart: unless-stopped
env_file: .env
ports:
- "4100:4100"
volumes:
- builder-data:/app/builder/data
# Lets the Pro+ Publish flow drive `docker buildx`. Remove this line
# if you won't publish workspace renderer images.
- /var/run/docker.sock:/var/run/docker.sock
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:4100/health"]
interval: 30s
timeout: 5s
retries: 5
volumes:
builder-data:
PUBLIC_URL=http://localhost:4100
# Paid tiers only — leave blank for Free
LICENSE_KEY=
Open http://localhost:4100. The first person to register becomes the account owner. After that, sign-up is invite-only.
Health check:
curl http://localhost:4100/health→{"ok":true,"service":"builder"}
| Env | Required | Notes |
|---|---|---|
PUBLIC_URL | yes | Where the install lives. Used in invite links, emails, OAuth callbacks. |
LICENSE_KEY | paid tiers only | The single-line JWT you got from us. Leave blank for Free. |
DATABASE_URL | optional | postgresql://USER:PASS@host:5432/pluma. Leave unset to use bundled SQLite. |
The session secret, internal-API secret, and three encryption keys are generated automatically on first boot and persisted to the volume. You don't set them.
The license is an EdDSA-signed JWT (RFC 7519). One single-line opaque string, no JSON wrapping, no files to mount.
Two ways to install it:
1. Env var (simplest)
LICENSE_KEY=eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJwbHVtYS1zZWxmaG9zdGVkIi…
Set, restart, done.
2. GUI — open Settings → License, paste, save. Rotates without a redeploy.
The signature is verified locally against a public key embedded in the image. The install works during network outages. Once a day the builder POSTs { sha256(license_jwt), pluma_version } to our license API so we can flag revoked or stolen keys; the raw key never leaves the box. A 14-day grace window means a server outage on our side never breaks an online install.
If you've contracted a fully air-gapped install, set PLUMA_LICENSE_HEARTBEAT=off.
The license never gates PDF generation. An expired support window stops auto-update checks and the support portal — it never stops rendering.
docker compose pull
docker compose up -d
Migrations run on boot. The image is backwards-compatible across minor versions.
The builder-data volume holds: SQLite DB (if used), uploaded PDFs/images, persisted internal secrets, license state. Snapshot it however you snapshot the rest of your stack.
If you use external Postgres + S3, the container itself is stateless except for those internal secrets — back the volume up anyway so a fresh install can read the same data.
| Symptom | Fix |
|---|---|
/api/runtime returns mode:"hosted" | You set PLUMA_DEPLOYMENT_MODE=hosted somewhere — remove it. The self-hosted image is self-hosted by default. |
License signature could not be verified | Wrong key, or copy-paste dropped a char. Re-copy from the portal; the key starts with eyJ and is one continuous line. |
Set LICENSE_SIGNING_KEY … on the server | That message is from the hosted issuer side, not this image. You're not supposed to see it on the self-hosted box. |
Renderer returns renderer_unreachable | Renderd crashed inside the container. Check docker compose logs pluma; restart with docker compose restart pluma. |
Full guide: https://plumapdf.com/docs/self-host — covers Postgres, S3, OAuth, backups, multi-host topology, all of it.
Source available, commercial. See LICENSE.md in the repo. For partner / OEM redistribution, talk to us.
Content type
Image
Digest
sha256:816189cb4…
Size
440.1 MB
Last updated
about 2 months ago
docker pull mertdalbudak/pluma