[Blog](https://deploywithox.com/blog) [Guides](https://deploywithox.com/blog#c-guides) # How to self-host Stirling-PDF without Docker on your VPS Run Stirling-PDF v3.1.0 on your own Ubuntu server with ox: a Gradle build of the Java app, OCR and office tools from apt, six variables, no Docker. Saurav Sharma Oct 17, 2026 10 min read On this page - [What you need](https://deploywithox.com/blog/self-host-stirling-pdf#what-you-need) - [The steps](https://deploywithox.com/blog/self-host-stirling-pdf#steps) - [The ox.toml, line by line](https://deploywithox.com/blog/self-host-stirling-pdf#config) - [Accounts and the database](https://deploywithox.com/blog/self-host-stirling-pdf#services) - [Updates and backups](https://deploywithox.com/blog/self-host-stirling-pdf#updates) - [Frequently asked questions](https://deploywithox.com/blog/self-host-stirling-pdf#faq) To self-host Stirling-PDF you need an Ubuntu server, a fork of the Stirling-PDF repo with one `ox.toml` file in it, and six variables, two of them the first admin's name and password. In our run the first deploy took about 15 minutes, which includes the Gradle build and the install of the PDF tools. **How we tested** On Oct 17, 2026, ox deployed Stirling-PDF v3.1.0 from commit [`b99fa929`](https://github.com/Stirling-Tools/Stirling-PDF/commit/b99fa929e365760c863956bf23c127b098c288e5) on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory. The run then redeployed it, backed it up and rebooted the server. Then a separate test agent, one that did not write the config, signed in, uploaded a PDF, compressed it, and got a valid PDF back, and checked the app's services and its backup. [Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF) is a PDF toolbox you run yourself. You upload a PDF in the browser, pick a tool and download the result. Its README counts more than 50 tools, among them merge, split, compress, convert, OCR, sign and redact, with a REST API for nearly all of them. The server is a Spring Boot app in Java. The project calls itself open-core, and the FAQ below covers its license. ox deployed Stirling-PDF v3.1.0 (commit b99fa929) on Oct 17, 2026 on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory; the first deploy took 15 minutes and a redeploy took 6 minutes. - **First deploy:** 15 minutes, from adding the project until the app passed the run's first checks. - **Redeploy of the same commit:** 6 minutes, until the app passed the checks again. - **Server:** DigitalOcean, 8 vCPUs and 16 GB of memory (`s-8vcpu-16gb`), with this app alone on it. Every one of the 85 apps we deployed ran on this one size, so it was not picked for this app. - **Memory in use after the reboot:** 1548 MB of 15986 MB, for the whole server: Ubuntu, the app, its services, Caddy and the ox agent. On a server with no apps, ox's tests on real servers hold the agent to 40 MB or less and Caddy to 50 MB or less. - **Source:** [github.com/Stirling-Tools/Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF) at v3.1.0, commit [`b99fa929`](https://github.com/Stirling-Tools/Stirling-PDF/commit/b99fa929e365760c863956bf23c127b098c288e5). The Stirling PDF log-in page The Stirling PDF workspace with its tool list after logging in A PDF compressed by Stirling PDF, ready to download ## What you need - An Ubuntu LTS server. The run used the size in the facts above and measured the memory there. - A GitHub account to fork `Stirling-Tools/Stirling-PDF`, and an ox account, which you open by signing in with GitHub. - A name and a password for the first admin. No outside account is involved. Email invites, single sign-on, phone uploads by QR code and a paid license key were not tested. ## The steps In the ox dashboard, choose Add a server. It prints one command with a single-use token. Paste it into a root shell on the server, which gets only the ox agent and Caddy. Fork `Stirling-Tools/Stirling-PDF` on GitHub and make sure your fork's default branch ends at commit `b99fa929`, the v3.1.0 release the run deployed. A newer commit was not tested. Add the `ox.toml` shown below at the repo root, then commit and push. No other file is needed. Write `stirling-pdf.env` with the six keys from the variables section, one `KEY=value` per line, and pick the admin's name and a long password. With the [ox CLI](https://deploywithox.com/docs/cli) signed in, run: Shell ```bash ox new /Stirling-PDF ox review stirling-pdf --from-file stirling-pdf.env --wait ``` ox lower-cases the repo name, so the project is `stirling-pdf`. The review saves the values and starts the first deploy, and `--wait` prints the log until it ends. The [quickstart](https://deploywithox.com/docs/quickstart) shows the browser version. When the deploy is green, `ox status stirling-pdf` prints the project's address. Open `/login` there and sign in with the admin name and password from your file. A welcome dialog may open on the first visit. Close it to reach the tool list. After a restart or a reboot, Stirling takes about 45 seconds before the page answers. The test agent pressed Add Files to open a one-page PDF, picked Compress in the tool list, kept the default level and pressed Compress. The page called `POST /api/v1/misc/compress-pdf`, the server answered 200, and the result appeared with Undo and Download. The file was already small, so Stirling's log says the compressed copy came out larger and it returned the original. A wrong password was refused with "Invalid username or password." The run used the preview address only. A domain of your own was not part of it, and the [domains page](https://deploywithox.com/docs/operate/domains) covers how ox adds one. ## The ox.toml, line by line TOML ```toml # Stirling-PDF v3.1.0 without Docker: the build of docker/embedded/Dockerfile # (the Spring Boot jar with the React frontend inside, proprietary flavor) and # the PDF tools its base image (docker/base/Dockerfile) ships, from Ubuntu's # packages. The app finds each tool by name on PATH (RuntimePathConfig) and # turns off the tools whose command is missing (ExternalAppDepConfig). packages = [ # LibreOffice: office files to PDF and back (soffice, one profile per job). "libreoffice-writer-nogui", "libreoffice-calc-nogui", "libreoffice-impress-nogui", "libreoffice-draw-nogui", "libreoffice-base-nogui", "libreoffice-java-common", # OCR: ocrmypdf over tesseract, with the languages the image ships. "ocrmypdf", "unpaper", "pngquant", "tesseract-ocr", "tesseract-ocr-eng", "tesseract-ocr-deu", "tesseract-ocr-fra", "tesseract-ocr-por", "tesseract-ocr-chi-sim", "tesseract-ocr-osd", # Compress, repair, HTML and e-book conversion, PDF to HTML. "ghostscript", "qpdf", "imagemagick", "weasyprint", "calibre", "poppler-utils", "poppler-data", # Fonts the image installs, so converted documents keep their text (ox's # java tool brings fontconfig). "fontforge", "fonts-dejavu", "fonts-liberation2", "fonts-crosextra-caladea", "fonts-crosextra-carlito", "fonts-noto-core", "fonts-noto-mono", "fonts-noto-extra", "fonts-noto-cjk", "fonts-freefont-ttf", ] # The first admin (InitialSecuritySetup): without both, the app makes admin with # the well-known password "stirling". required_variables = ["SECURITY_INITIALLOGIN_USERNAME", "SECURITY_INITIALLOGIN_PASSWORD"] [tools] java = "temurin-25" node = "24" task = "3.54.0" # The python3 on the app's PATH runs its scripts (static/python: OpenCV, pdf2image). python = "3.13.8" [build] commands = [ # The form-detection model the image bakes in (pinned commit and checksum, as in the Dockerfile). "mkdir -p preinstalled-models && curl -fsSL https://huggingface.co/Frooodle/ffdetr-int8/resolve/ad3223d7c3ae25ce1d1ec8fe972c8ff7630aff45/ffdetr-int8.onnx -o preinstalled-models/ffdetr.onnx && echo 'de43bb39adb08459fe62fa1a7be6bfb4f2d397bb034c46055f3b8fa3d04fc8f4 preinstalled-models/ffdetr.onnx' | sha256sum -c -", # The Dockerfile's gradle build; JDK 25 takes the compiler exports only in JDK_JAVA_OPTIONS. "JDK_JAVA_OPTIONS='--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' STIRLING_FLAVOR=proprietary ./gradlew clean build -PbuildWithFrontend=true -PbuildWithPortal=true -PjpdfiumPlatforms=$([ \"$(uname -m)\" = aarch64 ] && echo linux-arm64 || echo linux-x64) -PprototypesMode=false -PbundleOnnxRuntime=true -x spotlessApply -x spotlessCheck -x test -x sonarqube --no-daemon", "cp \"$(ls app/core/build/libs/stirling-pdf-*.jar | grep -v -- -plain | head -n 1)\" stirling-pdf.jar", # The image's /opt/venv, first on its PATH: the modules its Python scripts import # (static/python: OpenCV, pdf2image; scripts/convert_cff_to_ttf.py: fontTools). The app checks # `python3 -c "import cv2"` at start and turns off extract-image-scans without it. "python -m venv .venv && .venv/bin/pip install --no-cache-dir opencv-python-headless pdf2image fonttools", ] [app] # The image's JVM flags and LibreOffice environment (docker/embedded/Dockerfile, # scripts/init-without-ocr.sh). The model is read in place from this release, and the PDF # editor's font converter (pdfEditor.cffConverter) runs the venv's python on the repo's script, # which the image keeps at /opt/venv/bin/python3 and /scripts. start = "export PATH=\"$PWD/.venv/bin:$PATH\" FORMDETECTION_PREINSTALLEDMODELDIR=\"$PWD/preinstalled-models\" PDFEDITOR_CFFCONVERTER_PYTHONCOMMAND=\"$PWD/.venv/bin/python3\" PDFEDITOR_CFFCONVERTER_PYTHONSCRIPT=\"$PWD/scripts/convert_cff_to_ttf.py\" SAL_USE_VCLPLUGIN=svp SAL_DISABLE_PRINTERLIST=1 OOO_FORCE_DESKTOP=none MALLOC_ARENA_MAX=2 DBUS_SESSION_BUS_ADDRESS=/dev/null; exec java -XX:+ExitOnOutOfMemoryError -XX:+UseG1GC -XX:+UseCompactObjectHeaders -XX:+UseStringDeduplication -Dspring.threads.virtual.enabled=true -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Dserver.address=127.0.0.1 -Dserver.port=$PORT -jar stirling-pdf.jar" health = "/api/v1/info/status" # The embedded H2 database in configs/ is locked by the running copy. stop_before_start = true [storage] # The app's state tree, rooted at its working directory (InstallationPathConfig): # settings and the H2 database, logs, custom files, pipeline folders, stored files. keep = ["configs", "logs", "customFiles", "pipeline", "storage"] ``` The config redoes what two Stirling Dockerfiles do, without an image: `docker/embedded/Dockerfile` builds the jar, and `docker/base/Dockerfile` adds the tools around it. - `packages` installs most of what the image's base Dockerfile ships, from Ubuntu. LibreOffice reads office files. OCRmyPDF and Tesseract do OCR, here in English, German, French, Portuguese and simplified Chinese. Ghostscript, qpdf, ImageMagick, WeasyPrint, Calibre and poppler cover compression, repair and conversion. Outside Docker, Stirling finds each program by its plain name on `PATH` and switches off a tool whose program is missing. The config leaves out `unoconvert`, a LibreOffice helper the image has. Stirling's code tries it for office files and falls back to calling `soffice`. Converting office files was not tested. - `required_variables` names the first admin's two keys. Stirling creates that admin on its first start, while no user exists. With either key empty, its source creates `admin` with the password `stirling`, which is public in its source, so ox asks for both before the first deploy starts. - `[tools]` pins the Dockerfile's Java 25 (Temurin), Node 24 and Task 3.54.0, since Gradle calls `task` to build the web interface, and Python 3.13.8 for the scripts in Stirling's `static/python` folder. - The build commands follow the Dockerfile. First comes a download of the form-detection model at a pinned commit, checked against its SHA-256. Next is `./gradlew clean build` with the Dockerfile's flags: the default `proprietary` flavor, the web interface and its admin portal inside the jar, the native PDF library for x64 or arm64, no tests, no Gradle daemon. The `JDK_JAVA_OPTIONS` prefix holds compiler flags that JDK 25 takes only there. The jar is then copied to `stirling-pdf.jar`, and the last command makes a `.venv` with OpenCV, pdf2image and fontTools. At start Stirling runs `python3 -c "import cv2"` and turns the OpenCV tools off when the import does not work. - `start` puts the `.venv` first on `PATH`, points `FORMDETECTION_PREINSTALLEDMODELDIR` at the model, and moves the PDF editor's font converter off its defaults, which are Docker paths. The `SAL_` and `OOO_` settings come from Stirling's own start script, where they keep LibreOffice's memory small. Then `exec java` runs the jar with some of the image's JVM flags. `-Dserver.address` binds it to the loopback address, so only Caddy reaches it, and `-Dserver.port=$PORT` takes the port ox gives the app. - `health` is Stirling's status path, and ox switches traffic to a new release once it answers. `stop_before_start = true` stops the running copy first, because Stirling keeps accounts in an embedded H2 database file under `configs` and a second copy cannot open it. The cost is a short gap in every deploy, with 502 answers until the new release is up. - `keep` lists Stirling's state folders: `configs` for settings and the database, `logs`, `customFiles`, `pipeline` and `storage`. ox links each to a place that outlives a release. The config declares no workers and no scheduled jobs. The run found one systemd unit for Stirling-PDF and no timers. Shell ```bash SECURITY_ENABLELOGIN SECURITY_INITIALLOGIN_USERNAME SECURITY_INITIALLOGIN_PASSWORD SYSTEM_BACKENDURL SYSTEM_FRONTENDURL SYSTEM_ENABLEANALYTICS ``` `SECURITY_ENABLELOGIN` is `true` and turns the sign-in page on. `SECURITY_INITIALLOGIN_USERNAME` and `SECURITY_INITIALLOGIN_PASSWORD` are the first admin. Changing them after the first start changes nothing, because Stirling reads them only while no user exists. `SYSTEM_BACKENDURL` and `SYSTEM_FRONTENDURL` are both `${PUBLIC_URL}`, a reference ox fills in with the project's address, and Stirling uses them in the links it hands a browser. `SYSTEM_ENABLEANALYTICS` is `false`. Per Stirling's settings template, leaving it unset asks the admin on the first launch. See [variables](https://deploywithox.com/docs/operate/variables) for how references work. ## Accounts and the database The config asks ox for no service, so no Postgres was set up. Accounts and settings live in the kept `configs` folder. Stirling's source sets a default limit of five users until a license adds more, and the run had one admin. ## Updates and backups A new release reaches you when you merge its tag into your fork and push, and `ox deploy stirling-pdf --wait` deploys the branch again without a push. A newer version was not tested. The config follows two Dockerfiles, so compare them with the new tag first. Backups need no setup. ox backs up every kept folder daily, and the run's Back up now left one archive each for `configs`, `logs` and `pipeline`. `ox backups stirling-pdf` lists them. Stirling also exports its own database at midnight by default, into `configs`. A restore was not tested. The [backups page](https://deploywithox.com/docs/operate/backups) covers copies in an S3 bucket, and [self-hosting with ox](https://deploywithox.com/self-hosting) lists the other proven apps. ## Frequently asked questions ### Do I need Docker to self-host Stirling-PDF? No. The README starts with a `docker run` command, but the app is a Spring Boot jar. ox builds it with Gradle on your server, installs the PDF tools from Ubuntu's packages, and runs it as a system service. There is no image to pull, and an update is a push to your fork or one deploy command. ### How do I set the first Stirling-PDF admin? Set SECURITY_INITIALLOGIN_USERNAME and SECURITY_INITIALLOGIN_PASSWORD before the first deploy, then sign in at /login. If either is empty, Stirling's source creates a user named admin with the password stirling, so fill both. It reads them only on the first start, while no user exists. ### How much memory does Stirling-PDF use? The facts above give what the run used: the whole server, measured after a reboot with the app just started, so it counts Stirling, Caddy and the ox agent. The config sets no Java heap size. ox takes any server from 1 GB up and tells you when memory is why an app stopped. ### Is Stirling-PDF free to self-host? Most of the repo is MIT, and its [LICENSE](https://github.com/Stirling-Tools/Stirling-PDF/blob/b99fa929e365760c863956bf23c127b098c288e5/LICENSE) lists folders with their own terms. One is [`app/proprietary`](https://github.com/Stirling-Tools/Stirling-PDF/blob/b99fa929e365760c863956bf23c127b098c288e5/app/proprietary/LICENSE), which holds the sign-in code. Its text allows trial, evaluation or minimal use, and asks for a Stirling PDF User License for production use. Read it before a team depends on the server. New to running apps on your own server? [How ox self-hosts your apps](https://deploywithox.com/self-hosting) shows what ox sets up on your server. On this page - [What you need](https://deploywithox.com/blog/self-host-stirling-pdf#what-you-need) - [The steps](https://deploywithox.com/blog/self-host-stirling-pdf#steps) - [The ox.toml, line by line](https://deploywithox.com/blog/self-host-stirling-pdf#config) - [Accounts and the database](https://deploywithox.com/blog/self-host-stirling-pdf#services) - [Updates and backups](https://deploywithox.com/blog/self-host-stirling-pdf#updates) - [Frequently asked questions](https://deploywithox.com/blog/self-host-stirling-pdf#faq) - [Do I need Docker to self-host Stirling-PDF?](https://deploywithox.com/blog/self-host-stirling-pdf#faq-docker) - [How do I set the first Stirling-PDF admin?](https://deploywithox.com/blog/self-host-stirling-pdf#faq-login) - [How much memory does Stirling-PDF use?](https://deploywithox.com/blog/self-host-stirling-pdf#faq-memory) - [Is Stirling-PDF free to self-host?](https://deploywithox.com/blog/self-host-stirling-pdf#faq-license) Written by Saurav Sharma Builds ox. Writes about running apps on servers you own, without Docker and without a platform bill. [Older post How to self-host SurrealDB on your own VPS without Docker](https://deploywithox.com/blog/self-host-surrealdb) [Newer post How to self-host Sonarr without Docker on your own VPS](https://deploywithox.com/blog/self-host-sonarr) ## Keep reading - Guides [Self-Hosted Open-Source Apps Deployed with ox: All 85](https://deploywithox.com/blog/self-hosted-open-source-apps-deployed-with-ox) ox deployed 85 open-source apps from pinned commits on fresh servers, with no Docker. The stacks, services, deploy times and a table of every app. Saurav Sharma Oct 17, 2026 16 min read - Guides [How to self-host AFFiNE on your own VPS without Docker](https://deploywithox.com/blog/self-host-affine) Run AFFiNE v0.27.4 on your own Ubuntu server with ox: Postgres with pgvector, Redis, a Rust and Node build, and an admin made from two variables. Saurav Sharma Oct 17, 2026 12 min read - Guides [How to self-host Akaunting on your own VPS without Docker](https://deploywithox.com/blog/self-host-akaunting) Run Akaunting 3.2.4 on your own Ubuntu server with ox: PHP, MariaDB, a scheduler and the first company and admin made on deploy, from the config that ran. Saurav Sharma Oct 17, 2026 10 min read ## Deploy to a server you own ox sets up systemd, Caddy and PostgreSQL on your Ubuntu server and deploys on every push. Free for your first project. [Sign up with GitHub](https://deploywithox.com/login/github) [Read the docs](https://deploywithox.com/docs)