Skip to content

Install and upgrade

Follow these steps to run Typefold on a Linux server with Docker Compose. At the end, Studio and the API answer on one HTTPS address and you are signed in as the administrator.

  • A Linux server with an x86-64 (amd64) CPU. The images are built for linux/amd64 only.
  • Docker Engine with the Compose v2 plugin, so that docker compose works. See Docker’s install guide.
  • openssl and curl on the server.
  • Memory and CPU. About 6.8 GiB of memory with one worker, plus 2 GiB for each extra worker (see memory limits). One CPU for each worker, plus some for the other services.
  • A DNS name for the server. The bundled proxy gets its certificate from Let’s Encrypt, which must reach the server on ports 80 and 443. If the internet cannot reach the server, read on a private network first.
  • A license key. See get a key.

Each release comes with two files: compose.yaml (the whole installation, which you do not edit) and env.example (every setting, with comments). They and the images are in Typefold’s image registry, which you sign in to with your license key.

Sign in, paste your license key when asked for the password, then copy both files into a new directory on the server, such as /opt/typefold, and copy env.example to .env:

Terminal window
docker login registry.typefold.dev -u license
mkdir -p /opt/typefold && cd /opt/typefold
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/out" registry.typefold.dev/typefold-release:latest
cp env.example .env
chmod 600 .env

latest is the newest release; for another one, use its version, such as typefold-release:v1.4.0. Docker keeps the sign-in, so docker compose pulls the images with it. If your license ends or is revoked, the registry refuses it.

This is the installation directory. .env holds your settings and every secret, and every docker compose command in these docs runs from here.

Run each command once and keep its output for the next step:

Terminal window
openssl rand -hex 32 # for POSTGRES_PASSWORD
openssl rand -hex 32 # for BETTER_AUTH_SECRET
openssl rand -base64 32 # for DATA_ENCRYPTION_KEY

Open .env and set the address, the three secrets and your license key:

Terminal window
PUBLIC_URL=https://pdf.example.com
POSTGRES_PASSWORD=<first hex value>
BETTER_AUTH_SECRET=<second hex value>
DATA_ENCRYPTION_KEY=<base64 value>
LICENSE_KEY=<your trial or purchased key>

A first install needs nothing else. Required settings explains each value.

Create a DNS record for the host name in PUBLIC_URL that points at the server. Open TCP ports 80 and 443, and UDP port 443. The bundled proxy gets and renews the certificate.

Terminal window
docker compose up -d
docker compose ps
curl https://pdf.example.com/health

docker compose ps lists the services as running, and curl prints:

{ "status": "ok" }

If not, see if it does not start.

6. Become the first platform administrator

Section titled “6. Become the first platform administrator”

A new installation has no administrator. You claim the role with a one-time setup token that the API prints in its log.

  1. Create a password account for yourself. The command prints a generated password.

    Terminal window
    docker compose exec api node dist/password-account.js you@example.com
  2. Read the setup token:

    Terminal window
    docker compose logs api | grep setupToken
  3. Open PUBLIC_URL in a browser and sign in with your email address and that password.

  4. Studio opens Set up your installation. Paste the token into Setup token and choose Complete setup. You are now a platform administrator.

  5. Studio opens Set up your workspace. Enter a Workspace name and choose Create workspace.

If the token does not work, see if the setup token does not work.

  • /health answers 503. Its unavailable list names what the API cannot reach, database or redis. Run docker compose logs api to see why.
  • docker compose stops with Set <setting> in .env. That required setting is empty in .env.
  • The API exits with Unsafe production settings. It refuses an http:// PUBLIC_URL (except a localhost address) and well-known example secrets. The log names the setting to fix; it calls PUBLIC_URL BETTER_AUTH_URL.
  • A setting is invalid. The API and worker stop with a log line such as Invalid environment variables: OBJECT_STORAGE_BUCKET. See how settings work.
  • A service stays created or starting. Each service waits until the ones it needs are healthy. Read the logs of the first service that is not healthy.

An installation only your own network can reach still needs an https:// PUBLIC_URL, because sign-in cookies only work over HTTPS. What you set depends on the address:

Browsers and API clients reject the proxy’s certificate until you install its root certificate. After the first start, copy it out of the proxy container:

