First, thanks for this — it's a genuinely useful tool and the breadth of endpoint coverage is impressive.
Problem
Following the README's recommended path (docker compose up --build) on Linux, the app starts, logs ✓ Ready, and then every page returns HTTP 500:
app-1 | ▲ Next.js 15.5.2
app-1 | ✓ Ready in 83ms
app-1 | ⨯ SqliteError: unable to open database file
app-1 | code: 'SQLITE_CANTOPEN',
The "Ready" line makes it read like a working boot, which is what makes it confusing rather than merely broken.
Cause
Dockerfile creates /data owned by uid 1001 and then drops to that user:
RUN mkdir -p /data && chown nextjs:nodejs /data # uid 1001
USER nextjs
compose.yml then bind-mounts the host directory over it:
A bind mount replaces the directory, including its ownership. The host ./data is owned by the host user — typically uid 1000, mode 0775 — so uid 1001 inside the container gets r-x on its own database directory and better-sqlite3 cannot create the file.
Confirmed from inside the container:
$ docker compose exec app id
uid=1001(nextjs) gid=65533(nogroup)
$ docker compose exec app ls -ldn /data
drwxrwxr-x 2 1000 1000 4096 /data
This does not reproduce on Docker Desktop (macOS/Windows), where the VM maps uids for bind mounts — which I'd guess is why it hasn't come up before. It hits native Linux users whose uid isn't 1001.
Workaround
A docker-compose.override.yml runs the container as the host user:
services:
app:
user: "${HOST_UID:-1000}:${HOST_GID:-1000}"
After that, / and /dashboard both return 200 and data/seo-playground.db is created normally.
Possible fixes
A few options, roughly in order of how much I'd expect you to like them:
- Document it in the README's Docker section — the workaround above is two lines and costs nothing.
- Ship
user: in compose.yml with env defaults, so it works out of the box for the common case.
- Use a named volume instead of a bind mount, which Docker initialises with the image's ownership. Changes where the DB lives, so it's a bigger call.
- Entrypoint chown — works, but needs the container to start as root, which I wouldn't suggest given you've deliberately made it run unprivileged.
Happy to send a PR for whichever you prefer — (1) or (2) are both a few lines. Just say which and I'll open it.
Environment: Docker 29.8.0, Compose v5.5.1, Ubuntu, host uid 1000.
First, thanks for this — it's a genuinely useful tool and the breadth of endpoint coverage is impressive.
Problem
Following the README's recommended path (
docker compose up --build) on Linux, the app starts, logs✓ Ready, and then every page returns HTTP 500:The "Ready" line makes it read like a working boot, which is what makes it confusing rather than merely broken.
Cause
Dockerfilecreates/dataowned by uid 1001 and then drops to that user:compose.ymlthen bind-mounts the host directory over it:A bind mount replaces the directory, including its ownership. The host
./datais owned by the host user — typically uid 1000, mode 0775 — so uid 1001 inside the container getsr-xon its own database directory andbetter-sqlite3cannot create the file.Confirmed from inside the container:
This does not reproduce on Docker Desktop (macOS/Windows), where the VM maps uids for bind mounts — which I'd guess is why it hasn't come up before. It hits native Linux users whose uid isn't 1001.
Workaround
A
docker-compose.override.ymlruns the container as the host user:After that,
/and/dashboardboth return 200 anddata/seo-playground.dbis created normally.Possible fixes
A few options, roughly in order of how much I'd expect you to like them:
user:incompose.ymlwith env defaults, so it works out of the box for the common case.Happy to send a PR for whichever you prefer — (1) or (2) are both a few lines. Just say which and I'll open it.
Environment: Docker 29.8.0, Compose v5.5.1, Ubuntu, host uid 1000.