VPS Tutorials
Self-Host Stirling PDF on a VPS (2026): Complete Docker, OCR, HTTPS, Login and Backup Tutorial
Self-host Stirling PDF on a VPS with Docker Compose, configure Chinese OCR, Caddy HTTPS, enforced login, health checks and data backups, so you never have to upload sensitive PDFs to unfamiliar websites.

When merging PDFs online, compressing files or redacting a contract, the biggest worry is usually not whether the tool works well, but where the file actually ends up. For documents like ID cards, contracts and quotes, I would not recommend casually dropping them into online PDF sites of unknown origin.
Stirling PDF lets you put common PDF tools on your own VPS: merging, splitting, compressing, format conversion, OCR, watermarking and metadata removal all happen in the browser. File processing takes place on your own server, and you control the entry point, accounts and backups.
This tutorial deploys the standard image with Docker Compose, with Caddy handling HTTPS. The backend port 8080 only listens on 127.0.0.1 and is never exposed directly to the public internet; login, the Chinese interface, health checks and persistent directories are all enabled.
Stirling PDF suits the following situations well:
- You frequently handle contracts and scanned documents and don't want to upload them to third-party websites;
- A small team needs a shared PDF toolbox;
- You want to batch-merge, OCR or convert PDFs via the API;
- You already have a VPS and want to put idle resources to use.
It also has clear limitations. OCR, Office-to-PDF conversion and large-file compression can all consume a lot of CPU and memory in an instant; server administrators can still access the host machine, so "self-hosted" does not mean "no one can see the files". If you only occasionally handle a single non-sensitive PDF, desktop software is actually less hassle.
For personal use you can start with 2 cores, 2GB of memory and 20GB SSD. If you need Chinese OCR, Office document conversion, or several people using it at the same time, at least 2 cores, 4GB of memory and 40GB SSD are recommended.
| Use case | Recommended configuration | Image choice |
|---|---|---|
| Merging, splitting, rotating PDFs | 2 cores 2GB | latest |
| OCR, Office conversion, daily team use | 2-4 cores 4GB | latest |
| Need extra fonts and fuller conversion capabilities | 4 cores 8GB | latest-fat |
| Low-spec machine, basic features only | 1-2 cores 1-2GB | latest-ultra-lite |
The standard image is enough for most people. latest-fat is larger but has more complete extra fonts and tools; latest-ultra-lite saves disk space but is not suitable for public deployments that rely on login and full OCR.
The system used here is Ubuntu 24.04 LTS. Prepare a domain that already resolves to the VPS public IP, for example pdf.example.com, and open only SSH, 80 and 443 in the security group and UFW.
If Docker is not yet installed on the server, install it from the official Docker repository. After installation, first confirm both commands produce output:
docker --version
docker compose version
Do not keep using the no-longer-maintained docker-compose v1. All commands below use Compose v2, i.e. docker compose with a space in the middle.
Create the deployment directory:
sudo mkdir -p /opt/stirling-pdf/data/{configs,customFiles,logs,pipeline,tessdata}
sudo chown -R "$USER":"$USER" /opt/stirling-pdf
cd /opt/stirling-pdf
Do not put the example password directly on the public internet. First generate a random password, then write it into .env with permissions 600:
openssl rand -base64 24
nano .env
chmod 600 .env
The contents of .env are as follows; replace the second line with the value just generated:
STIRLING_ADMIN_USER=admin
STIRLING_ADMIN_PASSWORD=请替换成随机密码
Environment variables can still be viewed by anyone with Docker management permissions, so Docker management permissions themselves should only be given to trusted administrators. After the first login, change the password in Stirling PDF's account settings.
Create /opt/stirling-pdf/compose.yaml:
services:
stirling-pdf:
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest
container_name: stirling-pdf
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
volumes:
- ./data/configs:/configs
- ./data/customFiles:/customFiles:rw
- ./data/logs:/logs
- ./data/pipeline:/pipeline
- ./data/tessdata:/usr/share/tessdata
environment:
DISABLE_ADDITIONAL_FEATURES: "false"
SECURITY_ENABLELOGIN: "true"
SECURITY_INITIALLOGIN_USERNAME: "${STIRLING_ADMIN_USER:?set STIRLING_ADMIN_USER in .env}"
SECURITY_INITIALLOGIN_PASSWORD: "${STIRLING_ADMIN_PASSWORD:?set STIRLING_ADMIN_PASSWORD in .env}"
SYSTEM_DEFAULTLOCALE: "zh-CN"
SYSTEM_GOOGLEVISIBILITY: "false"
SHOW_SURVEY: "false"
healthcheck:
test:
- CMD-SHELL
- >-
curl -fsS http://localhost:8080/api/v1/info/status |
grep -q 'UP'
interval: 30s
timeout: 10s
retries: 10
start_period: 60s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
deploy:
resources:
limits:
cpus: "2.0"
memory: 4G
First let Compose expand the environment variables and check the syntax, then start:
docker compose config >/dev/null
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 stirling-pdf
The official project only provides security support for the latest version, so the example uses latest. This does not mean you should blindly update every day. After the first successful run, record the current image digest, back up /configs before upgrading, then run pull and rebuild:
docker image inspect \
docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest \
--format '{{index .RepoDigests 0}}'
If pulling from the official image registry fails, you can switch to stirlingtools/stirling-pdf:latest on Docker Hub, or ghcr.io/stirling-tools/stirling-pdf:latest on GitHub Container Registry.
Stirling PDF receives contracts, ID documents and other sensitive files, so do not log in over plain HTTP. After installing Caddy, edit /etc/caddy/Caddyfile:
pdf.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:8080
request_body {
max_size 200MB
}
}
Check the configuration and reload:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
sudo systemctl status caddy --no-pager
If you haven't configured Caddy before, you can first read the complete guide to using Caddy as a reverse proxy on a VPS.
Confirm the backend is not listening on the public internet:
sudo ss -lntp | grep ':8080'
curl -fsS http://127.0.0.1:8080/api/v1/info/status
curl -I https://pdf.example.com
The first line should show 127.0.0.1:8080, not 0.0.0.0:8080; the health endpoint returns JSON containing UP. Then open https://pdf.example.com, log in with the administrator account from .env and change the password immediately.
Stirling PDF's OCR uses Tesseract. The standard Docker image usually includes common language packs such as Simplified Chinese; the languages actually available still depend on the files in the current container:
docker exec stirling-pdf sh -lc \
'find /usr/share/tessdata -maxdepth 1 -name "*.traineddata" -printf "%f\n" | sort'
Simplified Chinese corresponds to chi_sim.traineddata, Traditional Chinese to chi_tra.traineddata, and English to eng.traineddata. If a language is not visible in the interface, don't just change the dropdown; first confirm whether the corresponding language pack actually exists.
OCR results also depend on the original file. A 300 DPI scan with clear text and correct page orientation is usually much better than a casual phone photo with obvious shadows and text smaller than 8pt. Tesseract can recognize text, but it will not reliably reconstruct complex table structures and formulas.
After processing a test scan, confirm at least three things: Chinese is searchable, text can be copied, and the downloaded PDF opens in common readers. Seeing only a task-completed message does not mean the recognition result is actually usable.
The most dangerous configuration is disabling login and exposing 8080 directly to the public internet. Others can not only consume your CPU and memory, but also upload maliciously crafted files to probe the conversion components.
Check each item before going live:
SECURITY_ENABLELOGIN=true, and the administrator password has been changed;- 8080 is bound only to
127.0.0.1, and only 80/443 are open to the public internet; /configsis persisted, and.envhas permissions600;- Unneeded users, API keys and features are disabled promptly;
- Pull security updates regularly, but back up before each update;
- Set disk alerts on the server to avoid temporary files or logs filling the disk.
Stirling PDF's security page explicitly states that older versions will not receive backported security fixes. Do not leave a public instance unupgraded for years, and do not treat "the container starts" as proof that security maintenance is complete.
On the free version, logged-in users, local settings and the built-in H2 database are mainly stored in /configs. The easiest approach is to back up the entire data directory rather than copying only compose.yaml.
First stop the container and make a more consistent archive:
cd /opt/stirling-pdf
mkdir -p backups
docker compose stop stirling-pdf
tar -czf "backups/stirling-data-$(date +%F-%H%M%S).tar.gz" data
docker compose start stirling-pdf
A local archive is not a complete backup. Then use restic, rclone or object storage to copy it to another machine and set a retention period. You can follow the VPS backup and restore drill guide to perform an off-machine restore.
When restoring, do not directly overwrite database files that are in use:
cd /opt/stirling-pdf
docker compose stop stirling-pdf
mv data "data.before-restore-$(date +%F-%H%M%S)"
tar -xzf backups/stirling-data-YYYY-MM-DD-HHMMSS.tar.gz
docker compose up -d
curl -fsS http://127.0.0.1:8080/api/v1/info/status
After restoring, actually log in and confirm that accounts, settings and OCR languages are still there. A backup file that exists but has never been restored only counts as "having an archive", not "having restore capability".
First check the exit reason and recent logs:
docker inspect stirling-pdf --format '{{.State.ExitCode}} {{.State.OOMKilled}}'
docker compose logs --tail=200 stirling-pdf
free -h
df -h
OOMKilled=true is usually insufficient memory, especially during OCR or Office conversion. First reduce concurrency and switch to the standard or ultra-lite image; if OOM still occurs frequently, you should add memory rather than endlessly adding swap.
First identify which layer the limit is at: Caddy's request_body max_size, Stirling PDF's own upload limit, and whether Cloudflare is in front. Whichever of the three layers is smaller will return 413. Re-verify after changing the configuration, and don't set the limit to unlimited all at once.
Check whether /configs is actually mounted:
docker inspect stirling-pdf \
--format '{{range .Mounts}}{{println .Destination .Source}}{{end}}'
ls -lah /opt/stirling-pdf/data/configs
If /configs is not mapped to the host, accounts and settings will disappear along with the container when it is deleted.
First confirm that chi_sim is selected, then check scan resolution, page orientation and contrast. For PDFs that already have a text layer, just use automatic mode; forcing re-OCR may actually make originally clear text worse.
After deployment, I would accept it against the following criteria rather than just checking whether the homepage opens:
docker compose psshows the container as healthy;- 8080 only listens on the loopback address and cannot be accessed directly from the public internet;
- The HTTPS certificate is valid and HTTP automatically redirects to HTTPS;
- Users who are not logged in cannot use the PDF tools;
- Run one test file each for merging, compression and Chinese OCR;
- Accounts and settings still exist after restarting the container;
- Restore from backup to a test directory, and you can log in again and process files.
If you plan to use it for your team long-term, then add container log rotation, disk alerts and upgrade windows. For production Compose health checks, resource limits and rollback approaches, you can continue reading the VPS Docker Compose production configuration checklist.
The value of Stirling PDF is not just "lots of free PDF tools", but that sensitive files don't have to pass through unfamiliar websites. Once you've set it up with a 2-core 4GB VPS, Docker Compose, Caddy HTTPS and enforced login, and then run a round of OCR, restart and restore tests, the service is truly usable.