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.
Before you start
Section titled “Before you start”- A Linux server with an x86-64 (amd64) CPU. The images are built for
linux/amd64only. - Docker Engine with the Compose v2 plugin, so that
docker composeworks. See Docker’s install guide. opensslandcurlon 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.
1. Download the release files
Section titled “1. Download the release files”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:
docker login registry.typefold.dev -u licensemkdir -p /opt/typefold && cd /opt/typefolddocker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/out" registry.typefold.dev/typefold-release:latestcp env.example .envchmod 600 .envlatest 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.
2. Generate the secrets
Section titled “2. Generate the secrets”Run each command once and keep its output for the next step:
openssl rand -hex 32 # for POSTGRES_PASSWORDopenssl rand -hex 32 # for BETTER_AUTH_SECRETopenssl rand -base64 32 # for DATA_ENCRYPTION_KEY3. Fill in .env
Section titled “3. Fill in .env”Open .env and set the address, the three secrets and your license key:
PUBLIC_URL=https://pdf.example.comPOSTGRES_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.
4. Point DNS at the server
Section titled “4. Point DNS at the server”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.
5. Start it
Section titled “5. Start it”docker compose up -ddocker compose pscurl https://pdf.example.com/healthdocker 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.
-
Create a password account for yourself. The command prints a generated password.
Terminal window -
Read the setup token:
Terminal window docker compose logs api | grep setupToken -
Open
PUBLIC_URLin a browser and sign in with your email address and that password. -
Studio opens Set up your installation. Paste the token into Setup token and choose Complete setup. You are now a platform administrator.
-
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.
If it does not start
Section titled “If it does not start”/healthanswers503. Itsunavailablelist names what the API cannot reach,databaseorredis. Rundocker compose logs apito see why.docker composestops withSet <setting> in .env. That required setting is empty in.env.- The API exits with
Unsafe production settings. It refuses anhttp://PUBLIC_URL(except a localhost address) and well-known example secrets. The log names the setting to fix; it callsPUBLIC_URLBETTER_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
createdorstarting. Each service waits until the ones it needs are healthy. Read the logs of the first service that is not healthy.
On a private network
Section titled “On a private network”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:
- An IP address, or a name ending in
.internal,.home.arpaor.local, such ashttps://10.0.0.5: nothing. The proxy issues the certificate from its own certificate authority. Then trust the proxy’s certificate authority. - Any other name, such as
https://pdf.lan: setCERTIFICATE_SOURCE=internalin.env, then trust the proxy’s certificate authority. - A certificate from your company’s certificate authority: use your own certificate.
Trust the proxy’s certificate authority
Section titled “Trust the proxy’s certificate authority”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:
docker compose cp proxy:/data/caddy/pki/authorities/local/root.crt typefold-root.crtInstall 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.
Use your own certificate
Section titled “Use your own certificate”-
Set
CERTIFICATE_SOURCE=filesin.env. -
Put the certificate chain and its unencrypted private key, both PEM, in a
certificatesdirectory next tocompose.yaml(CERTIFICATE_PATHchanges the directory):/opt/typefold/certificates/certificate.pem/opt/typefold/certificates/key.pem -
Run
docker compose up -d. When you replace the files later, rundocker compose restart proxy.
Behind a CDN or load balancer
Section titled “Behind a CDN or load balancer”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:
-
In
.env, setTRUSTED_PROXY_RANGESto 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 -
Run
docker compose up -d proxy.
For requests from those ranges, the proxy takes the client’s address from the X-Forwarded-For
header.
File storage
Section titled “File storage”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 |
Amazon S3 and compatible services
Section titled “Amazon S3 and compatible services”-
Create a bucket, and an access key that can read, write, list and delete objects in it.
-
Add to
.env:Terminal window STORAGE_DRIVER=s3OBJECT_STORAGE_ENDPOINT=https://s3.eu-central-1.amazonaws.comOBJECT_STORAGE_REGION=eu-central-1OBJECT_STORAGE_BUCKET=my-typefold-bucketOBJECT_STORAGE_ACCESS_KEY_ID=...OBJECT_STORAGE_SECRET_ACCESS_KEY=...OBJECT_STORAGE_FORCE_PATH_STYLE=false # false for Amazon S3; leave it out for MinIO -
Set up CORS, then run
docker compose up -d.
File storage settings lists the rest, such as a separate address for browsers.
Azure Blob Storage
Section titled “Azure Blob Storage”-
Create a storage account and a container.
-
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=azureAZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.netOBJECT_STORAGE_BUCKET=pdf-results # the container's name -
Set up CORS, then run
docker compose up -d.
Bucket CORS and Studio
Section titled “Bucket CORS and Studio”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:
STUDIO_FILE_ORIGINS=https://my-bucket.s3.eu-central-1.amazonaws.comIf the browser console shows Refused to ... messages about the Content-Security-Policy, an
origin is missing from STUDIO_FILE_ORIGINS.
Scaling and memory
Section titled “Scaling and memory”Add workers
Section titled “Add workers”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.
-
Create
compose.override.yamlnext tocompose.yaml:services:worker:deploy:replicas: 3 -
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.
Memory limits
Section titled “Memory limits”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.
Upgrade
Section titled “Upgrade”Run these in the installation directory:
-
Back up the database and file storage.
-
Download the new release’s files as in step 1, which replaces
compose.yamlandenv.example. Keep.envand anycompose.override.yaml. -
Apply it:
Terminal window docker compose up -d --remove-orphans -
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.
One-off steps for older installations
Section titled “One-off steps for older installations”-
Releases with a
backupservice kept backups in thepdf-renderer_backupsvolume, or in the directory named byBACKUP_PATH. Nothing deletes them any more: copy any you still need, then rundocker 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.jsEach 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.
What happens to work in flight
Section titled “What happens to work in flight”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 afterWORKER_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:
RENDER_TIMEOUT_MS=120000WORKER_CLOSE_GRACE_MS=135000WORKER_STOP_GRACE_PERIOD=150sWhat gets installed
Section titled “What gets installed”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. |
Where your data lives
Section titled “Where your data lives”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.
The data encryption key
Section titled “The data encryption key”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.
Next steps
Section titled “Next steps”- Create an API key under Manage → API keys and follow the quickstart.
- Invite people, and turn on email or Microsoft sign-in for them.
- Turn on monitoring so you hear when something breaks.