Skip to content

Start typing to search the documentation.

to navigateto open

Administration

Self-hosting Pluraprint

Run Pluraprint on your own infrastructure with Docker Compose.

Pluraprint runs as a small set of container images that you can deploy on a single machine with Docker Compose. This page covers what those images are, how to bring a deployment up, and how to keep it running: upgrades, backups, and the handful of constraints worth knowing before you plan around them.

Each Pluraprint deployment serves one organization. There is no tenant switcher inside the product; separate organizations run as separate deployments.

What gets deployed

ImageWhat it doesExposed
pluraprint-webServes the dashboard and the station kiosk, and forwards API trafficThe only published port
pluraprint-apiThe application server, including the channel your agents connect throughInternal only
pluraprint-tasksBackground workers: dispatching jobs, thumbnails, snapshots, scheduled maintenanceNothing; outbound only
pluraprint-migrateApplies database migrations, then exitsNot a service
pluraprint-agentThe on-site daemon that talks to printersRuns at each site, not usually on the server

Alongside those you need PostgreSQL, Redis, and S3-compatible object storage. The example Compose file includes all three so a fresh deployment works with nothing else installed.

Images are published to the GitHub Container Registry at ghcr.io/lemony-click/pluraprint-<name>, built for both linux/amd64 and linux/arm64. Every release is tagged X.Y.Z, and moving tags X.Y, X, and latest follow it.

Requirements

  • A machine with 4 CPU cores, 8 GB RAM, and 100 GB of disk to start. Storage is the dimension that grows: design files, thumbnails, and printer snapshots accumulate.
  • Docker Engine 24 or newer with the Compose plugin.
  • Outbound internet access to pull images, unless you are installing air-gapped.
  • A TLS certificate and somewhere to terminate it; see Putting TLS in front.

Bringing a deployment up

cp .env.example .env
cp deploy/config.example.yaml config/config.yaml
docker compose up -d

Before the first up, open .env and fill in every value marked REQUIRED. The three secrets are generated the same way:

openssl rand -base64 32

Compose refuses to start if any of them is missing, rather than falling back to a default that would be identical on every deployment in the world.

Set PUBLIC_URL to the address people will actually type. Sign-in callbacks are signed against it, so a mismatch shows up as a login that loops back to the sign-in page.

Once the containers are healthy the dashboard is at the address in PUBLIC_URL and the station kiosk is at /station under it. Sign in with the SEED_ADMIN_EMAIL and SEED_ADMIN_PASSWORD you set, then change that password.

Configuration lives in two places

.env holds infrastructure: passwords, connection strings, the public URL. It is read when containers start.

config/config.yaml holds product configuration: branding, sign-in providers, station credential types, integrations. deploy/config.example.yaml is a documented starting point. It supports ${VARIABLE} interpolation, so a secret referenced there can still live in .env rather than being written into the file.

Both are read at startup. After editing either, restart the affected containers:

docker compose up -d --force-recreate api tasks

Putting TLS in front

The web container speaks plain HTTP on port 8080. That is deliberate: certificate management belongs with whatever already handles it on your network. Terminate TLS in a reverse proxy in front of it, and forward everything to that port.

TLS is not optional for anything beyond a local trial. Agents refuse to connect to a server over an insecure transport, and sign-in tokens travel over the same connection as everything else.

Keeping live updates working

Dashboards and stations stay current over a long-lived streaming connection (text/event-stream) rather than by polling. Your reverse proxy has to leave that connection alone: turn response buffering off for traffic to the web container, and allow a read timeout of at least an hour. The web container is already configured correctly for the connection between it and the API. This is only about whatever sits in front of it.

For nginx, that is:

proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_http_version 1.1;

A proxy that buffers the stream does not produce an error; it accepts the connection and then delivers nothing. Pluraprint detects that on its own and falls back to refreshing every 30 seconds, with a Degraded indicator in the sidebar and on each station screen, so the symptom is a visibly slower product rather than a silently stale one. If you see that indicator on a healthy deployment, this setting is the first thing to check.

Object storage and encryption at rest

