Skip to content

Install

Paprika comes as one image and two files: compose.yaml, which runs the services, and .env.example, their settings. You need a machine with Docker and Docker Compose, and the image loaded into Docker or reachable in your registry.

  1. Copy the settings and change every secret in them: the database password, PAPRIKA_ADMIN_TOKEN and PAPRIKA_SIGNING_KEYS. Set PAPRIKA_IMAGE to the image you were given.

    Terminal window
    cp .env.example .env

    A long random value is a good secret: openssl rand -base64 32 makes one.

  2. Start the services and wait until every one is healthy:

    Terminal window
    docker compose up --wait

    The migrate service brings the database up to date first; the API starts after it. The editor is then at http://localhost:4178 and the API at http://localhost:4179, both reachable from this machine only.

  3. Check the API is alive and ready. Ready means its storage is writable and PostgreSQL answers:

    Terminal window
    export PAPRIKA_URL=http://localhost:4179
    curl "$PAPRIKA_URL/health"
    { "status": "ok" }
    Terminal window
    curl "$PAPRIKA_URL/ready"
    { "ready": true, "checks": { "storage": "ok", "database": "ok" } }

    When it isn’t ready the answer is 503 with what failed, in checks.

Each organisation using Paprika is a tenant, with its own templates, fonts, outputs and keys. Make one with the admin token from .env:

Terminal window
export PAPRIKA_ADMIN_TOKEN=…
curl -H "Authorization: Bearer $PAPRIKA_ADMIN_TOKEN" -H "Content-Type: application/json" -d '{"id": "northwind", "name": "Northwind Traders"}' "$PAPRIKA_URL/v1/admin/tenants"
{
"id": "northwind",
"name": "Northwind Traders",
"tier": null,
"retentionDays": null,
"createdAt": "2026-11-10T15:49:12.073Z",
"suspendedAt": null,
"deletedAt": null
}

Then give it a key with every scope, for the person who will make the organisation’s other keys:

Terminal window
curl -H "Authorization: Bearer $PAPRIKA_ADMIN_TOKEN" -H "Content-Type: application/json" -d '{"name": "integration", "scopes": ["render", "publish", "keys"]}' "$PAPRIKA_URL/v1/admin/tenants/northwind/keys"
{
"key": "ppk_n0AAaSmCGft8_Jz6nXGhvrp9wmbfdskmFQI1hN5t8tqAUAsJA1tqF78k",
"id": "n0AAaSmCGft8",
"tenant": "northwind",
"name": "integration",
"scopes": ["render", "publish", "keys"],
"createdAt": "2026-11-10T15:49:12.117Z",
"expiresAt": null,
"revokedAt": null,
"lastUsedAt": null,
"rotatedTo": null,
"tier": null
}

The secret in key is shown only here; Paprika keeps a hash of it. Hand it over safely. With it, the organisation’s integrators can follow rendering from the API; a quick check that it works:

Terminal window
curl -H "Authorization: Bearer $PAPRIKA_KEY" "$PAPRIKA_URL/v1/templates"

Tenants and API keys covers the rest of the admin API.

The editor works for the built-in tenant local: the editor-key service issues its key. Designers using the editor publish into local, so give the integrators who render those templates keys of local too (/v1/admin/tenants/local/keys). One tenant per install is the usual self-hosted set-up.

The Compose file publishes the editor and the API on 127.0.0.1 only. To serve them to others, put a reverse proxy that terminates HTTPS in front of them, passing on the host name the browser asked for, and tell each service its names in .env: PAPRIKA_EDITOR_HOSTS for the editor, PAPRIKA_API_HOSTS for the API, comma-separated.

Terminal window
PAPRIKA_EDITOR_HOSTS=paprika.example.com
PAPRIKA_API_HOSTS=api.paprika.example.com

A name alone is answered as the proxy passes it on, https://paprika.example.com included; a name with a port (paprika.internal:8443) only on that port. A request for any other host is refused, and so is a browser’s change sent from another site, which keeps out DNS rebinding and cross-site requests.

Keep /v1/admin off the internet: route it only from where you administer (security).

The worker service renders jobs and batches; PAPRIKA_WORKERS sets how many it renders at once. For more, run more of them:

Terminal window
docker compose up --wait --scale worker=3

A worker that stops mid-job leaves the job to another once its lease runs out, so stopping and replacing workers loses nothing.

docker compose down stops the services and keeps the volumes: the database, the data and the editor’s key. docker compose down --volumes deletes them, and everything with them.