Troubleshooting
Start with the service state, logs, and health check:
These commands target the default stack. For the Cloudflare Tunnel stack, use
-f docker-compose.tunnel.yml in every Compose command. See
Public access with Cloudflare Tunnel.
docker compose ps
docker compose logs --tail=100 pantrypal
curl -s http://127.0.0.1:8787/health
/health is public. The image also has a healthcheck:
docker inspect --format '{{.State.Health.Status}}' pantrypal
Compose cannot find the external network
The base file joins an external network named rakazo_app, with or without the override. The tunnel stack joins it too, so sibling containers (rakazo, homehub) can reach pantrypal:8787. If it does not exist, Compose stops with an error such as network rakazo_app declared as external, but could not be found.
Create it once:
docker network create rakazo_app
To use another existing network, set RAKAZO_NETWORK:
RAKAZO_NETWORK=my_network docker compose up -d --build
Compose rejects !override
The override file uses the !override tag to replace the base port list. Update Docker Compose if your version does not accept it. Then run docker compose up -d --build again.
Permission errors on the database
Symptoms include SQLITE_CANTOPEN, EACCES, attempt to write a readonly database, or a container that restarts during startup. The image runs as uid 1000. On a Linux host, give that user write access:
mkdir -p data
sudo chown -R 1000:1000 data
docker compose up -d
Do not use chmod 777. See Configuration.
Port 8787 is already in use
Docker reports that the port is allocated. Change the host side of the mapping in docker-compose.override.yml for the default stack, or in docker-compose.yml for base-only:
ports: !override
- "127.0.0.1:9000:8787"
Keep !override when you edit the override file; it replaces the base port list.
If you edit the base file alone, use plain ports: instead.
Run docker compose up -d and open http://127.0.0.1:9000.
The web app cannot reach the API
-
Check
/auth/session.{"enabled":false,...}means no browser password is configured. With the base file alone, network trust is also off and the web app sends no token, so API calls return401. Use the default stack on a trusted network, setPANTRYPAL_PASSWORD, or use a proxy that supplies a valid bearer token. See Access and security. -
If password mode is on, the web app shows a login screen. Sign in first. A wrong or expired session returns
401; rotate the password only when you want to sign every browser out. -
If you see
503and a message about no household, runhousehold initor load the example data:docker compose exec pantrypal node dist/cli.js household init --name "Our House" -
If a proxy adds an
Authorizationheader, it must be a valid token. A sent token is always checked, even when a session cookie is present, and a bad token returns401. -
With the tunnel stack, use
docker compose -f docker-compose.tunnel.yml exec pantrypal ...for the command above.
Browser login fails
/auth/sessionreportsenabled: false: no password reached the process. SetPANTRYPAL_PASSWORDand recreate the container withdocker compose up -d. With the tunnel stack, edit.envand usedocker compose -f docker-compose.tunnel.yml up -d --force-recreate pantrypal.- Startup exits with a message that
PANTRYPAL_PASSWORDis required:PANTRYPAL_REQUIRE_PASSWORDis on and the password is unset. Set the password or remove the requirement. - Login succeeds but the next request returns
401: the cookie isSecureand needs HTTPS, except onhttp://localhost. Use HTTPS for a LAN address or public hostname. - Login returns
403: the requestOriginhost does not matchHost, the origin is not HTTPS (outside loopback), or the proxy removedOrigin. Preserve both headers.X-Forwarded-Hostdoes not count. - Login returns
401: wrong password. It returns429after 10 failures per minute from one peer or 40 in total; wait forRetry-Afterinstead of retrying. A body over 4 KiB returns413; a non-JSON body returns415. - Sessions disappear after a restart only if the password changed. Sessions are signed with the password, so rotating it signs out every browser. A copied cookie otherwise stays valid until it expires.
- To sign out, use Settings → Log out. Closing the tab does not end the session.
MCP returns 401
/mcp always requires a token, even when network trust or a browser password is
on. It never accepts a session cookie. Create a token and send it in the
Authorization: Bearer <token> header. See MCP setup.
A token in a query parameter does not work. A revoked token also returns 401.
The server uses the wrong port
A source run defaults to 4787. The Docker image sets 8787. Check the startup log line:
PantryPal listening on http://0.0.0.0:8787
Set PORT to choose a port. If you change the container port, also change the Compose mapping. See Configuration.
The database is locked
PantryPal waits up to 5 seconds for a lock. If the error continues, another process is using the same database.
- Run one server against one database file.
- Stop old containers or local servers that use the same
datadirectory. - Keep the database on a local filesystem. Do not use NFS or SMB.
An update or migration fails
The new server exits before it serves traffic. Read the logs, then restore the backup from before the update and run the previous code. Do not delete the data directory. See Update PantryPal and Backups and restore.
Do not run CLI commands to inspect the database if you plan to roll back. Every CLI command that opens the database applies pending migrations.
The seed is refused
The seed only loads into a household with no ingredients or recipes. --force skips that check; it does not clear, replace, or merge data safely. Use a fresh database or restore a backup. See Example data and Data import.
The web app is not served
The log says the web app is missing and the server is API only. For a source run, build it with pnpm build, not pnpm build:server. For a custom build, check PANTRYPAL_WEB_DIR and make sure the directory contains index.html. See Configuration.
The tunnel has no connection
docker compose -f docker-compose.tunnel.yml ps
docker compose -f docker-compose.tunnel.yml logs --tail=100 cloudflared
cloudflaredneeds outbound HTTPS to Cloudflare. A blocked egress firewall stops the connector.- A wrong or revoked tunnel token stops the connector. Copy the token again from
the tunnel dashboard and recreate
cloudflared. - The public hostname must use service type
HTTPand URLpantrypal:8787. - The stack publishes no host ports on purpose. Test through the public URL, not
localhost:8787, on the Docker host. - Do not add Cloudflare Access to this hostname. It blocks MCP bearer tokens.
- If
pantrypalis unhealthy, fix that first;cloudflaredwaits for it.
Environment changes do not apply
docker compose restart restarts the existing container and keeps its environment. Run docker compose up -d to recreate it with the new values. Check the merged settings with:
docker compose config
For the tunnel stack, edit .env, then recreate the affected service:
docker compose -f docker-compose.tunnel.yml up -d --force-recreate pantrypal
For a source run, restart the process. A plain source run does not read .env.
Data appears empty after a restart
Compose resolves ./data from the directory that contains the base Compose file.
Check the resolved mount with docker compose config.
If you restored data, check the ownership of data and the container logs. See Backups and restore.
A local build fails on better-sqlite3
Use Node.js 22 or later. The native module needs Python 3 and a C++ toolchain when no prebuilt binary exists. On Debian or Ubuntu:
sudo apt install build-essential python3
pnpm rebuild better-sqlite3
The Docker image includes the compiled module, so you do not need a host toolchain. Use Compose if you do not want to build the module on the host.
A token is lost
The secret is shown only once. Create a new token, update the client, then revoke the old token. See Access and security.