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.
This commit is contained in:
2026-07-18 23:54:50 +07:00
parent fe89bb2b1c
commit 1d86c68665
5 changed files with 32 additions and 4 deletions
+2
View File
@@ -18,6 +18,8 @@ tests
.swarm
.mcp.json
ruvector.db
.playwright-mcp
scripts
# Local env files
.env
+5 -1
View File
@@ -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
+6 -2
View File
@@ -32,13 +32,17 @@ services:
environment:
SESSION_SECRET: "<long random string — e.g. openssl rand -hex 32>"
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
+3 -1
View File
@@ -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.
+16
View File
@@ -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 "$@"