Why run it yourself at all?
Because contracts, payslips and scanned IDs should not be uploaded to a stranger's site just to join two files. Stirling PDF gives you more than 50 tools behind one web page: merge and split, compress, convert Office files, make scans searchable with OCR, redact, fill forms and sign. Its README describes it as a way to do this "without sending documents to external services". The project also offers optional anonymous usage statistics, and its docs say document contents are never part of them; you can switch them off completely.
If you already keep a paper archive in Paperless-ngx, this is the companion that edits the files before or after they go in.
How much server does it need?
Less than you might fear for a small office, more than you might expect for heavy OCR. The project's performance guide splits it by team size:
- Up to 10 people: 2-4 processor cores, 4 GB of memory, 10 GB of disk.
- 10 to 50 people: 4 to 8 cores, 8 to 16 GB, 50 GB with an SSD recommended.
- More than 50: 8 cores or more, 16 GB or more, upwards of 100 GB on SSD.
What your team actually does matters as much as how many of you there are. Reading text out of scans (OCR) is the hungriest job, rated very high for processor and high for memory; each Office-to-PDF conversion keeps one core busy; turning pages into images eats memory. Joining and splitting files is light. The guide also warns that a big PDF can need many times its own size in memory, and that if the server runs out, the container stops and every job in progress fails. That is the case for a machine whose memory is all yours.
Three images, one choice
latestis where the docs tell you to start. It has user accounts and the admin pages, plus LibreOffice, Tesseract, OCRmyPDF, Calibre and the other conversion tools. Suggested memory: 4 GB.latest-fatis the same with an AI engine built in, which stays off until you enable it. Suggested: 6 GB, more once AI runs.latest-ultra-litekeeps only the core PDF tools: no OCR, no Office or ebook conversion, and no login of any kind. Suggested: 2 GB.
Swapping later is painless: change the tag, recreate the container, and your folders stay as they were. For a team server, pin a release number from the releases list on GitHub instead of latest, so an update only happens on the day you pick.
With no login, ultra-lite lets anybody who can reach it use it. Fine on your laptop; on a machine anyone on the internet can reach, keep it private.
The server that fits
With a dedicated server you rent a whole physical computer that runs your work and nobody else's, so an OCR batch at month-end never has to share memory with another customer. You log in as root, nobody meters the port, and you can reinstall the operating system yourself from the control panel whenever you want a clean start. Rack servers and blades, as well as the m510 and m710x cartridges, add a console with the screen and the power button, which saves the day if a firewall rule shuts you out. A ready server is handed over within 30 minutes of the payment being approved, and the term is month to month.
For a few people, one of the small cartridge servers with 8 GB or more is plenty. For a whole office feeding it scans, start with an everyday single-processor server or a two-processor one and choose the memory before anything else. Whichever you pick, you manage the server; we keep the hardware and the network running.
Up and running in an evening
What you need: an Ubuntu install, Docker Engine, release 28 or later, plus the Compose plugin, and your own login with sudo. One thing to know about Docker first: if a compose file publishes a port with no address in front, Docker opens it on all of the server's addresses, IPv4 and IPv6 alike (0.0.0.0 and [::]). Over IPv4, that traffic skips past ufw's rules, and over IPv6 it does the same when Docker manages IPv6 itself. Publishing on 127.0.0.1 keeps the app private, and from Engine 28 on that really means only this machine can reach it.
- Become root with
sudo -iand paste the first block below. It switches on the firewall with only SSH allowed (setting IPV6 first so the rules cover both kinds of address), creates the folders, hands them to user 1000 because that is who the container runs as, and writes a private settings file. - Read the file once with
cat stirling.env: the admin password inside was generated by the server, so you never typed it and it is not in your shell history. - Save the second block as
docker-compose.ymlin/srv/stirling-pdf, then launch it:docker compose up -d. If the page never loads,docker logs stirling-pdfusually says why.
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH
ufw --force enable
mkdir -p /srv/stirling-pdf/data/{configs,logs,customFiles,pipeline,storage,tessdata}
chown -R 1000:1000 /srv/stirling-pdf/data
cd /srv/stirling-pdf
umask 077
{
echo "SECURITY_INITIALLOGIN_USERNAME='teamadmin'"
echo "SECURITY_INITIALLOGIN_PASSWORD='$(openssl rand -base64 24)'"
echo "SYSTEM_ENABLEANALYTICS='false'"
} > stirling.env
chmod 600 stirling.env
services:
stirling-pdf:
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest
container_name: stirling-pdf
ports:
- '127.0.0.1:8080:8080'
env_file: stirling.env
volumes:
- ./data/configs:/configs
- ./data/logs:/logs
- ./data/customFiles:/customFiles
- ./data/pipeline:/pipeline
- ./data/storage:/storage
- ./data/tessdata:/usr/share/tessdata
mem_limit: 4g
restart: unless-stopped
mem_limit matters more than it looks: without it, Java sizes itself from all the memory in the server rather than from what you meant to give it. Use 6g for the fat image.
Sign in first, privately
Out of the box, every login build creates an account named admin whose password is stirling, and everybody who has read the docs knows it. That is why the app stays on 127.0.0.1 until you have signed in.
- On your own computer, run
ssh -L 9090:127.0.0.1:8080 you@your.server.ipwith your usual login. Port 9090 on your side leads to the app. - Open
http://localhost:9090in your browser and sign in asteamadminwith the password fromstirling.env. - Skipped the settings file? Then sign in as admin / stirling now; the app makes you pick a new password of 8 characters or more straight away.
- Remove the password line from the file with
sudo sed -i '/^SECURITY_INITIALLOGIN_PASSWORD=/d' /srv/stirling-pdf/stirling.envand bring the container back:sudo docker compose -f /srv/stirling-pdf/docker-compose.yml up -d. It is only read while there are no users, and leaving it in the container's environment meansdocker inspectcan show it. - Add your colleagues under Settings, Workspace, Users. Without email set up, choose Create account directly.
Giving it a proper web address
Your team will want something like https://pdf.yourcompany.com, which means a reverse proxy: a web server in front that handles the certificate and passes requests to the app. The docs' production checklist includes a ready nginx configuration; three of its settings matter most.
client_max_body_size 2000M, because nginx's own default stops at 1 MB per request, while the app takes uploads up to 2000 MB. When this is missing, a big upload turns into a downloaded .htm file.proxy_buffering offwith 1800-second read and send timeouts, so long OCR runs are not cut off.- The forwarding headers for protocol, host and port (the X-Forwarded-* family), so the app builds correct links.
If nginx runs on the server itself (not in Docker), the firewall keeps it unreachable over both IPv4 and IPv6 until the rule ufw allow 80,443/tcp is added. Then add your public address as SYSTEM_FRONTENDURL and SYSTEM_BACKENDURL in stirling.env, in single quotes like the rest, so invite emails and share links point to the right place. Prefer clicking to config files? Our Nginx Proxy Manager guide shows that route.
Settings that stick
Most choices live in the app under Settings, and they are written to settings.yml in the configs folder. Two habits save you a confused afternoon. First, if a value is set in stirling.env, it wins and the admin page cannot change it; stirling.env beats custom_settings.yml, which beats settings.yml. Second, the app rebuilds settings.yml from its template at every start: commenting a line out just brings back the default, unknown keys disappear, and a file shorter than 31 lines counts as invalid: the app keeps it as a .bak file stamped with the time and puts the defaults in its place. Put your own small overrides in custom_settings.yml instead.
Keeping it updated, backed up and licensed
Before an update, copy the configs folder somewhere safe. If you pinned a release number, put the new one in docker-compose.yml first. Then sudo docker compose pull and sudo docker compose up -d, both from /srv/stirling-pdf. Your data lives in the mounted folders and survives the new container. Moving from the old V1 line is a bigger step, because the database is upgraded in place, so read the upgrade guide first.
For backups, take three folders: configs (your settings and keys, plus the built-in database with the nightly copies it saves in configs/backup/db/), storage (files people saved on the server) and customFiles if you added branding or signatures. Keep copies on another machine. To check that the app is alive from a monitor, call /api/v1/info/status; it needs no sign-in.
About the licence: the project calls itself open-core. Its main LICENSE file is the MIT licence, but it opens with "Portions of this software are licensed as follows" and lists directories, such as app/proprietary/ and engine/, that each carry their own LICENSE file; everything outside them is MIT. The one in app/proprietary/ is a commercial user licence. Without a paid subscription it allows use "for the sole purposes of internal trial, evaluation, or minimal use", within conditions it lists. The plans are a separate text, on the docs' Plans page, which does not mention that licence file. It describes every self-hosted install as starting on a Free plan: 5 users with every PDF tool, the API and OAuth2/OIDC sign-in. Unless you are on Enterprise, automated Processor runs and the AI tools use up a monthly processing allowance. PDF tool calls sent with an API key do too, except on a server holding a Team licence key, so API use on Free has a ceiling. Team or Enterprise add more users, an external database, clustering and, on Enterprise, SAML and audit logs. If the server will matter to your business, compare both licence files with the Plans page before you rely on it.
Questions people ask
- Is self-hosted Stirling PDF free?
- The core code is MIT, and the docs' Free plan for your own install covers up to 5 users with all the PDF tools. Parts of the repository, such as the app/proprietary directory, are under their own licence. Tool calls through an API key use up a monthly processing allowance, except with a Team licence key, and features like an external database, clustering or SAML need Team or Enterprise. Hold both licences and the Plans page up against how you intend to use it.
- What are the login details after install?
- admin and stirling, unless the two SECURITY_INITIALLOGIN_* variables named your own admin when the container first started. The first sign-in with the default account makes you choose a fresh password, minimum 8 characters. Do this over an SSH tunnel, with the app still listening only on 127.0.0.1.
- Can a small server run it?
- For up to ten people the project's guide suggests 2-4 cores and 4 GB of memory, with 10 GB of disk. The ultra-lite image gets by on about 2 GB but drops OCR and Office conversions. Heavy OCR use is what pushes you to a bigger machine.
- Does it work on ARM servers?
- Yes. Every image is published for both amd64 and arm64, so the same compose file runs on an ARM machine without changes.
- My big uploads fail. What is wrong?
- Almost always the reverse proxy. nginx accepts 1 MB per request unless you raise client_max_body_size, so set it to 2000M to match the app's default upload limit and lengthen the proxy timeouts.