Troubleshooting
Hit a snag? Find your symptom below and jump to the fix. These cover every deploy path — the scaffolded wizard output, a hand-edited version of it, or running from source as a contributor.
Grouped by area. For the steps these errors tend to come up during, see also Manual / advanced deployment and Public HTTPS deploy.
Startup / .env
postgresorfibergate-corefail to start / crash on boot — almost always a blank required var in.env. Checkdocker compose logs postgresordocker compose logs fibergate-corefor the specific error, then fill in the missing value —create-fibergategenerates these for you (see Quickstart for merchants); if you hand-edited.envafterward, double-check you didn't blank one out.fibergate-corenever starts, even though.envlooks complete — it hasdepends_on: condition: service_healthyon bothpostgresandfiber-node, so it intentionally won't start until both report healthy. Rundocker compose psto see which one isn't healthy yet, then check that service's logs.Dashboard login fails even with the right password — check that
ADMIN_PASSWORD_HASH_B64in.envis the base64-encoded hashcreate-fibergategenerated for you, not the raw$2y$10$...hash pasted directly. Decode it locally to sanity-check:echo "$ADMIN_PASSWORD_HASH_B64" | base64 -dshould print a string starting with$2y$or$2b$.Why base64, if you're curious
Docker Compose's
.envinterpolation anddotenv-expand(used bypnpm dev/build) each corrupt literal$characters differently, and a bcrypt hash is full of them. base64 has no$in its alphabet, so it's the one encoding that survives both paths intact. Full story in Decisions & trade-offs.fibergate-corefails to connect to Postgres withpassword authentication failed for user "fibergate", even though.envlooks correct — almost always a stalepostgres-datavolume from an earlierdocker compose up -din the same directory. The officialpostgresimage only appliesPOSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DBthe first time it initializes an empty data volume — if you already randocker compose up -donce before (then re-rancreate-fibergateinto the same deploy directory, which generates a brand-new random password each time, or hand-editedPOSTGRES_PASSWORD), the running Postgres container is silently still using the old password baked into the volume, whilefibergate-coreconnects with the new one from.env.- If there's no real data in that Postgres instance yet (a fresh/test deploy):
docker compose down -v(drops the volume — destroys all data in it) thendocker compose up -dto reinit fresh against the current.env. - If you need to keep existing data (real invoices/webhooks already recorded): don't run
down -v. Instead, sync the password inside the already-running container to match.env:docker compose exec postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "ALTER USER \"$POSTGRES_USER\" WITH PASSWORD '<the POSTGRES_PASSWORD value from .env>';"
- If there's no real data in that Postgres instance yet (a fresh/test deploy):
fiber-node
fiber-node-preflightexits with a "missing CKB testnet key" message andfiber-nodenever starts — expected if you skipped providing the CKB key. The message printed by that container tells you exactly what's missing; add the key file atdocker/fiber-node/ckb/keyand re-rundocker compose up -d.fiber-noderestarts in a loop withSecret key file error: decryption failed: aead::Error— usually not a wrong password. The most common cause is the key file having the wrong format: it must be exactly 1 line of raw private-key hex, no0xprefix. If you exported it withckb-cli account export --extended-privkey-path, that file has 2 lines (private key + chain code) —head -n 1it first. Only after ruling that out, check whetherFIBER_SECRET_KEY_PASSWORDactually matches the passphrase used to encrypt the key.
0.0.0.0 counts as "public", even inside a private Docker network
fiber-node starts but then exits with "Cannot listen on a public address without a biscuit public key set in the config" — don't change docker/fiber-node/config.yml's rpc.listening_addr to 0.0.0.0; fnn treats that as a public bind and refuses to start without Biscuit auth configured. It's already pre-seeded with a static private IP (172.28.0.10) matching the fiber-node service's ipv4_address in docker-compose.yml — if you edited either of those, keep them in sync.
Public HTTPS / TLS / WSS
https://$DOMAINshows a certificate warning / "not secure" — expected until you've run the one-timedocker compose run --rm certbot-initin Public HTTPS deploy. Until thennginxis serving the temporary self-signed certnginx-certs-preflightgenerated so it could start at all — normal on first boot, not a bug.certbot-initfails with a challenge/timeout error — almost always DNS or port-forwarding, not.env. ConfirmDOMAINactually resolves to this host's public IP (dig +short $DOMAINfrom a machine that isn't this one) and that ports 80/443/8228 are forwarded to it — Let's Encrypt has to reach port 80 on this host from the public internet to validate the challenge.certbot-initfails with a DNS-related timeout during Let's Encrypt's validation, even though DNS and port-forwarding are both actually correct — free/dynamic DNS providers can have slow or unreliable authoritative DNS answering, which can cause Let's Encrypt's multi-perspective ("secondary validation") checks to fail even when your own setup is fine. Confirm this is the cause by checking DNS resolution consistency for$DOMAINacross a couple of different public resolvers (e.g. a DNS-over-HTTPS query to two different providers) — if they don't agree yet, this is likely transient, and simply retryingdocker compose run --rm certbot-initafter a few minutes is often enough.certbot-initprintsAccount registered./Requesting a certificate for $DOMAINwith no validation error, then fails withlive directory exists for $DOMAIN— this means the Let's Encrypt challenge actually succeeded; the failure is a local storage conflict between certbot andnginx-certs-preflight's temporary self-signed placeholder, whichcertbot-initnow clears automatically before requesting a real cert (fixed 2026-07-15 — if you're on an older image, update and re-rundocker compose run --rm certbot-init). Not a DNS/port-forwarding issue, and safe to retry immediately (no rate-limit risk from this failure mode).certbot-initreportsSuccessfully received certificate, butdocker compose exec nginx nginx -s reloadthen fails withcannot load certificate ... No such file or directory— this means the cert got saved under a-0001-suffixed name (e.g.live/$DOMAIN-0001/) instead of the plainlive/$DOMAIN/that nginx expects. This happens if you already hit the "live directory exists" error above once and cleaned it up by hand — a failedcertonlyrun can leave an orphanedrenewal/$DOMAIN.confbehind even though it errored, and manuallyrm -rf-ing onlylive//archive/(not that config file too) makes the next run think the domain name is already taken.certbot-initnow checks for this too (fixed 2026-07-15) — update and clear all oflive/$DOMAIN,archive/$DOMAIN,renewal/$DOMAIN.conf(and any-0001-suffixed variants already created) before re-runningdocker compose run --rm certbot-init.- The real cert is installed and verified server-side (
curl -v https://$DOMAINshowsSSL certificate verify ok), but a browser tab still shows "Not secure" — if that tab already had the site open before the fix (e.g. it previously hit the temporary self-signed cert's warning), the browser can keep showing cached "insecure" state for that origin. Try a hard refresh, or open the URL in a private/incognito window, before assuming the server-side fix didn't work. - "Pay with browser wallet" in the demo storefront still can't connect after setting up WSS — double-check
docker/fiber-node/config.yml'sannounced_addrswas actually uncommented/edited with the real domain andfiber-nodewas restarted (docker compose restart fiber-node) — this file isn't templated from.env, it's a manual edit.
Testing payments with fiber-node-payer
(See Demo storefront for the full flow this refers to.)
connect_peersucceeds but the peer disappears fromlist_peerswithin ~1s, andopen_channelfails with"...waiting for peer to send Init message"— gossip backlog overflow, not a transient issue. Happens whenfiber-nodehas accumulated a lot of real gossip data and dumps it all on the freshly-connected payer at once. Already worked around indocker/fiber-node-payer/config.yml(gossip_network_num_targeted_active_syncing_peers: 0, nobootnode_addrs) — if you still hit this, checkfiber-node's owngraph_channelscount vianode_info.open_channelreturns atemporary_channel_id, butfiber-node's logs show"Failed to fund channel: ... need more capacity"—fiber-nodeitself needs its own 99 CKB channel reserve to accept a channel, not just the opener's funding amount. Fundfiber-node's own address via the faucet.- Channel stuck at
AwaitingTxSignatureswith all 3 signature flags set — normal; wait ~20-30s for the funding transaction to confirm on-chain, then re-polllist_channels. A tx hash showingstatus: "unknown"right after signing is expected, not a failure.