6 Troubleshooting
Tiago Águeda edited this page 2026-09-22 15:51:02 +02:00

Troubleshooting

Which version am I running?

The page footer says Postulo X.Y.Z; 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 docker logs postulo gives you the same records if you have a terminal to hand. 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 — about five megabytes in total by default — so it cannot fill a disk, which also means a problem from last month may have scrolled off.

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.

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, and there is no setting to permit it. Paste the posting text in by hand.

"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.

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.