Stirling-PDF is a self-hosted toolbox for PDFs. Merge, split, compress, sign, redact, convert and OCR run on your own server, so the files never go to an online PDF site. On October 17, 2026, the GitHub repository had about 94,300 stars, and the stirlingtools/stirling-pdf image had about 16.3 million pulls on Docker Hub, plus 22.5 million under the older frooodle/s-pdf name, which still gets new releases.
This guide runs Stirling-PDF 3.2.0 with Docker Compose and fixes the parts the quick start leaves to you. The default login is shown on the login page to anyone who opens it, extra OCR languages need a volume, and uploads through nginx fail above 1 MB until you change one setting. I ran every step on a Debian 13 test server with Docker 29.9.1, OCR'd scans I generated in English and Spanish, and upgraded a second install from 3.1.0 to 3.2.0.
TL;DR:
- Use the official compose file with a pinned tag such as
docker.stirlingpdf.com/stirlingtools/stirling-pdf:3.2.0, publish the port on127.0.0.1:8080, and keepmem_limit: 4g. - A fresh install creates
adminwith the passwordstirlingand prints both on the login page. SetSECURITY_INITIALLOGIN_USERNAMEandSECURITY_INITIALLOGIN_PASSWORDbefore the first start, or log in and change the password right away. - The standard image can OCR English, German, French, Portuguese and Simplified Chinese. For any other language, put its
.traineddatafile in the folder mounted at/usr/share/tessdataand restart. - Behind nginx, set
client_max_body_size 2000M. To update, change the tag, then rundocker compose pullanddocker compose up -d.
What Stirling-PDF runs and which image to pick
What's in the container
Stirling-PDF is one container. It holds a Java web app plus the command-line tools that do the heavy work. The diagram shows the setup this guide builds:
The 3.2.0 container logs its tool versions at startup. Mine listed OpenJDK 25.0.4, OCRmyPDF 17.13.0, Tesseract 5.3.4, LibreOffice 26.2.6.3, Ghostscript 10.08.0, qpdf 12.4.2 and calibre 9.15.0. LibreOffice doesn't start until the first Office conversion, so it uses no memory until you need it. One feature is off out of the box. The log said Missing dependency: rar - Disabling group: rar (Affected features: PDF To Cbr), which matches the docs.
Settings, the user database and generated keys go in /configs. On first start the app also creates credential-encryption.key there and logs a warning to back it up, because losing it makes stored integration secrets unrecoverable.
Standard, fat or ultra-lite
The Docker install docs list three images:
| Tag | Login | What's included | Suggested memory |
|---|---|---|---|
3.2.0 |
Yes | OCR plus Office, HTML and ebook conversion | 4 GB |
3.2.0-fat |
Yes | The same plus a bundled AI engine, off until you turn it on | 6 GB |
3.2.0-ultra-lite |
No | Core PDF tools only, no OCR or conversions | 2 GB |
Use the standard image. It's the one I tested end to end. The compressed download on Docker Hub was 1.17 GB for 3.2.0 and 394 MB for 3.2.0-ultra-lite. On disk they took 3.58 GB and 1.04 GB.
Ultra-lite is lighter, and my test showed what you give up. It used 357 MiB of RAM after starting, against 795 MiB to 1.08 GiB for the standard image. But it has no login at all. The browser went straight to the editor, API calls worked without credentials, and the OCR endpoint answered 403 This endpoint is disabled. Only run it on a network you trust.
The docs use latest in their examples, but latest moves with every release. Pin a version tag instead. All three tags above exist on docker.stirlingpdf.com, Docker Hub and ghcr.io.
Docker Compose setup
The compose file
This is the compose file from the install docs with three changes. The tag is pinned to the version I tested, the port listens on 127.0.0.1 only, and the first admin account comes from a .env file. Create the folder and the .env first:
mkdir -p ~/stirling-pdf/stirling-data/tessdata && cd ~/stirling-pdf
umask 077
cat > .env <<EOF
SECURITY_INITIALLOGIN_USERNAME=pdfadmin
SECURITY_INITIALLOGIN_PASSWORD=$(openssl rand -base64 18 | tr -d '/+=')
EOF
umask 022
cat .env
umask 077 makes .env readable only by you. Save the generated password in your password manager. Then save this as compose.yaml:
services:
stirling-pdf:
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:3.2.0
container_name: stirling-pdf
ports:
- "127.0.0.1:8080:8080"
environment:
SECURITY_INITIALLOGIN_USERNAME: ${SECURITY_INITIALLOGIN_USERNAME}
SECURITY_INITIALLOGIN_PASSWORD: ${SECURITY_INITIALLOGIN_PASSWORD}
volumes:
- ./stirling-data/configs:/configs
- ./stirling-data/logs:/logs
- ./stirling-data/customFiles:/customFiles
- ./stirling-data/pipeline:/pipeline
- ./stirling-data/storage:/storage
- ./stirling-data/tessdata:/usr/share/tessdata
mem_limit: 4g
restart: unless-stopped
Keep mem_limit. The image sizes the Java heap from it. My container logged Detected container memory: 4096MB and let Java use up to 70% of that. Without a limit, the server FAQ says the heap is sized from the host's RAM, which is why people see Stirling-PDF use a lot of memory while idle.
The mkdir creates the tessdata folder in advance on purpose. When I let Docker create it, the folder belonged to root, so I needed sudo to copy language files in later. The other folders ended up owned by UID 1000, the user the container runs as. If your files belong to a different user, the install docs say to set PUID and PGID.
First start
docker compose pull
docker compose up -d
curl -s http://127.0.0.1:8080/api/v1/info/status
The pull took 17 seconds on my server. About 19 seconds after up -d, the status endpoint answered:
{"version":"3.2.0","status":"UP"}
That endpoint needs no login, so it's a good target for an Uptime Kuma HTTP monitor. Port 8080 now only answers on the server itself. To use the web UI from your computer, open an SSH tunnel with ssh -L 8080:127.0.0.1:8080 you@your-server and browse to http://localhost:8080, or put a reverse proxy in front (see below).
Lock down the default login
What a fresh install shows
Login is on by default in the standard image. When I started a fresh install without the .env variables, the log said Default admin user created: admin, and the login page showed this to every visitor:
The page prints the password, so anyone who reaches it before you change it can log in as admin. Upstream's compose file publishes '8080:8080' on every interface, and Docker's published ports skip ufw rules, so a host firewall won't save you. That's the reason for the 127.0.0.1 binding.
Logging in as admin with stirling first asked whether to send anonymous usage data, then forced a new password:
After the change, Stirling-PDF logged me out, and the credentials box was gone from the login page. The sign-in docs add that an account locks for 120 minutes after 5 failed attempts.
Set your own first admin
The .env approach above skips all of that. On a fresh 3.1.0 install with SECURITY_INITIALLOGIN_USERNAME=pdfadmin, the log said Admin user created: pdfadmin. The login page had no credentials box, logging in went straight to the editor with no password prompt, and admin with stirling got a 401. The docs say both variables are ignored once the database has users, so changing them later does nothing. Change the password in the app instead.
Don't turn login off unless only you can reach the server. The server overview puts it plainly for SECURITY_ENABLELOGIN=false: "Anyone who can reach the server can then use it." With login on, the API is protected too. A POST to /api/v1/misc/ocr-pdf without credentials got a 401. The API description at /v1/api-docs and the Swagger page answered 200 without login, but they only describe the endpoints.
Add OCR languages
What's bundled, and what a missing language looks like
The standard image ships Tesseract data for Simplified Chinese, English, French, German and Portuguese. tesseract --list-langs in my container showed chi_sim, deu, eng, fra, osd and por. The ultra-lite image has no OCR.
I generated an image-only scan of an English invoice and OCR'd it through the API in 2.2 seconds. The invoice number, date and amounts came back right. Then I tried a scanned Spanish letter. Asking for Spanish before it was installed got an error:
HTTP 400 {"detail":"Invalid OCR languages format: none of the selected languages are valid", ... "errorCode":"E041"}
Per the docs, the web UI only lists installed languages. Running the Spanish letter with English worked, but every accent was gone, and "señora Núñez" became "sefiora Niifiez".
Add a language with the tessdata volume
The OCR languages docs describe two ways. The in-app download under Settings, Server, Advanced puts files inside the container, so they disappear when the container is recreated. The volume survives that. Download the .traineddata file from tesseract-ocr/tessdata (larger, more accurate) or tessdata_fast (smaller, faster) and restart:
cd ~/stirling-pdf
curl -fL -o stirling-data/tessdata/spa.traineddata \
https://github.com/tesseract-ocr/tessdata/raw/main/spa.traineddata
docker compose restart
docker exec stirling-pdf tesseract --list-langs
The file was 18 MB. At startup, the log said Found user-mounted tessdata in /usr/share/tessdata, copying to /usr/share/tesseract-ocr/5/tessdata, and spa joined the list. Spanish then showed up in the OCR tool:
Here's what the two runs stored for the same scan:
With spa, the accents and the ñ came out right, but three words were still wrong. OCR on a noisy scan isn't perfect in any language, so check important documents. If you want every scan OCR'd and filed automatically, Paperless-ngx uses the same OCRmyPDF and Tesseract tools on each document it imports. Stirling-PDF fits one-off jobs better.
The volume path is the most common mistake. In a GitHub issue where a user had mounted language files at /data/tessdata, maintainer Frooodle answered: "Your path is wrong, all our docs show it as /usr/share/tessdata". The docs add that files already in Tesseract's folder aren't replaced, and that eng.traineddata must stay, because English is always needed.
Reverse proxy, updates and backups
Big uploads behind nginx
For HTTPS, put Stirling-PDF behind a reverse proxy that forwards to 127.0.0.1:8080. Nginx Proxy Manager does that with a free certificate, and the Umami guide shows the same 127.0.0.1 pattern behind Caddy.
With nginx, check the upload size first. nginx accepts 1 MB request bodies by default, and PDFs are often bigger. I sent a 2.6 MB four-page scan through nginx 1.30.6 with a plain proxy_pass and got 413 Request Entity Too Large. The nginx log said client intended to send too large body: 2602334 bytes. The production checklist has the settings that fixed it:
server {
# listen 443 ssl, server_name and certificates as usual
client_max_body_size 2000M;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_buffering off;
proxy_read_timeout 1800s;
proxy_send_timeout 1800s;
}
}
The same request then returned 200 in 2.2 seconds with all four pages OCR'd. 2000 MB matches Stirling-PDF's own default upload limit. The long timeouts and proxy_buffering off are for slow jobs. Nginx Proxy Manager already sets client_max_body_size 2000m in the main nginx.conf of its 2.16.0 image, so this limit mostly bites hand-written nginx configs. The checklist also says to set SYSTEM_FRONTENDURL to your public address, because invite emails and share links use it.
Update and back up
Back up before you update. The production checklist names the folders. configs holds settings, keys and the built-in database, which also writes a nightly backup to configs/backup/db/. storage holds files users saved on the server. A tarball is enough:
cd ~/stirling-pdf
sudo tar czf stirling-backup-$(date +%F).tgz stirling-data/configs stirling-data/storage
Then change the tag and recreate the container. I tested this on a second install that started on 3.1.0 with its own admin account:
sed -i 's/stirling-pdf:3.1.0/stirling-pdf:3.2.0/' compose.yaml
docker compose pull
docker compose up -d
curl -s http://127.0.0.1:8080/api/v1/info/status
22 seconds later the status said 3.2.0, and the admin from the first start could still log in. Read the release notes before each bump. Releases come fast: 3.0.0 came out on September 24, 2026, 3.1.0 on October 5 and 3.2.0 on October 15. Coming from V1, read the upgrade guide the install docs link to first. If you'd rather deploy from a dashboard, the same compose file works as a Docker Compose resource in Coolify.
FAQ
Login and access
What is the default username and password for Stirling-PDF?
admin and stirling. A fresh standard image creates that account and shows it on the login page until you change the password, which the first login forces. To skip the default account, set SECURITY_INITIALLOGIN_USERNAME and SECURITY_INITIALLOGIN_PASSWORD before the first start. They only apply while the user database is empty.
Can I run Stirling-PDF without a login?
Yes. Set SECURITY_ENABLELOGIN=false, or use the ultra-lite image, which has no login at all. Then anyone who can reach the port can use every tool and the API, as my ultra-lite test showed. Keep it on 127.0.0.1 or a private network if you do this.
Is Stirling-PDF free to self-host?
Yes. The repository is MIT licensed, with some folders, such as the proprietary and SaaS code, under their own licenses. Per the plans page, a self-hosted server runs on the Free plan, with all PDF tools, the REST API and OAuth2 single sign-on, for up to 5 users. Team and Enterprise add more users and features like an external database.
OCR and performance
Why doesn't my OCR language show up in Stirling-PDF?
The .traineddata file isn't where the app looks. Mount your folder at /usr/share/tessdata inside the container, restart, and check with docker exec stirling-pdf tesseract --list-langs. If the API returns Invalid OCR languages format, the language you asked for isn't installed.
Why does Stirling-PDF use so much RAM?
Java sizes its heap from the memory it sees. Set mem_limit in compose or --memory with docker run. With a 4 GB limit, my standard container used between 795 MiB and 1.08 GiB while idle. If that's too much and you don't need OCR or conversions, ultra-lite used 357 MiB.
Why do large uploads fail behind a reverse proxy?
The proxy is rejecting the upload. nginx allows 1 MB by default and returned 413 Request Entity Too Large for my 2.6 MB scan. Set client_max_body_size 2000M in the server block and reload nginx. Nginx Proxy Manager already allows 2000 MB.
Originally published on DeployLark: Stirling-PDF Docker Setup: Login, OCR Languages and Updates





Top comments (0)