{"id":169486,"date":"2026-06-22T22:09:42","date_gmt":"2026-06-22T19:09:42","guid":{"rendered":"https:\/\/computingforgeeks.com\/?p=169486"},"modified":"2026-06-22T22:09:42","modified_gmt":"2026-06-22T19:09:42","slug":"self-host-stirling-pdf-docker","status":"publish","type":"post","link":"https:\/\/computingforgeeks.com\/self-host-stirling-pdf-docker\/","title":{"rendered":"Install and Self-Host Stirling-PDF with Docker"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Stirling-PDF is a self-hosted PDF toolbox that does the jobs people normally hand to a random website: merge, split, compress, convert, sign, redact, and run OCR on scanned documents. The difference is that your files never leave your own server. For invoices, contracts, ID scans, and anything else you would rather not upload to a stranger, that one fact is the whole reason to run it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide sets it up the practical way: Docker Compose for the app, an Nginx reverse proxy with a real Let&#8217;s Encrypt certificate in front, and optional login so it is not wide open. After that you get a tour of the everyday tools and the no-code pipeline feature that chains several steps into one click. By the end you will have a private &#8220;Adobe Acrobat in a browser tab&#8221; running on your own box.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Ran through this with Docker on both Ubuntu 24.04 and Rocky Linux 10 in June 2026, so the steps cover Debian and RHEL based systems alike. Stirling-PDF 2.13.1 and Docker 29.6.0 were the versions under test.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A Linux server running a Debian or Ubuntu release, or a RHEL-based one such as Rocky Linux, AlmaLinux, or RHEL, with <a href=\"https:\/\/computingforgeeks.com\/install-docker-compose-ubuntu-2604\/\">Docker and the Compose plugin<\/a> (installed below if you do not have them).<\/li>\n<li>A domain name with an A record pointing at the server, and port 80 reachable, so Let&#8217;s Encrypt can validate over HTTP. Any DNS provider works.<\/li>\n<li>Sudo access and a few minutes.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">On sizing: the app itself is light at idle, but two features are memory-hungry. Office conversions spin up a headless LibreOffice, and OCR runs tesseract plus Ghostscript over every page. The working number is driven by those, not by idle usage. Two vCPUs and 4 GB of RAM handle a single user converting and OCRing comfortably; a busy multi-user instance that processes large scans wants 8 GB and room to grow. The container caps its JVM at 70% of available RAM, so give it a host where that 70% is a sensible figure. The lab for this guide ran 2 vCPUs and 4 GB, which is a floor for following along, not a recommendation for a shared production box.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Set reusable shell variables<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A handful of values show up in many commands below. Set them once at the top of your SSH session and paste the rest as-is. Swap in your real domain and email:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>export SITE_DOMAIN=\"stirling.example.com\"\nexport ADMIN_EMAIL=\"you@example.com\"\nexport APP_DIR=\"\/opt\/stirling-pdf\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">These hold only for the current shell. If you reconnect or jump into a root shell, export them again. Confirm they are set before going further:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>echo \"Domain: ${SITE_DOMAIN}\"\necho \"Dir:    ${APP_DIR}\"<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Install Docker and the Compose plugin<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Skip this step if Docker is already running. Pick the block for your distribution; both pull the engine and the Compose plugin straight from Docker&#8217;s own repository.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Debian and Ubuntu<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The repository line keys off your distribution ID and codename, so one block covers Debian and every current Ubuntu release:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>sudo apt update\nsudo apt install -y ca-certificates curl gnupg\nsudo install -m 0755 -d \/etc\/apt\/keyrings\n. \/etc\/os-release\ncurl -fsSL https:\/\/download.docker.com\/linux\/$ID\/gpg | sudo gpg --dearmor -o \/etc\/apt\/keyrings\/docker.gpg\nsudo chmod a+r \/etc\/apt\/keyrings\/docker.gpg\necho \"deb [arch=$(dpkg --print-architecture) signed-by=\/etc\/apt\/keyrings\/docker.gpg] https:\/\/download.docker.com\/linux\/$ID $VERSION_CODENAME stable\" | sudo tee \/etc\/apt\/sources.list.d\/docker.list\nsudo apt update\nsudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">RHEL-based systems (Rocky, AlmaLinux, RHEL)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Add Docker&#8217;s CentOS repository, which serves the whole RHEL family, then install the same set of packages. On genuine RHEL you can swap <code>centos<\/code> for <code>rhel<\/code> in the URL; on Rocky and AlmaLinux the CentOS repo is the right one:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>sudo dnf -y install dnf-plugins-core\nsudo dnf config-manager --add-repo https:\/\/download.docker.com\/linux\/centos\/docker-ce.repo\nsudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">On any distribution, start the service and enable it at boot:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>sudo systemctl enable --now docker<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Confirm the engine and the Compose plugin are both present:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>docker --version\ndocker compose version<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">One gotcha shows up only on slim RHEL-based cloud images: the daemon refuses to start with an iptables error about a missing kernel module, because the image shipped <code>kernel-core<\/code> without the netfilter modules Docker&#8217;s bridge networking needs. Pull the full module set and reboot into it, after which Docker starts cleanly:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>sudo dnf install -y kernel-modules\nsudo reboot<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A standard Rocky Linux or AlmaLinux server install already carries those modules, so this only bites trimmed-down cloud images, never a full install.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Create the docker-compose file<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Create the project directory and open a Compose file:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>sudo mkdir -p \"${APP_DIR}\"\nsudo vim \"${APP_DIR}\/docker-compose.yml\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Add the following. Login is off for now so you can confirm the app works before locking it down, and the four volumes keep your settings, logs, OCR language data, and saved pipelines on the host:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>services:\n  stirling-pdf:\n    image: stirlingtools\/stirling-pdf:latest\n    container_name: stirling-pdf\n    ports:\n      - \"127.0.0.1:8080:8080\"\n    volumes:\n      - .\/stirling-data\/tessdata:\/usr\/share\/tessdata\n      - .\/stirling-data\/configs:\/configs\n      - .\/stirling-data\/logs:\/logs\n      - .\/stirling-data\/pipeline:\/pipeline\n    environment:\n      - SECURITY_ENABLELOGIN=false\n      - LANGS=en_GB\n    restart: unless-stopped<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Binding the port to <code>127.0.0.1<\/code> instead of <code>0.0.0.0<\/code> means the container is only reachable from the host. Nginx will be the only thing exposed to the internet, which is exactly what you want once TLS is in place.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Start Stirling-PDF and verify<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Pull the image and bring the stack up:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>cd \"${APP_DIR}\"\nsudo docker compose up -d<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The first start takes a minute while it initializes LibreOffice and the OCR engine. Check the container and ask the app for its status:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>sudo docker compose ps\ncurl -s http:\/\/localhost:8080\/api\/v1\/info\/status<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A healthy container and a status of <code>UP<\/code> mean it is ready:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1840\" height=\"672\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-stirling-pdf-docker-status-verify.png\" alt=\"Stirling-PDF 2.13.1 running healthy in Docker, verified via docker compose ps\" class=\"wp-image-169503\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-stirling-pdf-docker-status-verify.png 1840w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-stirling-pdf-docker-status-verify-300x110.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-stirling-pdf-docker-status-verify-1024x374.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-stirling-pdf-docker-status-verify-768x280.png 768w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-stirling-pdf-docker-status-verify-1536x561.png 1536w\" sizes=\"auto, (max-width: 1840px) 100vw, 1840px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">If the status call returns nothing for the first thirty seconds, that is normal on a cold start. Give it another moment and try again. If it ever returns &#8220;This endpoint is disabled,&#8221; usage metrics are turned off on your instance; the <code>docker compose ps<\/code> health column is the reliable readiness signal in that case.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Put Nginx and HTTPS in front<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Serving a PDF tool over plain HTTP is a bad idea, since every file and any login travel in the clear. Nginx terminates TLS and proxies to the container, with a free <a href=\"https:\/\/computingforgeeks.com\/letsencrypt-ssl-certificate-linux\/\">Let&#8217;s Encrypt certificate<\/a> doing the encryption. Install Nginx and Certbot. On Debian and Ubuntu:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>sudo apt install -y nginx certbot python3-certbot-nginx<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">On RHEL-based systems, Nginx is in the default repos and Certbot lives in EPEL:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>sudo dnf install -y nginx epel-release\nsudo dnf install -y certbot python3-certbot-nginx\nsudo systemctl enable --now nginx<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Create a server block. Dropping it in <code>conf.d<\/code> keeps the path identical across distributions, since both the Debian and RHEL packages include that directory by default:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>sudo vim \/etc\/nginx\/conf.d\/stirling.conf<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Use a literal placeholder for the hostname, since an Nginx file is not a shell and will not expand your variable. A larger body size lets you push big PDFs through the proxy:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>server {\n    listen 80;\n    server_name SITE_DOMAIN_HERE;\n    client_max_body_size 200M;\n\n    location \/ {\n        proxy_pass http:\/\/127.0.0.1:8080;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Swap the placeholder for your real domain, test the config, and reload:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>sudo sed -i \"s\/SITE_DOMAIN_HERE\/${SITE_DOMAIN}\/\" \/etc\/nginx\/conf.d\/stirling.conf\nsudo nginx -t\nsudo systemctl reload nginx<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Open ports 80 and 443 in the firewall. On Debian and Ubuntu that is ufw:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>sudo ufw allow 'Nginx Full'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">On RHEL-based systems it is firewalld:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>sudo firewall-cmd --permanent --add-service=http --add-service=https\nsudo firewall-cmd --reload<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Now issue the certificate. The Nginx plugin validates over port 80 and rewrites the server block to serve HTTPS with an automatic HTTP redirect:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>sudo certbot --nginx -d \"${SITE_DOMAIN}\" --non-interactive --agree-tos --redirect -m \"${ADMIN_EMAIL}\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Open <code>https:\/\/your-domain<\/code> in a browser and the Stirling-PDF workspace loads, with the full tool list down the right-hand side. This is the same catalog you will use for the rest of the guide:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-stirling-pdf-dashboard-tools-ubuntu.png\" alt=\"Stirling-PDF dashboard showing 50+ PDF tools in the browser\" class=\"wp-image-169485\" title=\"\"><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">A note on SELinux<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">On RHEL-based systems with SELinux in enforcing mode, this proxy works with no extra steps, because port 8080 is already a recognized HTTP port type that Nginx is allowed to reach. You only need to flip a boolean if you move Stirling-PDF to a non-HTTP port, in which case run <code>sudo setsebool -P httpd_can_network_connect 1<\/code> so Nginx can connect to the backend.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">If your server has no public port 80<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">On a home server behind NAT, a private LAN box, or when you want a wildcard certificate, the HTTP challenge will not reach you. Use the DNS challenge instead. Certbot has plugins for most providers (Cloudflare, Route 53, DigitalOcean, Google Cloud DNS, Linode, OVH, and others); the Cloudflare one looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>sudo apt install -y python3-certbot-dns-cloudflare\necho \"dns_cloudflare_api_token = your-scoped-token\" | sudo tee \/etc\/letsencrypt\/cloudflare.ini\nsudo chmod 600 \/etc\/letsencrypt\/cloudflare.ini\nsudo certbot certonly --dns-cloudflare \\\n  --dns-cloudflare-credentials \/etc\/letsencrypt\/cloudflare.ini \\\n  -d \"${SITE_DOMAIN}\" --non-interactive --agree-tos -m \"${ADMIN_EMAIL}\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Substitute your provider&#8217;s plugin if you are not on Cloudflare. With <code>certonly<\/code> you point the <code>ssl_certificate<\/code> and <code>ssl_certificate_key<\/code> directives at the files under <code>\/etc\/letsencrypt\/live\/your-domain\/<\/code> by hand and add a <code>listen 443 ssl;<\/code> block to the Nginx file.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: Turn on login<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">An exposed PDF tool with no authentication is an open invitation. Edit the Compose file to enable login and set your own first-run credentials:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>sudo vim \"${APP_DIR}\/docker-compose.yml\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Change the environment block to switch login on and seed an admin account:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>    environment:\n      - SECURITY_ENABLELOGIN=true\n      - SECURITY_INITIALLOGIN_USERNAME=admin\n      - SECURITY_INITIALLOGIN_PASSWORD=ChangeMe#Strong2026\n      - LANGS=en_GB<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Recreate the container so the new settings take effect:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>cd \"${APP_DIR}\"\nsudo docker compose up -d<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Reload the page and you now get a login screen. If you did not set the initial credentials above, Stirling-PDF falls back to <code>admin<\/code> \/ <code>stirling<\/code> and prints the defaults right on the form so you are never locked out. Either way, change the password from the user menu after the first login:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-stirling-pdf-login-page-ubuntu.png\" alt=\"Stirling-PDF login page with default admin credentials\" class=\"wp-image-169478\" title=\"\"><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">For a team, this same login layer supports per-user accounts and OAuth single sign-on, so you are not stuck sharing one password. Worth a look once you outgrow the single admin.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Use the everyday PDF tools<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Everything starts the same way: add files, pick a tool, run it, download the result. Click <strong>Upload from computer<\/strong> or drag a few PDFs in, and they show up in the workspace with page counts and thumbnails:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-stirling-pdf-files-uploaded-ubuntu.png\" alt=\"Two PDFs uploaded to the Stirling-PDF workspace\" class=\"wp-image-169480\" title=\"\"><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Merge several PDFs into one<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Select the files you want, open <strong>Merge<\/strong>, and drag the rows into the order you want them stitched. The settings let you drop digital signatures or build a table of contents from the source files. A two-page invoice and a three-page report combined into a single five-page document, exactly as ordered:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-stirling-pdf-merge-pdfs-ubuntu.png\" alt=\"Merging two PDFs in Stirling-PDF\" class=\"wp-image-169482\" title=\"\"><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Splitting is the mirror image. The <strong>Split<\/strong> tool takes a page number or a range and hands back a zip with each section as its own file, which is handy for pulling one chapter out of a long scan.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Shrink a bloated PDF<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Open <strong>Compress<\/strong> and you choose between targeting a quality level or an exact output size. There are extra knobs for grayscale conversion and web-optimized linearization when you need to squeeze harder. On the test documents the quality setting trimmed a merged file by roughly a third with no visible change:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-stirling-pdf-compress-pdf-ubuntu.png\" alt=\"Compressing a PDF with Stirling-PDF quality settings\" class=\"wp-image-169483\" title=\"\"><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Convert to and from PDF<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The <strong>Convert<\/strong> group is the longest list in the app. PDF to Word, Excel, images, HTML, and plain text go one way; images, Office files, Markdown, and HTML come back the other. The Office conversions are where that LibreOffice process from the prerequisites earns its keep, so the first conversion after a restart is a little slow while it warms up. A merged PDF turned into a valid <code>.docx<\/code> in a couple of seconds on the lab box.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">OCR a scanned document<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This is the one that earns Stirling-PDF a permanent spot. <strong>OCR \/ Cleanup scans<\/strong> takes an image-only PDF, recognizes the text with tesseract, and writes a searchable text layer back into the file. Pick your languages, choose whether to skip pages that already have text, and run it:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-stirling-pdf-ocr-scanned-pdf-ubuntu.png\" alt=\"Running OCR on a scanned PDF in Stirling-PDF\" class=\"wp-image-169481\" title=\"\"><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Before OCR, a scanned PDF has zero selectable text. After, the same file is fully searchable. The English, German, French, Portuguese, Chinese, and orientation data ship in the image; for anything else, drop the matching tesseract <code>.traineddata<\/code> into the <code>tessdata<\/code> volume.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One real gotcha worth flagging: on some virtualized hosts OCR fails with a NumPy error about &#8220;baseline optimizations.&#8221; That happens when the virtual machine advertises a stripped-down CPU model that hides instruction sets the OCR renderer expects. The fix is to give the VM a host-passthrough CPU type (on Proxmox, set the CPU to <code>host<\/code>) and restart. Bare-metal and most cloud instances never see this.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Automate multi-step jobs with pipelines<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The single tools are useful, but the <strong>Automate<\/strong> feature is what turns Stirling-PDF into infrastructure. A pipeline chains operations so a file runs through several steps in one go, no code involved. It ships with ready-made workflows like sanitizing a PDF before publishing, preparing documents for email, and processing images, and you can build your own:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/06\/wm-stirling-pdf-pipeline-automation-ubuntu.png\" alt=\"Stirling-PDF Automate pipeline with suggested workflows\" class=\"wp-image-169484\" title=\"\"><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">A simple example is rotate then compress: every file dropped in gets straightened and shrunk in a single pass, with the output named to a template you set. Because each step maps to a REST endpoint under <code>\/api\/v1\/<\/code>, the same logic is callable from a script. That is the bridge from &#8220;click the tool&#8221; to &#8220;watch a folder and process everything that lands in it,&#8221; which is where Stirling-PDF starts replacing a pile of one-off scripts. If you already run containers, it sits naturally next to a manager like <a href=\"https:\/\/computingforgeeks.com\/install-portainer-ubuntu-2604\/\">Portainer<\/a> and a file host like <a href=\"https:\/\/computingforgeeks.com\/install-nextcloud-ubuntu-2604\/\">Nextcloud<\/a> in the same self-hosted stack.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Back up and restore your Stirling-PDF data<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">All the state that matters lives in the <code>stirling-data<\/code> folder next to your Compose file: user accounts and settings in <code>configs<\/code>, your saved pipelines in <code>pipeline<\/code>, and any extra OCR languages in <code>tessdata<\/code>. The uploaded PDFs themselves are processed in memory and not stored, so a backup is small and quick. Stop the stack, archive the folder, and start again:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>cd \"${APP_DIR}\"\nsudo docker compose down\nsudo tar czf \"stirling-backup-$(date +%Y%m%d).tar.gz\" stirling-data\nsudo docker compose up -d<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Restoring on a new host is the reverse: install Docker, copy your Compose file and the archive over, extract it, and bring the stack up. Because the certificate is managed by Certbot and the app data is one tarball, moving the whole thing to a bigger server is a five-minute job. Keep that archive on whatever you use for the rest of your backups, behind a <a href=\"https:\/\/computingforgeeks.com\/configure-ufw-firewall-ubuntu-2604\/\">properly configured firewall<\/a>, and your private PDF suite survives a rebuild without losing a single setting.<\/p>\n\n","protected":false},"excerpt":{"rendered":"<p>Stirling-PDF is a self-hosted PDF toolbox that does the jobs people normally hand to a random website: merge, split, compress, convert, sign, redact, and run OCR on scanned documents. The difference is that your files never leave your own server. For invoices, contracts, ID scans, and anything else you would rather not upload to a &#8230; <a title=\"Install and Self-Host Stirling-PDF with Docker\" class=\"read-more\" href=\"https:\/\/computingforgeeks.com\/self-host-stirling-pdf-docker\/\" aria-label=\"Read more about Install and Self-Host Stirling-PDF with Docker\">Read more<\/a><\/p>\n","protected":false},"author":15,"featured_media":169508,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[316],"tags":[218,217,2254],"cfg_series":[39823],"class_list":["post-169486","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-containers","tag-containers","tag-docker","tag-ubuntu","cfg_series-ubuntu-2604-self-hosted"],"_links":{"self":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/169486","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/users\/15"}],"replies":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/comments?post=169486"}],"version-history":[{"count":3,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/169486\/revisions"}],"predecessor-version":[{"id":169509,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/169486\/revisions\/169509"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media\/169508"}],"wp:attachment":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media?parent=169486"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/categories?post=169486"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/tags?post=169486"},{"taxonomy":"cfg_series","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/cfg_series?post=169486"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}