Skip to content

Troubleshooting

Which version am I running?

Once you are signed in, the foot of every page says Powered by Postulo X.Y.Z, linked to that release's notes; Server settings → Overview says it in full beside the Python and Django versions; and GET /healthz returns it as version, which is the one to quote in a bug report and the one monitoring can watch to see an upgrade happen.

Reading the log

Server settings → Logs shows what the instance has been saying, newest first, filtered by level, by which part of Postulo said it, or by a word in the message. It is usually the quickest answer to "the notification did not arrive" or "the document was not filed": the failure is recorded with the connection it happened to and the reason the other end gave.

Everything on that page is also on the console, so the container's output gives you the same records if you have a terminal to hand (docker compose -f docker/compose.yml logs postulo). Every line written while answering a request ends with [request <id>], the id the response carried in X-Request-ID and gunicorn's access line ends with; search the log for it and you have everything that one request said. Set POSTULO_LOG_FORMAT=json if something parses your console rather than a person reading it.

Two things worth knowing. Records name people, companies and applications in the course of explaining what failed, so treat the page as you would the rest of the instance before pasting any of it into a bug report. And the file is rotated by size — five megabytes a file and three rotations kept, so about twenty in total by default — so it cannot fill a disk, which also means a problem from last month may have scrolled off.

Up to and including 0.3.0 far less than that was kept. The web workers, the scheduler and the worker all write the one file, and each rotated it on its own word: one rotation became one per process, the older files were pushed off the end, and what was left was one full file and a few fragments, with records out of order between them. If a log from then looks as if it starts too late, that is why.

If you keep logs centrally, the same records can be served at /logs for a collector to scrape. It is off by default and needs a token; see Hardening before turning it on.

If you keep graphs, /metrics serves Prometheus metrics — what exists, what is waiting, what has failed, and whether the database and migrations are healthy. Also off by default, and it carries nothing about anybody.

"No PDF backend is usable"

WeasyPrint is installed with Postulo, so this almost always means its system libraries are missing rather than the package:

sudo apt install libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0     # Debian and Ubuntu

On Windows those libraries are impractical; use the fallback renderer instead:

uv sync --extra chromium
uv run playwright install chromium

If you have installed one and still see the message, check POSTULO_PDF_BACKEND — when it names a specific backend, only that one is tried and no fallback happens.

Everything except export works without any renderer.

"The weasyprint PDF backend is configured but not usable"

POSTULO_PDF_BACKEND=weasyprint is set explicitly, and Pango is missing. Either install the libraries above, or set the value to auto so that Postulo can fall back.

"The chromium PDF backend is configured but not usable"

The fallback renderer is two installs, and this is the second one missing: the playwright package is there and the browser it drives is not.

uv run playwright install chromium

It takes effect without a restart. Up to and including 0.3.0 Postulo checked only for the package, so Server settings → Overview named chromium as the renderer while a draft answered with an error page and an export ended in "Something went wrong". If the browser is installed and the message stays, the log line "Chromium could not be launched" carries what Playwright said, which on Linux is usually a system library the browser needs.

There is no sign-up form

The instance is invite-only, which is the default. Either get an invitation from a staff member, or set POSTULO_REGISTRATION_OPEN=true. See Accounts and invitations.

"This invitation may only be used with the address it was sent to"

The invitation names an email address and you are signing up with a different one. Use the address it was issued for, or ask for an unbound invitation.

The admin returns 404

Most likely it is not running: POSTULO_ADMIN_URL is empty by default since 0.3.0, and an empty value mounts no admin at all. Set it to a path of your choosing to turn it on, and restart. If it is set, that path is where the admin is — /admin/ will 404 unless that is what you chose. Server settings covers people, sign-in policy, plugins, email, logs and defaults without it.

CSRF verification failed

Almost always a reverse proxy. Set POSTULO_CSRF_TRUSTED_ORIGINS to your full origin including the scheme:

POSTULO_CSRF_TRUSTED_ORIGINS=https://postulo.example.org

Make sure the proxy forwards X-Forwarded-Proto.

The pages have no styling

collectstatic has not run, or POSTULO_STATIC_ROOT points somewhere the application cannot read:

DJANGO_SETTINGS_MODULE=postulo.config.settings.prod uv run manage.py collectstatic --noinput

Downloads give "File not found"

The database has a record whose file is missing from MEDIA_ROOT. Usually the media directory was not restored alongside the database — see Backups and your data.

"The site refused the request (403)"

Bot protection, not a mistake on your part. The page is visible to your browser and refused to your server.

Paste the page source instead: on the capture page, open The site refuses Postulo, or the posting needs a login, view the source in your browser, and paste it in. Postulo fetches nothing when you do. See Capturing postings.