Pluraprint will not start unless its object store reports that it encrypts objects at rest by default. The bundled MinIO is configured to satisfy this honestly: the setup step enables SSE-S3 on the bucket, so everything written to it afterwards is encrypted by the store.

To use AWS S3, Cloudflare R2, or an existing MinIO instead, point the S3_* variables in .env at it and remove the minio and minio-init services from docker-compose.yaml. Whatever you point at must have default encryption enabled: see Data Security for what that protects.

Local disk

The object store holds the permanent copy of every file. The API, the task workers, and each site agent additionally keep a temporary local copy of a file while they are working with it: parsing an upload, sending a job to a printer, because those operations need the file on disk rather than over the network.

These copies manage themselves. A file is held only while something is actually using it, released as soon as that work finishes, and then deleted once it has gone unused for an hour. Anything left behind by a crash or a restart is reclaimed on the next sweep. You do not need to clean these directories up by hand, and nothing on disk is the only copy of anything.

The defaults suit most deployments. On a host with little disk, or one handling unusually large files, these can be tuned in .env:

VariableApplies toDefaultWhat it does
FILE_CACHE_MAX_BYTESAPI, workers2 GiBTotal budget. Over it, the least recently used files go.
FILE_CACHE_IDLE_TTL_MSAPI, workers1 hourHow long an unused file is kept before deletion.
FILE_CACHE_MAX_AGE_MSAPI, workers24 hHard age limit, used or not.
FILE_CACHE_DIRAPI, workers${DATA_DIR}/cacheWhere copies are written.
AGENT_CACHE_MAX_BYTESAgent2 GiBSame budget, for a site agent.
AGENT_CACHE_IDLE_TTL_MSAgent1 hourSame idle limit, for a site agent.

Lowering the idle limit reclaims disk sooner at the cost of re-downloading a file if a reprint of the same job arrives shortly after. Raising it does the opposite.

To limit how long files are kept in the object store, see Data retention: a separate, opt-in policy.

Upgrading

Migrations only ever move forward; there are no down-migrations. Back up the database before you upgrade, every time.

# 1. Back up
docker compose exec -T postgres pg_dump -U pluraprint pluraprint | gzip > backup-$(date +%F).sql.gz

# 2. Change PLURAPRINT_VERSION in .env to the new version

# 3. Pull and restart. The migration container runs first and must finish
#    before the API and workers start.
docker compose pull
docker compose up -d

Rolling back the application is a matter of setting the previous version and running the same two commands. Rolling back the database means restoring your backup, which is the other reason to take one first.

Backing up

Three things need backing up, and a backup you have never restored is a hypothesis:

  1. The database: everything about jobs, users, printers, and policy.
  2. The object store: uploaded design files, thumbnails, snapshots. Use mc mirror against the MinIO volume, or your provider’s replication if you use one.
  3. .env and config/: in particular SECRETS_ENCRYPTION_KEY. Stored printer-vendor credentials are encrypted with it, and losing it makes them unrecoverable even with a perfect database backup.

Things to know before you plan around them

The API and the worker each run exactly one replica. The API keeps its agent connections in memory, and the worker clears the job queues when it starts. Running a second copy of either will misroute agent commands or interrupt work in progress. Scale up rather than out: give the containers more CPU and memory.

Agents run at the site, not on the server. An agent can only reach printers on its own network, so there is one per physical location. The Compose file includes an optional agent service for the case where the printers are on the same machine: see Connecting an Agent for the general setup.

Air-gapped installations

Sites with no internet access can run Pluraprint from images exported on a connected machine:

docker compose pull
docker save \
  ghcr.io/lemony-click/pluraprint-{web,api,tasks,migrate,agent}:X.Y.Z \
  postgres:16-alpine redis:7-alpine minio/minio minio/mc \
  | gzip > pluraprint-X.Y.Z-images.tar.gz

Transfer that file along with your docker-compose.yaml, .env, and config/, then docker load < pluraprint-X.Y.Z-images.tar.gz at the destination and bring the stack up normally. Upgrades follow the same path with a newer bundle.

Email notifications and any external identity provider will be unavailable without network access. Plan on local sign-in and in-app notifications for these deployments.