How to Load Custom Fonts on Heroku With wicked_pdf and wkhtmltopdf
Deploy custom fonts with wicked_pdf and wkhtmltopdf on Heroku, configure asset paths, verify rendering, and fix the failures developers commonly hit.
Direct answer: make the font files available in the deployed Heroku slug, install a wkhtmltopdf binary compatible with your stack, configure wicked_pdf to use that executable, and reference fonts with URLs or paths that the separate wkhtmltopdf process can resolve. Then generate a PDF in the Heroku runtime and inspect the actual glyphs. A font path that works on your laptop does not prove the same file, font registry, or executable exists in production.
wkhtmltopdf runs outside the Rails process. Your Rails application builds HTML, but an external executable loads that HTML, CSS, images, and fonts. Every one of those assets must be available to the executable in the deployed environment.
1. Confirm the rendering components
Before changing CSS, identify the Heroku stack, the wkhtmltopdf distribution and version, and the wicked_pdf version used by the running app.
wicked_pdfis a Rails wrapper; the wkhtmltopdf executable must be installed separately and its path configured. See the wicked_pdf documentation.- Third-party Heroku buildpacks can install binaries or fonts, but Heroku says third-party buildpacks are unsupported. Check the buildpack source, stack compatibility, release history, and binary version before using one. Read Heroku’s buildpack guidance.
- Heroku packages build output into a slug. Files excluded by
.slugignoreare removed before buildpacks run, so a font in your repository may still be absent at runtime. See the slug compiler documentation.
2. Keep font files in the deployed application
Place the font files in a directory that is committed and included in the slug. A common Rails layout is:
app/assets/fonts/
AcmeSans-Regular.woff2
AcmeSans-Bold.woff2
AcmeSans-Regular.ttf
AcmeSans-Bold.ttf
Use the formats your wkhtmltopdf build can load reliably. TrueType files are a useful fallback for older Qt/WebKit builds; WOFF2 support depends on the binary. Keep the family and weight declarations consistent with the font’s internal metadata.
Check that no ignore rule removes the files:
git ls-files app/assets/fonts
cat .slugignore
If you use a font buildpack, treat its file names, install locations, and supported stacks as examples specific to that buildpack. The Heroku Elements listings for system fonts, wkhtmltopdf, and another wkhtmltopdf buildpack are third-party references, not universal Heroku instructions.
3. Install a compatible wkhtmltopdf executable
Add the chosen buildpack in the order required by that buildpack and your Ruby buildpack. Then confirm the executable path in a one-off dyno or release environment. Do not assume a local package manager path such as /usr/local/bin/wkhtmltopdf exists on Heroku.
Configure wicked_pdf explicitly:
# config/initializers/wicked_pdf.rb
WickedPdf.config = {
exe_path: ENV.fetch("WKHTMLTOPDF_PATH", "/app/.buildpack/bin/wkhtmltopdf")
}
The fallback path above is only an example. Set WKHTMLTOPDF_PATH to the path provided by your selected buildpack, or use the path documented by the binary package you installed.
For a Rails controller, a minimal PDF action is:
class InvoicesController < ApplicationController
def show
@invoice = Invoice.find(params[:id])
respond_to do |format|
format.html
format.pdf do
render pdf: "invoice-#{@invoice.id}",
template: "invoices/show",
layout: "pdf"
end
end
end
end
4. Reference fonts in HTML that wkhtmltopdf can resolve
Define the font in the PDF stylesheet. Rails asset helpers can produce fingerprints and paths, but the resulting reference must be usable by the external process. When relative asset URLs fail, use an absolute URL served by the application or a stable absolute file reference supported by your setup. The wicked_pdf asset documentation describes these asset helpers and external references.
/* app/assets/stylesheets/pdf.scss */
@font-face {
font-family: "Acme Sans";
src: url("<%= asset_path('AcmeSans-Regular.ttf') %>") format("truetype");
font-weight: 400;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "Acme Sans";
src: url("<%= asset_path('AcmeSans-Bold.ttf') %>") format("truetype");
font-weight: 700;
font-style: normal;
font-display: block;
}
body {
font-family: "Acme Sans", sans-serif;
}
.invoice-total {
font-weight: 700;
}
If the generated CSS contains a relative URL that wkhtmltopdf cannot resolve, generate an absolute asset URL:
# config/environments/production.rb
config.action_controller.asset_host = ENV["ASSET_HOST"] if ENV["ASSET_HOST"].present?
Then set ASSET_HOST to the HTTPS host that serves your compiled assets. Ensure the dyno can reach that host and that authentication does not block the font request. A CDN URL is another option when your deployment already serves public, immutable assets.
5. Generate and verify a production PDF
- Deploy the application and buildpacks.
- Open a one-off dyno and verify the font file and executable are present using paths appropriate to your buildpack.
- Request a PDF through the deployed Rails endpoint.
- Inspect distinctive glyphs, weights, and punctuation in the PDF. Check both a regular and bold face.
- If possible, extract the PDF’s font metadata with a PDF inspection tool and compare it with the intended family.
Successful deployment is not proof that wkhtmltopdf used the font. The decisive check is the rendered document produced by the production executable.
6. Common failure modes
| Symptom | Likely cause | Fix |
|---|---|---|
| PDF uses a fallback font | Font URL is unreachable, file is missing from the slug, or the format is unsupported by the binary. | Inspect the deployed file, use an absolute resolvable URL, try TTF, and verify the CSS family and weight names. |
command not found: wkhtmltopdf |
The buildpack was not added, ran in the wrong order, or does not support the current stack. | Inspect build output, confirm the stack, and set WKHTMLTOPDF_PATH to the installed executable. |
| wicked_pdf cannot find the executable | exe_path points to a local development path. |
Configure the production path through an environment variable and log the resolved path at boot. |
| Fonts work locally but not on Heroku | Local system fonts or paths are unavailable in the slug. | Commit the files or install them in a compatible build step; do not rely on a developer workstation. |
| Font requests return 404 | Fingerprinting changed the URL, the asset host is wrong, or the external process cannot resolve a relative URL. | Inspect the final HTML/CSS, use asset_path, and switch to an absolute HTTPS asset URL. |
| PDF is blank or times out | HTML, CSS, or fonts depend on a blocked host, slow request, JavaScript, or an incompatible binary. | Remove unnecessary dependencies, confirm network access, increase the render timeout carefully, and test the production binary. |
| Only bold or only regular text falls back | The declared weight does not match the font face or the corresponding file is absent. | Declare each face with the correct font-weight and verify both files are deployed. |
7. Buildpack and app-managed font approaches
You can install fonts through a font buildpack or keep the files with the application and expose them through the asset pipeline. Compare them on these points:
- Stack compatibility: confirm the buildpack supports your current Heroku stack and architecture.
- Runtime presence: verify the exact files survive
.slugignoreand are readable by the dyno user. - Binary alignment: make sure the wkhtmltopdf executable and wicked_pdf configuration refer to the same installation.
- Asset resolution: test the final HTML and CSS as loaded by the external process.
- Maintenance: third-party buildpacks are unsupported by Heroku, so you own compatibility checks and upgrades.
Heroku’s Buildpack API documentation explains how build output is added to the slug. Use it to understand what your selected buildpack actually places in the runtime.
8. Performance, reliability, and cost considerations
- Font files are loaded during rendering, so large files and multiple weights increase request and render time. Include only the faces and character coverage your PDFs need.
- Self-hosting the files in the slug avoids a dependency on an external font host, while an asset host can simplify absolute URL resolution. Either way, test from the dyno network.
- Pin the wkhtmltopdf distribution and buildpack revision where practical. A binary change can alter CSS, font, or JavaScript behavior.
- Warm and cold dynos can behave differently when assets or external hosts are slow. Exercise PDF generation after deploy and during a dyno restart.
- Measure queue time and render time separately if PDF generation runs in a background job. Set job retries so a transient load failure does not create duplicate records or downloads.
- Heroku slug size and build time are affected by bundled fonts and binaries. Remove unused font families and build artifacts.
9. A repeatable deployment checklist
- [ ] The selected wkhtmltopdf build supports the Heroku stack.
- [ ] The executable path is configured through
WKHTMLTOPDF_PATHor the documented wicked_pdf setting. - [ ] Font files are committed or installed by a build step.
- [ ]
.slugignoredoes not remove the font directory. - [ ] The final CSS uses the intended family, style, and weight.
- [ ] Every font URL is resolvable by the external wkhtmltopdf process.
- [ ] A production PDF shows the intended glyphs and weights.
- [ ] A deploy, dyno restart, and background-job run have all been checked.
Or skip the browser setup
If you need a rendered page image or PDF without maintaining a browser, font, and wkhtmltopdf runtime, ScreenshotNeo provides a single HTTP request for screenshots and PDFs. Its capture flow accepts cookie and consent banners before the shot and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the page verdict and billing result in headers. An MCP server also lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API documentation for all options. This cURL request captures a PDF-capable page endpoint as an image response:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots, and every feature is on every plan. Create a free ScreenshotNeo account.
FAQ
Do I need to install fonts globally on Heroku?
No. You can ship the files with the app and reference them in CSS, provided wkhtmltopdf can resolve and read them. A system-font buildpack is an alternative when your chosen stack and buildpack support it.
Why does a valid Rails asset path still fail?
Rails generated the path in one process, while wkhtmltopdf fetched it in another. The external process may not understand the relative path, may lack network access, or may receive a different host. Inspect the final URL from the rendered HTML.
Is one wkhtmltopdf build correct for every Heroku stack?
No. Binary and buildpack compatibility is stack-specific. Verify the current stack and the exact distribution you deploy.
How can I prove the intended font was embedded?
Inspect distinctive glyphs visually and, where available, inspect the PDF’s font metadata. Also confirm the deployed font file, CSS URL, and executable version.


