From 1d86c68665244c105507f62ba436fc716a76812d Mon Sep 17 00:00:00 2001 From: grabowski Date: Sat, 18 Jul 2026 23:54:36 +0700 Subject: [PATCH] Fix bind-mount data dir permissions via entrypoint Docker creates ./data root-owned on the host, but the app runs as the unprivileged node user, causing SQLITE_CANTOPEN on first deploy. New entrypoint starts as root, chowns DATA_DIR, then drops privileges with setpriv. Compose keeps the SQLite database in ./data next to the compose file. Also trims scripts/ and playwright artifacts from the image. --- .dockerignore | 2 ++ Dockerfile | 6 +++++- README.md | 8 ++++++-- docker-compose.yml | 4 +++- docker-entrypoint.sh | 16 ++++++++++++++++ 5 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 docker-entrypoint.sh diff --git a/.dockerignore b/.dockerignore index b15bfb8..2c20319 100644 --- a/.dockerignore +++ b/.dockerignore @@ -18,6 +18,8 @@ tests .swarm .mcp.json ruvector.db +.playwright-mcp +scripts # Local env files .env diff --git a/Dockerfile b/Dockerfile index 234b8a2..2b41a5b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,11 @@ COPY public/ ./public/ # Owned by the unprivileged `node` user that ships with the base image. RUN mkdir -p /app/data && chown -R node:node /app -USER node +# The entrypoint starts as root only to chown a bind-mounted DATA_DIR +# (Docker creates host dirs root-owned), then drops to `node` via setpriv. +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh +ENTRYPOINT ["docker-entrypoint.sh"] EXPOSE 3000 diff --git a/README.md b/README.md index 47564af..3aff76b 100644 --- a/README.md +++ b/README.md @@ -32,13 +32,17 @@ services: environment: SESSION_SECRET: "" volumes: - - ./data:/app/data + - ./data:/app/data # database lives next to this compose file # No public port mapping needed when Caddy shares a Docker network with # the app (recommended). For a host-level Caddy, map localhost only: ports: - "127.0.0.1:3000:3000" ``` +The entrypoint chowns `./data` to the app's unprivileged `node` user on +startup, so a root-created bind-mount directory works out of the box. (Images +older than this note need a one-time `sudo chown -R 1000:1000 ./data`.) + ```bash docker compose up -d ``` @@ -108,7 +112,7 @@ npm test # API tests (node:test + supertest) ## Data -All state lives in a single SQLite file under `DATA_DIR`. In Docker this is `/app/data`, mounted from `./data` on the host. No external database or services are required (geocoding calls OpenStreetMap's Nominatim, which needs no API key; airport lookups use a bundled offline dataset). +All state lives in a single SQLite file under `DATA_DIR`. In Docker this is `/app/data`, bind-mounted from `./data` next to the compose file — back up that directory to back up all trips. No external database or services are required (geocoding calls OpenStreetMap's Nominatim, which needs no API key; airport lookups use a bundled offline dataset). ## Documentation diff --git a/docker-compose.yml b/docker-compose.yml index d2f9bd4..70fadee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,9 @@ services: ports: - "3000:3000" volumes: - # SQLite database persists on the host across container rebuilds. + # SQLite database lives in ./data next to this compose file and + # persists across container rebuilds. The entrypoint fixes ownership + # automatically (the app itself runs as the unprivileged `node` user). - ./data:/app/data environment: # CHANGE THIS: set a long random string before exposing the app. diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..e4fbdc9 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -e + +# Bind-mounted data directories (e.g. ./data next to the compose file) are +# created root-owned by Docker, but the app runs as the unprivileged `node` +# user. When we start as root: fix ownership of DATA_DIR, then drop +# privileges. When started with a custom --user, just run as-is. +DATA_DIR="${DATA_DIR:-/app/data}" + +if [ "$(id -u)" = "0" ]; then + mkdir -p "$DATA_DIR" + chown -R node:node "$DATA_DIR" + exec setpriv --reuid=node --regid=node --init-groups "$@" +fi + +exec "$@"