Terminal window
docker compose cp proxy:/data/caddy/pki/authorities/local/root.crt typefold-root.crt

Install typefold-root.crt as a trusted root on every computer and in every API client that uses the installation. It is valid for ten years. It lives in the caddy-data volume: if you lose that volume, the proxy makes a new root and every client must trust it again.

  1. Set CERTIFICATE_SOURCE=files in .env.

  2. Put the certificate chain and its unencrypted private key, both PEM, in a certificates directory next to compose.yaml (CERTIFICATE_PATH changes the directory):

    /opt/typefold/certificates/certificate.pem
    /opt/typefold/certificates/key.pem
  3. Run docker compose up -d. When you replace the files later, run docker compose restart proxy.

The API rate-limits each client address, including sign-in attempts. Behind a CDN or load balancer, every request arrives from its address, so all clients share one limit. To fix it, tell the proxy which addresses belong to the CDN or load balancer:

  1. In .env, set TRUSTED_PROXY_RANGES to its address ranges, separated by spaces:

    Terminal window
    # A CDN: list every range it publishes.
    TRUSTED_PROXY_RANGES=203.0.113.0/24 198.51.100.0/24 2001:db8::/32
    # A load balancer on the server's private network:
    TRUSTED_PROXY_RANGES=private_ranges
  2. Run docker compose up -d proxy.

For requests from those ranges, the proxy takes the client’s address from the X-Forwarded-For header.

Rendered PDFs and uploaded files (images, fonts and other workspace files) live in file storage. The default keeps them in the storage-data Docker volume on the server and needs no setup. To keep them in a cloud service instead, set it up before you go live:

STORAGE_DRIVER Files live in
local (default) The storage-data volume on the server
s3 Amazon S3 or another S3-compatible service, like MinIO
azure Azure Blob Storage
  1. Create a bucket, and an access key that can read, write, list and delete objects in it.

  2. Add to .env:

    Terminal window
    STORAGE_DRIVER=s3
    OBJECT_STORAGE_ENDPOINT=https://s3.eu-central-1.amazonaws.com
    OBJECT_STORAGE_REGION=eu-central-1
    OBJECT_STORAGE_BUCKET=my-typefold-bucket
    OBJECT_STORAGE_ACCESS_KEY_ID=...
    OBJECT_STORAGE_SECRET_ACCESS_KEY=...
    OBJECT_STORAGE_FORCE_PATH_STYLE=false # false for Amazon S3; leave it out for MinIO
  3. Set up CORS, then run docker compose up -d.

File storage settings lists the rest, such as a separate address for browsers.

  1. Create a storage account and a container.

  2. Add to .env, with the connection string from the storage account’s Access keys page (it must include the account key):

    Terminal window
    STORAGE_DRIVER=azure
    AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net
    OBJECT_STORAGE_BUCKET=pdf-results # the container's name
  3. Set up CORS, then run docker compose up -d.

Studio loads files straight from the bucket or container. In its CORS rules, allow requests from your PUBLIC_URL origin: GET for Azure, GET and PUT for S3, because Studio uploads to S3 directly.

Optionally, limit which file addresses Studio may load from with STUDIO_FILE_ORIGINS in .env. A service on http://, such as MinIO on your network, must be listed there:

Terminal window
STUDIO_FILE_ORIGINS=https://my-bucket.s3.eu-central-1.amazonaws.com

If the browser console shows Refused to ... messages about the Content-Security-Policy, an origin is missing from STUDIO_FILE_ORIGINS.

Each worker renders one document at a time, so the number of workers is the number of documents rendered in parallel. Run about one worker per CPU you want to spend on rendering.

  1. Create compose.override.yaml next to compose.yaml:

    services:
    worker:
    deploy:
    replicas: 3
  2. Run docker compose up -d.

Compose reads compose.override.yaml automatically, so the count survives upgrades. Do not use docker compose up -d --scale worker=3: the next docker compose up -d goes back to one worker.

Workspaces share the workers fairly; see how workspaces share workers.

Every service has a memory limit, so one runaway process cannot take the server down. With one worker the limits add up to about 6.8 GiB, each extra worker adds 2 GiB, and the optional Prometheus adds 1 GiB. They are ceilings, so an idle installation uses much less.

A container that reaches its limit is killed and restarted, and docker inspect <container> shows "OOMKilled": true. Raise its *_MEMORY_LIMIT setting in .env; the memory settings list each one with its default. The ones most likely to need it:

  • Worker (WORKER_MEMORY_LIMIT): large documents. If you raise the render memory caps, raise this with them.
  • API (API_MEMORY_LIMIT): importing large exported folders.
  • Redis (REDIS_MEMORY_LIMIT): holds every render waiting to run and never drops one, so size it for the largest backlog you expect.

Run these in the installation directory:

  1. Back up the database and file storage.

  2. Download the new release’s files as in step 1, which replaces compose.yaml and env.example. Keep .env and any compose.override.yaml.

  3. Apply it:

    Terminal window
    docker compose up -d --remove-orphans
  4. Check the release notes for one-off steps.

Compose runs the database migrations, replaces the worker, and starts the new API once the new worker is healthy. The API is unavailable for a few seconds. --remove-orphans removes services the new release dropped. Templates, files, API keys, users and renders stay where they are, so there is nothing to export or import.

  • Releases with a backup service kept backups in the pdf-renderer_backups volume, or in the directory named by BACKUP_PATH. Nothing deletes them any more: copy any you still need, then run docker volume rm pdf-renderer_backups.

  • Releases before imported and spreadsheet reports became templates set those reports aside, and they are unavailable until you merge them. Run once after the upgrade:

    Terminal window
    docker compose run --rm --no-deps api node dist/merge-reports.js

    Each report becomes a template with the same ID, name and folder, so API calls keep working. Its saved revisions become drafts and its published revision becomes version 1. Running it again is safe. If it exits with code 1, fix the report it names and run it again.

On an upgrade or docker compose stop:

  • The API gives open requests 30 seconds to finish. A request still waiting for its PDF fails; retry it with the same idempotency key.
  • The worker finishes its current render first, for up to WORKER_CLOSE_GRACE_MS (75 seconds). Compose kills it after WORKER_STOP_GRACE_PERIOD (90 seconds). A render cut off this way starts over automatically.

If you raise RENDER_TIMEOUT_MS, raise both grace periods with it, each at least 15 seconds above the one before:

Terminal window
RENDER_TIMEOUT_MS=120000
WORKER_CLOSE_GRACE_MS=135000
WORKER_STOP_GRACE_PERIOD=150s

compose.yaml runs these services. Their names are the ones docker compose ps and docker compose logs use.

Service What it does
proxy Caddy, the only public entry point. Serves Studio and the API on one address, over HTTPS.
api The API, sign-in and file downloads. It also sends webhooks.
worker Renders documents, one at a time per worker.
sandbox Runs template scripts and helpers, with no secrets and no network access.
studio The web app.
postgres PostgreSQL 18. Accounts, workspaces, templates, render history and the license state.
redis Redis 8. Renders waiting to run, and rate-limit counters.
migrate Runs the database migrations on every start, then exits.
prometheus Optional. Collects metrics and evaluates alert rules; see monitoring.

Back up these, and restore them together:

What Holds
The PostgreSQL database (postgres-data volume) Accounts, workspaces, templates and their versions, render history, API keys, webhooks, signing certificates and the license state.
File storage: the storage-data volume, or your S3 or Azure bucket Uploaded files and rendered PDFs.
.env and compose.override.yaml, in the installation directory Every secret, including DATA_ENCRYPTION_KEY, and your settings.

The other volumes hold nothing you need to keep, with one exception: caddy-data when you use the proxy’s own certificate authority. Keep it, or every client must trust a new root certificate.

When you restore:

  • Restore the database and the files from the same moment. The database records which files exist.
  • Restore into the same release or a newer one, never an older one. A newer release brings the database up to date as it starts.
  • A restored database is the same installation for your license; see installations per license.

DATA_ENCRYPTION_KEY in .env encrypts the secrets stored in the database, such as webhook signing secrets and signing certificate keys, and cannot be recreated. Keep the copy of .env apart from database copies: anyone with both can read those secrets.

A database restored without the key it was encrypted with still works, except that every webhook’s secret must be rotated and every signing certificate uploaded again under Manage → Signing certificates. To restore a database from before a key rotation, set the old key as DATA_ENCRYPTION_KEY_PREVIOUS next to the current one.