"Nothing resembling a job posting was found on that page"

The page carries no structured data and no usable title. Postings behind a login, or built entirely by JavaScript after the page loads, cannot be read from the outside — Postulo fetches HTML, it does not run a browser.

Use Record an application and paste the text in. If it is a site you use often, a plugin can be written for it: see Writing a plugin.

"That address is on a private or local network"

Capture refuses anything resolving to a loopback, private or link-local address, or to an IPv6 address carrying one (as NAT64 and 6to4 addresses do), and there is no setting to permit it. Paste the posting text in by hand.

"That page took longer than 30 seconds to arrive"

The whole page — its headers, its redirects and every byte — has thirty seconds; a site sending it more slowly than that is given up on rather than allowed to hold a worker. Try again later, or paste the page source in by hand.

"That page was sent with the content encoding …, which Postulo does not unpack"

Postulo asks a site for gzip or deflate and unpacks nothing else: the other compressions can turn a few hundred bytes into hundreds of megabytes. A site that answers in another one anyway — or labels its page with a Content-Encoding that is not a compression at all, such as UTF-8 — is refused unread, and the message names what it sent. Paste the page source in by hand; if the site is yours, the header is what to fix.

"This site's robots.txt asks automated clients not to fetch that page"

The site has declined. You can set POSTULO_CAPTURE_IGNORE_ROBOTS=true, which makes you responsible for the requests your instance makes, or copy the text in by hand.

An API token stopped working

Tokens return 401 when missing, mistyped, revoked, or belonging to a disabled account. Check the token list under Settings → API tokens: it shows which are revoked and when each was last used. A lost token cannot be recovered, only replaced.

Times are wrong

Set POSTULO_TIME_ZONE for the instance, and check the time zone on your own profile, which overrides it. Times are stored in UTC and displayed in your zone; the stored data is not wrong, only its presentation.

Verification or password reset emails never arrive

Postulo only sends email through the account system, and only if it is configured. See Configuration. Until it is, nobody who signs up can complete the verification that sign-in requires — except the account made with createsuperuser, whose address is trusted, and people who arrived through an invitation bound to their address.

To verify an address by hand, open the admin (Accounts → Email addresses), find it, and tick Verified. To change a password without email:

uv run manage.py changepassword alex.morgan     # the username

I cannot get past the code prompt

Two-factor authentication is on for the account and the phone with the app is gone. Use one of the recovery codes shown when it was set up. Without those, someone with a shell on the server removes the second factor:

uv run manage.py mfa_reset alex.morgan

Then sign in with the password alone and set it up again.

Single sign-on sends me back with an error

Nine times out of ten the redirect URI the identity provider has on file is not the one Postulo sent. Compare the callback shown under Server settings → Sign-in with the provider's application settings character for character: scheme, host, port, trailing slash. A test instance reached on plain HTTP inside a mesh needs the provider to accept a plain-HTTP redirect for it.

If the provider is reached but Postulo refuses the address as unverified, the provider is not sending email_verified: true in its claims; most can be told to. Until then the person receives a verification link as anyone else would.

Checking a deployment

This reports anything unsafe about your production configuration:

DJANGO_SETTINGS_MODULE=postulo.config.settings.prod uv run manage.py check --deploy

Is it running?

/healthz returns JSON and checks the database connection. Useful for uptime monitoring, and it is what the container's own health check asks — so a container marked unhealthy means that endpoint answered 503 or did not answer at all.

Up to and including 0.3.0 there was a third cause, and it had nothing to do with the database. The check asks by 127.0.0.1, so with POSTULO_ALLOWED_HOSTS set to the instance's own name alone every probe was refused with a 400: the container was reported unhealthy while serving pages, and the scheduler and worker services, which wait for a healthy one, never started. The log shows it as DisallowedHost, every thirty seconds. On those versions keep 127.0.0.1 in the list beside your own name; later ones answer the container's own check whatever the list holds.

It is exempt from the HTTPS redirect, and has to be: the check is curl inside the container talking to 127.0.0.1, where there is no TLS to be redirected to. Before v0.3.0 it was not exempt, so the probe received a 301 — and curl -f treats a redirect as success. Every container running 0.2.x reports healthy whatever is wrong with it, including a database that has gone away. If you are on 0.2.x and want a working probe before upgrading, set POSTULO_SSL_REDIRECT=false and let your reverse proxy do the redirecting, which is what most of them do anyway.

/metrics is exempt for the same reason. /logs is not: it carries entries naming connections, companies and applications, so a collector must reach it over HTTPS through the proxy rather than over plain HTTP inside the network.

Something else

Open an issue on the repository, with the version (git rev-parse --short HEAD), what you did, and what happened. Please do not paste your .env.