Skip to content
Omicron

Self-hosting

Troubleshooting

The failures people actually hit, and what each one means.

Start here, always:

docker compose ps                 # what is running
docker compose logs -f backend    # or: caddy, frontend, postgres, redis

Installation

The first up takes a long time or seems stuck. The first build compiles the images — a few minutes is normal. Watch progress with docker compose logs -f.

A service keeps restarting. Read its logs specifically. A restarting backend is usually a database it cannot reach; a restarting postgres is usually a pgdata volume that does not match the password in secrets (see Backups).

Reaching the site

I cannot reach it at all.

  • Local: use http://localhost (not HTTPS). The app is also directly at http://localhost:5173 for debugging.
  • Public: confirm the DNS A/AAAA record points at the server, and that ports 80 and 443 are open in both the host firewall and your provider’s security group.

Ports 80/443 already in use. Set HTTP_PORT / HTTPS_PORT in .env to free ports, or stop the conflicting service.

HTTPS

The certificate is not being issued.

  • DNS must resolve to this server first.
  • Ports 80 and 443 must be reachable from the internet — Let’s Encrypt validates over them.
  • Check the ACME error: docker compose logs -f caddy.

localhost has no certificate. Expected. Local development is served over plain HTTP.

Email

“Port 25 is blocked” when setting up direct email. Expected on most hosts, and always on localhost. Use the smtp or relay mode instead, or ask your provider to unblock port 25 and set reverse DNS.

The test email fails. The error is shown verbatim. Common causes: wrong SMTP password, the wrong port/TLS combination (587 = STARTTLS, 465 = implicit TLS), or a provider that requires a verified sender. Fix and press send test again.

Mail sends but lands in spam. With direct, check SPF, DKIM, DMARC, and especially the PTR record. With any mode, a brand-new IP or domain has no reputation yet.

A visitor reports “This instance can’t send email”. That is the console-mode notice, not a fault: the instance is still on its default transport and only prints mail to the backend log. Configure smtp, relay, or direct, press send test email, and the notice disappears for visitors.

Federation

The federation toggle did not take effect. It applies on restart. Run docker compose up -d, then check curl localhost:8000/version for "federation": true.

/.well-known/nodeinfo returns 404. Federation is still off, or the restart has not happened.

The instance does not appear in FediDB, instances.social, or fediverse.observer. Those crawl /.well-known/nodeinfo from the public internet, follow the link in it, and read the document it names. Check all three answer from outside your network:

curl https://your-domain/.well-known/nodeinfo   # -> a links array, not an empty one
curl https://your-domain/nodeinfo/2.1
curl https://your-domain/nodeinfo/2.0

If all three answer here but the directory still shows nothing, the remaining delay is on its side: re-crawls are typically on the order of a day, and a directory generally has to have discovered the instance first — either because another server federated with it, or because the operator submitted it through the directory’s own form.

Nobody can find @me@my-domain from Mastodon. Check, from another machine:

curl "https://your-domain/.well-known/webfinger?resource=acct:me@your-domain"
curl -H "Accept: application/activity+json" https://your-domain/users/me

Both must return JSON over HTTPS on your real domain. If the domain in the returned URIs is wrong, your public domain setting does not match the domain being served.

Keep the Accept header on the second one. Without it that URL redirects to the profile page, by design — so a bare curl -L returns HTML and looks like a failure when nothing is wrong.

Posts are not reaching remote followers. Delivery is queued. Check the backend logs for delivery errors, confirm the remote instance has not defederated you, and confirm the post is published — not a draft, and not still waiting for a scheduled time.

Scraper shield

The toggle in Admin → Security is unavailable. Caddy has not been recreated with the newer Caddyfile. Run once:

docker compose up -d --build --force-recreate

The challenge page is stuck on “Calculating…” and only loads after a refresh. Fixed in the release that added ANUBIS_REDIRECT_DOMAINS. The fix is a Caddyfile change, so a plain up -d does not apply it — recreate once:

