Rocky Linux 10, AlmaLinux 10 and RHEL 10 do not package wkhtmltopdf, and EPEL 10 does not carry it either. The project is discontinued, so no EL10 build will ever appear. The EL9 package from the archived packaging repository still installs and works, though, and that is what this guide uses.
To install wkhtmltopdf on Rocky Linux 10 or AlmaLinux 10, download the almalinux9 RPM and let dnf pull its font dependencies from AppStream. The sections below cover that install, local file access, page numbers, wkhtmltoimage and a batch script. They also cover the two PATH traps that break it under sudo and cron. Everything was run on Rocky Linux 10 with SELinux enforcing in October 2026, as a regular user. AlmaLinux 10 and RHEL 10 use the same AppStream packages, and RHEL needs an active subscription to reach them.
Before you install: the project is archived
The main repository is archived, and the packaging releases stop at a 2023 rebuild. The project status page warns against rendering untrusted HTML, and CVE-2022-35583 (an SSRF rated 9.8) will stay open forever.
Use it to keep existing jobs alive. For new work, our comparison of wkhtmltopdf alternatives benchmarks eleven maintained replacements, and every one we recommend was tested on Rocky Linux 10. On Ubuntu or Debian, the Ubuntu and Debian wkhtmltopdf guide covers the .deb packages instead.
Check what dnf offers
Nothing, even with EPEL and CRB enabled. Confirm it on your host:
dnf info wkhtmltopdf
dnf has no package by that name:
Error: No matching Packages to list
Don’t go looking for an almalinux10 RPM either. The packaging repo stopped before EL10 existed, and the URL returns curl: (22) The requested URL returned error: 404. The EL9 build runs on EL10 because it bundles its own patched Qt instead of linking against the system one.
Install wkhtmltopdf on Rocky Linux 10
Detect the latest release tag and the CPU architecture so the download line never needs editing. The fallback tag only kicks in if the GitHub API rate limit returns nothing:
ARCH=$(uname -m)
WK_TAG=$(curl -s https://api.github.com/repos/wkhtmltopdf/packaging/releases/latest | grep -oP '"tag_name": "\K[^"]+')
WK_TAG=${WK_TAG:-0.12.6.1-3} # pin: archived repo, last tag https://github.com/wkhtmltopdf/packaging/releases
echo "$WK_TAG $ARCH"
Both values should come back populated:
0.12.6.1-3 x86_64
Download the EL9 package. An aarch64 build exists as well, and the variable selects it on ARM servers:
curl -fsSLO "https://github.com/wkhtmltopdf/packaging/releases/download/${WK_TAG}/wkhtmltox-${WK_TAG}.almalinux9.${ARCH}.rpm"
ls -lh wkhtmltox-*.rpm
The RPM is about 16 MB:
-rw-r--r--. 1 josphat josphat 16M Oct 8 10:52 wkhtmltox-0.12.6.1-3.almalinux9.x86_64.rpm
Install it with dnf, not rpm, so the dependencies resolve:
sudo dnf install -y ./wkhtmltox-${WK_TAG}.almalinux9.${ARCH}.rpm
On our lab host dnf only had to add five font and font-tool packages, because Chrome had already pulled in the X11 and image libraries:
Installing:
wkhtmltox x86_64 1:0.12.6.1-3.almalinux9 @commandline 15 M
Installing dependencies:
libfontenc x86_64 1.1.7-5.el10 appstream 32 k
mkfontscale x86_64 1.2.2-8.el10 appstream 32 k
ttmkfdir x86_64 3.0.9-72.el10 appstream 56 k
xorg-x11-fonts-75dpi noarch 7.5-40.el10 appstream 2.8 M
xorg-x11-fonts-Type1 noarch 7.5-40.el10 appstream 505 k
Transaction Summary
================================================================================
Install 6 Packages
A minimal server needs more. The same command in a clean Rocky Linux 10 container installed 26 packages, including libX11, libXrender, fontconfig and libjpeg-turbo, all from BaseOS and AppStream.
Verify the binaries. Run this as your normal user, not through sudo (the next section explains why):
wkhtmltopdf --version
which wkhtmltopdf wkhtmltoimage
The patched build reports itself clearly:
wkhtmltopdf 0.12.6.1 (with patched qt)
/usr/local/bin/wkhtmltopdf
/usr/local/bin/wkhtmltoimage
SELinux stays enforcing. Rendering as a normal user produced no AVC denials for wkhtmltopdf (sudo ausearch --input-logs -m avc -ts today listed none), because the binaries run unconfined as the calling user and only read and write files the user owns.
Error: sudo: wkhtmltopdf: command not found
The package installs to /usr/local/bin. A normal SSH login on Rocky Linux 10 has that directory on its PATH, but sudo replaces PATH with secure_path from /etc/sudoers, which is /sbin:/bin:/usr/sbin:/usr/bin. Running it with sudo fails:
sudo wkhtmltopdf --version
The binary is installed, but sudo cannot see it:
sudo: wkhtmltopdf: command not found
Call it by full path when root really is required, or better, do not render as root at all:
sudo /usr/local/bin/wkhtmltopdf --version
That works. The same trap hits any process manager that starts with a minimal PATH. Cron is the usual victim, covered further down.
Convert HTML to PDF
Create a working directory and a small invoice that references a local logo, which is where most real templates trip:
mkdir -p ~/invoices && cd ~/invoices
vim invoice.html
Paste the template and save it. Put any PNG named logo.png in the same directory:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Invoice 2026-118</title>
<style>
body { font-family: "DejaVu Sans", sans-serif; padding: 24px; }
h1 { color: #2c3e50; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
th { background: #f4f4f4; }
</style>
</head>
<body>
<img src="logo.png" width="64" alt="logo">
<h1>Invoice 2026-118</h1>
<p>Billed to: Example Corp</p>
<table>
<tr><th>Item</th><th>Price</th></tr>
<tr><td>VPS hosting (12 months)</td><td>$240.00</td></tr>
<tr><td>Managed backups</td><td>$60.00</td></tr>
<tr><td><strong>Total</strong></td><td><strong>$300.00</strong></td></tr>
</table>
</body>
</html>
Convert it with the plain two-argument form first:
wkhtmltopdf -q invoice.html invoice.pdf; echo "exit=$?"
The plain conversion fails on the logo:
Exit with code 1 due to network error: ProtocolUnknownError
exit=1
wkhtmltopdf has blocked file:// resources by default since 0.12.6. Allow them for templates you trust:
wkhtmltopdf -q --enable-local-file-access invoice.html invoice.pdf
pdfimages -list invoice.pdf
pdfinfo invoice.pdf | grep -E "Pages|Page size|Producer"
The logo is embedded and the page is A4:
page num type width height color comp bpc enc interp object ID x-ppi y-ppi size ratio
--------------------------------------------------------------------------------------------
1 0 image 128 128 rgb 3 8 image no 6 0 240 240 291B 0.6%
Producer: Qt 4.8.7
Pages: 1
Page size: 595 x 842 pts (A4)
pdfimages and pdfinfo come from poppler-utils, which is in AppStream if your host lacks them.
Set page size, margins, header and footer
Headers and footers accept placeholders: [page], [topage], [date], [title] and a few more. This run adds the date on the right of the header and page numbers in the footer:
wkhtmltopdf -q --enable-local-file-access \
--page-size A4 --margin-top 20mm --margin-bottom 20mm \
--title "Invoice 2026-118" \
--header-right "[date]" \
--footer-center "Page [page] of [topage]" --footer-font-size 8 \
invoice.html invoice-styled.pdf
Check the metadata and the rendered header and footer text:
pdfinfo invoice-styled.pdf | grep -E "Title|Pages"
pdftotext invoice-styled.pdf - | grep -E "Page 1|/"
The date follows the short format of the host locale:
Title: Invoice 2026-118
Pages: 1
10/8/26
Page 1 of 1
Landscape and US Letter are two more flags:
wkhtmltopdf -q --orientation Landscape --page-size Letter --enable-local-file-access invoice.html landscape.pdf
pdfinfo landscape.pdf | grep "Page size"
Width and height swap as expected:
Page size: 792 x 612 pts (letter)
Render URLs and images
A URL works anywhere a file does:
wkhtmltopdf -q https://example.com example.pdf
pdftotext example.pdf - | head -2
The text came through:
This domain is for use in documentation examples without
needing permission. This is not a service; avoid relying on
wkhtmltoimage writes JPG or PNG. JPG at quality 80 is a good default for email previews:
wkhtmltoimage -q --enable-local-file-access --format jpg --quality 80 --width 800 invoice.html invoice.jpg
file invoice.jpg
The image is 800 pixels wide and as tall as the content:
invoice.jpg: JPEG image data, JFIF standard 1.01, resolution (DPI), density 75x75, segment length 16, baseline, precision 8, 800x391, components 3
Modern single-page apps will not render correctly. The engine dates from 2012 and does not understand current JavaScript. Use headless Chrome with print to PDF or Puppeteer for those pages. Both were tested on Rocky Linux 10.
Batch convert a directory with a script
Most real use is a loop over generated HTML. One detail matters here: by default wkhtmltopdf only warns about a missing image and still exits 0, so a broken invoice ships without anyone noticing. The script below sets --load-media-error-handling abort to turn that into a failure, and nullglob keeps an empty directory from being reported as a failure. Create it:
sudo vim /usr/local/bin/html2pdf-batch
Add the following. It calls the binary by full path, so it also works from cron and systemd:
#!/usr/bin/env bash
# Convert every .html file in a directory to a PDF next to it.
set -uo pipefail
shopt -s nullglob
dir=${1:?usage: html2pdf-batch DIR}
rc=0
for f in "$dir"/*.html; do
if /usr/local/bin/wkhtmltopdf -q --enable-local-file-access --load-media-error-handling abort "$f" "${f%.html}.pdf"; then
echo "ok $f"
else
echo "fail $f" >&2
rc=1
fi
done
exit $rc
Make it executable:
sudo chmod 0755 /usr/local/bin/html2pdf-batch
Build a test directory with the good invoice and a copy that points at a logo that does not exist:
mkdir -p ~/batch
cp ~/invoices/invoice.html ~/invoices/logo.png ~/batch/
sed 's/logo.png/missing-logo.png/' ~/batch/invoice.html > ~/batch/broken.html
Run the script against it:
html2pdf-batch ~/batch; echo "exit=$?"
The broken file is reported and the script exits non-zero, which is what a cron wrapper or CI job needs:
Exit with code 1 due to network error: ContentNotFoundError
fail /home/josphat/batch/broken.html
ok /home/josphat/batch/invoice.html
exit=1
wkhtmltopdf still writes a partial broken.pdf next to the source; a clean re-run recreated it. Delete outputs for failed inputs if anything downstream picks up every PDF in the directory.
Run wkhtmltopdf from cron
Cron on Rocky Linux 10 starts jobs with PATH=/usr/bin:/bin:/usr/sbin:/sbin (captured with an env job), and /usr/local/bin is not in it. To see the effect, open a user crontab:
crontab -e
Add two test entries, one with the bare command name and one with the full path:
* * * * * wkhtmltopdf --version > /tmp/cron-wk-plain.log 2>&1
* * * * * /usr/local/bin/wkhtmltopdf --version > /tmp/cron-wk-full.log 2>&1
After one minute the two logs told the story:
/bin/sh: line 1: wkhtmltopdf: command not found
wkhtmltopdf 0.12.6.1 (with patched qt)
Delete the two test lines with crontab -e now, since they fire every minute. Use the full path in real crontab entries, or set PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin at the top of the crontab. The batch script above already uses the full path, so a nightly job is one line: 30 2 * * * /usr/local/bin/html2pdf-batch /srv/invoices/out.
Production notes for a frozen renderer
Run the renderer as a dedicated unprivileged user, never root. Feed it only HTML your own code generated. Keep --enable-local-file-access off for anything that contains user input, since an iframe can then read local files into the PDF. Treat the 0.12.6 SSRF as permanent and keep the host away from internal-only services if the HTML can carry URLs. Plan the exit: WeasyPrint for templated documents, Gotenberg for a shared PDF service, and Playwright for JavaScript-heavy pages all ran on Rocky Linux 10 with SELinux enforcing in our tests.