Skip to content

Docker on Linux: every page 500s with SQLITE_CANTOPEN on first run (bind mount overrides the image's /data ownership) #7

Description

@michaelcopeland

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:

volumes:
  - ./data:/data

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:

  1. Document it in the README's Docker section — the workaround above is two lines and costs nothing.
  2. Ship user: in compose.yml with env defaults, so it works out of the box for the common case.
  3. 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.
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions