diff --git a/.env.example b/.env.example index 882e9cd..7893e00 100644 --- a/.env.example +++ b/.env.example @@ -18,3 +18,7 @@ FILES_PATH=/app/data/files # Default Meilisearch host MEILISEARCH_HOST=http://meilisearch:7700 + +# User and group that own everything in data/ (set automatically by setup.sh) +PUID= +PGID= diff --git a/Dockerfile.dev b/Dockerfile.dev index 1bd8cf0..7a99aca 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -9,6 +9,8 @@ RUN apt-get update \ COPY package*.json ./ RUN npm ci COPY . . +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh EXPOSE 3000 +ENTRYPOINT ["docker-entrypoint.sh"] CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/README.md b/README.md index 57426a7..db37b59 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,14 @@ git clone https://github.com/OneBacklog/OneArctic cd OneArctic ``` -2. Generate secrets and create data folders: +2. Generate secrets and create data folders (run it as your normal user, not with `sudo`): ```bash APP_ENV=production bash setup.sh ``` +The containers run as the user who ran `setup.sh`, so everything in `data/` stays owned by you. + > [!IMPORTANT] > It is recommended to serve the app over HTTPS. If you don't, change `COOKIE_SECURE` to `false` in the `.env` file. @@ -80,6 +82,7 @@ The app reads secrets from `.env` (created by `setup.sh`). | `DATABASE_PATH` | Yes | SQLite database file path. | | `FILES_PATH` | Yes | Attachments storage path. | | `MEILISEARCH_HOST` | Yes | Meilisearch HTTP endpoint. | +| `PUID` / `PGID` | No | User and group that own everything in `data/`. Set automatically by `setup.sh`; if empty, the app uses the owner of `data/db`. | | `SEARCH_REINDEX` | No | Set to `true` to rebuild the search index on startup, e.g. after deleting `data/meili/`. Remove it afterwards. | ## Data Location @@ -93,6 +96,8 @@ data/ meili/ — Meilisearch Index ``` +Everything in `data/` is owned by the user set in `PUID`/`PGID`. If `setup.sh` reports files owned by another user (for example root), fix them once with the `chown` command it prints. + ## Backup and Restore Everything lives in `data/`, so a backup is a copy of that directory. Stop the app first so the SQLite database is in a consistent state: diff --git a/compose.dev.yaml b/compose.dev.yaml index 9f63b4d..88ab6ef 100644 --- a/compose.dev.yaml +++ b/compose.dev.yaml @@ -11,8 +11,12 @@ services: - JWT_SECRET=${JWT_SECRET} - MEILI_MASTER_KEY=${MEILI_MASTER_KEY} - DATABASE_PATH=${DATABASE_PATH} + - PUID=${PUID:-} + - PGID=${PGID:-} - FILES_PATH=${FILES_PATH} - MEILISEARCH_HOST=${MEILISEARCH_HOST} + # Build output and caches inside the image that the dev server writes to + - WRITABLE_DIRS=/app/.nuxt /app/node_modules/.cache volumes: - ./data/db:/app/data/db - ./data/files:/app/data/files @@ -37,6 +41,8 @@ services: profiles: [tools] environment: - DATABASE_PATH=${DATABASE_PATH} + - PUID=${PUID:-} + - PGID=${PGID:-} volumes: - ./data/db:/app/data/db command: node /app/scripts/setup-auth.mjs @@ -45,6 +51,8 @@ services: meilisearch: image: getmeili/meilisearch:v1.38.2 + # Run as the data folder owner so data/meili is not root-owned + user: "${PUID:-1000}:${PGID:-1000}" environment: - MEILI_NO_ANALYTICS=true - MEILI_ENV=development diff --git a/compose.traefik.yaml b/compose.traefik.yaml index fb67b9c..1fba908 100644 --- a/compose.traefik.yaml +++ b/compose.traefik.yaml @@ -7,6 +7,8 @@ services: - JWT_SECRET=${JWT_SECRET} - MEILI_MASTER_KEY=${MEILI_MASTER_KEY} - DATABASE_PATH=${DATABASE_PATH} + - PUID=${PUID:-} + - PGID=${PGID:-} - FILES_PATH=${FILES_PATH} - MEILISEARCH_HOST=${MEILISEARCH_HOST} volumes: @@ -31,6 +33,8 @@ services: profiles: [tools] environment: - DATABASE_PATH=${DATABASE_PATH} + - PUID=${PUID:-} + - PGID=${PGID:-} volumes: - ./data/db:/app/data/db command: node /app/scripts/setup-auth.mjs @@ -39,6 +43,8 @@ services: meilisearch: image: getmeili/meilisearch:v1.38.2 + # Run as the data folder owner so data/meili is not root-owned + user: "${PUID:-1000}:${PGID:-1000}" environment: - MEILI_NO_ANALYTICS=true - MEILI_ENV=production diff --git a/compose.yaml b/compose.yaml index 4fed3cd..7bc2505 100644 --- a/compose.yaml +++ b/compose.yaml @@ -9,6 +9,8 @@ services: - JWT_SECRET=${JWT_SECRET} - MEILI_MASTER_KEY=${MEILI_MASTER_KEY} - DATABASE_PATH=${DATABASE_PATH} + - PUID=${PUID:-} + - PGID=${PGID:-} - FILES_PATH=${FILES_PATH} - MEILISEARCH_HOST=${MEILISEARCH_HOST} volumes: @@ -26,6 +28,8 @@ services: profiles: [tools] environment: - DATABASE_PATH=${DATABASE_PATH} + - PUID=${PUID:-} + - PGID=${PGID:-} volumes: - ./data/db:/app/data/db command: node /app/scripts/setup-auth.mjs @@ -34,6 +38,8 @@ services: meilisearch: image: getmeili/meilisearch:v1.38.2 + # Run as the data folder owner so data/meili is not root-owned + user: "${PUID:-1000}:${PGID:-1000}" environment: - MEILI_NO_ANALYTICS=true - MEILI_ENV=production diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 855f431..d87f732 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,14 +1,26 @@ #!/bin/sh set -e -# Bind-mounted data directories can be root-owned (e.g. created by Docker on first run). -# Hand them to the unprivileged user, then drop privileges. +# Run the app as the user who owns the data folder, so everything it writes in data/ belongs +# to that user on the host. PUID/PGID (written to .env by setup.sh) take precedence; otherwise +# the owner of the database folder is used, falling back to 1000 if Docker created it as root. +# WRITABLE_DIRS lists extra directories the app must be able to write to (used in development). if [ "$(id -u)" = "0" ]; then - for dir in "$(dirname "${DATABASE_PATH:-/app/data/db/onearctic.db}")" "${FILES_PATH:-/app/data/files}"; do + db_dir="$(dirname "${DATABASE_PATH:-/app/data/db/onearctic.db}")" + files_dir="${FILES_PATH:-/app/data/files}" + mkdir -p "$db_dir" "$files_dir" + + uid="${PUID:-$(stat -c %u "$db_dir")}" + gid="${PGID:-$(stat -c %g "$db_dir")}" + [ "$uid" = "0" ] && uid=1000 + [ "$gid" = "0" ] && gid=1000 + + for dir in "$db_dir" "$files_dir" ${WRITABLE_DIRS:-}; do mkdir -p "$dir" - find "$dir" ! -user node -exec chown node:node {} + + find "$dir" \( ! -user "$uid" -o ! -group "$gid" \) -exec chown "$uid:$gid" {} + done - exec setpriv --reuid=node --regid=node --init-groups "$@" + + exec setpriv --reuid="$uid" --regid="$gid" --clear-groups "$@" fi exec "$@" diff --git a/setup.sh b/setup.sh index b796a9a..caaf4ca 100755 --- a/setup.sh +++ b/setup.sh @@ -24,9 +24,22 @@ if ! command -v openssl >/dev/null 2>&1; then exit 1 fi +# Everything in data/ should belong to the person running setup, even under sudo. +# Root itself maps to 1000, the default user the containers fall back to. +if [ "$(id -u)" = "0" ]; then + owner_uid="${SUDO_UID:-1000}" + owner_gid="${SUDO_GID:-1000}" +else + owner_uid="$(id -u)" + owner_gid="$(id -g)" +fi + # Create data directories mkdir -p data/db data/files data/meili -chmod 755 data/db data/files data/meili +chmod 755 data/db data/files data/meili 2>/dev/null || true +if [ "$(id -u)" = "0" ]; then + chown -R "${owner_uid}:${owner_gid}" data +fi echo "✓ Data directories ready" # Copy .env.example if .env doesn't exist @@ -37,19 +50,30 @@ else echo "✓ .env created from .env.example" fi -# Set a key in .env — only overwrites if the value is currently empty -set_env() { +# Write KEY=value to .env, adding the line if the key is missing (e.g. an older .env) +write_env() { local key="$1" local value="$2" - if grep -qE "^${key}=\s*$" .env 2>/dev/null; then + if grep -qE "^${key}=" .env; then if [[ "$OSTYPE" == "darwin"* ]]; then sed -i '' "s|^${key}=.*|${key}=${value}|" .env else sed -i "s|^${key}=.*|${key}=${value}|" .env fi - echo "✓ ${key} set" else + printf '%s=%s\n' "$key" "$value" >> .env + fi +} + +# Set a key in .env — only if it is missing or empty +set_env() { + local key="$1" + local value="$2" + if grep -qE "^${key}=.+" .env; then echo "✓ ${key} already set, skipping" + else + write_env "$key" "$value" + echo "✓ ${key} set" fi } @@ -57,11 +81,7 @@ set_env() { force_env() { local key="$1" local value="$2" - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "s|^${key}=.*|${key}=${value}|" .env - else - sed -i "s|^${key}=.*|${key}=${value}|" .env - fi + write_env "$key" "$value" echo "✓ ${key} set to '${value}'" } @@ -75,6 +95,18 @@ else force_env "COOKIE_SECURE" "true" fi +# The containers run as this user, so the files they write in data/ belong to you +force_env "PUID" "$owner_uid" +force_env "PGID" "$owner_gid" + +# Files written by an older setup (or by containers running as root) need a one-time fix +if [ -n "$(find data \( ! -user "$owner_uid" -o ! -group "$owner_gid" \) -print -quit 2>/dev/null)" ]; then + echo "" + echo "! Some files in data/ are owned by another user. Fix them once with:" + echo "" + echo " sudo chown -R ${owner_uid}:${owner_gid} data" +fi + echo "" echo "Done! Start the app with:" echo ""