docker compose up -d --build --force-recreate

My uptime monitor started reporting 503. Check what it is fetching. A challenged request is answered with 503 and Retry-After by design — see what a challenged request gets back. Nothing a reader loads is challenged, so a monitor pointed at /, an article, or /healthz will never see one; a monitor pointed at /login or /admin will see one on every check. Point it at a page rather than at the sign-in form.

Readers are being challenged again after a restart. Not expected. The signing key lives in the secrets volume and survives restarts. If it recurs, the challenge service is failing to read that key — check its logs for ED25519_PRIVATE_KEY_HEX_FILE or generating random key. Deleting the secrets volume also loses the key, and with it the Postgres password and session secret.

A visitor says the challenge page never finishes loading. Solving it needs JavaScript. Either theirs is off — in the browser, or blocked for your domain by an extension — or the challenge script itself failed to load, which a reload fixes. The screen’s footer says both, and links them back to your home page; reading is never challenged, so that link works whatever their browser is doing. Put your own support address in the impressum.page body of botPolicy.yaml if you want one on that screen.

The challenge screen looks nothing like my site. Caddy serves anubis-theme.css at the path the challenge page asks for; when that file is missing, or the Caddy container predates it, the challenge service answers with its own stylesheet instead and the screen reverts to its stock look. git pull restores the file, and Caddy picks it up on the next request — unless the container is older than the mount, in which case recreate it:

docker compose up -d --force-recreate caddy

I edited botPolicy.yaml and nothing changed. The rules are read once at startup and the file is mounted into the container, so it keeps serving the old set until it restarts:

docker compose restart anubis

Every page load returns 500 while the shield is on. The challenge service could not determine the client IP. Its logs will say X-Real-Ip header is not set. Caddy supplies that header, so this means Caddy is running an older Caddyfile — recreate once with the command above.

A shared link shows the domain and the raw URL instead of a card. The fetcher never parsed the page. Fetch it the way the app does and compare:

curl -sA 'WhatsApp/2.23.20.0 A' https://your-domain/@you/your-post \
  | grep -o '<meta property="og:[a-z]*" content="[^"]*"'

If the tags are missing and the response is small, check the status: a 503 whose title is “Making sure you’re not a bot!” means the scraper shield answered instead of the article. That should not happen — post URLs are not challenged — so look at whether botPolicy.yaml has been customised. If the tags are there, the fetcher’s own cache is the likely culprit: preview caches are keyed by URL, so a card built while the site was down or mid-upgrade sticks. Share the link with a query string appended (?v=2) to force a fresh fetch — if the card is correct then, it was cached.

The card has a title and description but no image. Check the og:image URL directly: it must return 200, an image/* content-type, and be under about 600 KB. Then check it is not blocked — a crawler reads robots.txt before fetching an image, and an image under a disallowed path is silently skipped.

A post with no pictures shows the instance’s logo rather than a card. Expected in two cases, and neither is a fault. A federated copy of someone else’s post is illustrated by the instance that published it, not by yours. And the card is drawn in one bundled font covering Latin, Greek and Cyrillic — a title in Japanese, Korean, Arabic or Hebrew cannot be set in it, and the logo is served instead of an empty rectangle. Anything else, fetch the card directly: /api/og/posts/<post-id>.jpg should answer 200 image/jpeg, or 302 to /og-image.png.

Data

I ran docker compose down -v. That deleted the volumes: database, uploads, secrets, and certificates. Restore from a backup; there is no other recovery.

The backend cannot connect to Postgres after a restore. The classic pairing failure: a fresh pgdata with an old secrets, or the reverse. See the pairing rule in Backups and restore.

Still stuck

Collect the version and the relevant logs before opening an issue:

curl localhost:8000/version
docker compose logs --since 30m backend > backend.log

Then open an issue at github.com/the-jk-labs/omicron/issues.

Found a mistake?Edit this page on GitHub.