Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
2 changes: 2 additions & 0 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions compose.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions compose.traefik.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand Down
22 changes: 17 additions & 5 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -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 "$@"
52 changes: 42 additions & 10 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,31 +50,38 @@ 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
}

# Set a key in .env — always overwrites
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}'"
}

Expand All @@ -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 ""
Expand Down
Loading