diff --git a/.env.example b/.env.example index ec957eb..a35c90e 100644 --- a/.env.example +++ b/.env.example @@ -1,76 +1,31 @@ -# cp .env.example .env — and fill it in. Docker Compose reads .env from this -# directory, next to docker-compose.yml; nothing looks for it anywhere else. -# -# Back the finished .env up together with the database: without ENCRYPTION_KEY -# the stored notes cannot be read again, and a backup of one without the other -# is worth nothing. +# cp .env.example .env — Compose reads it from this directory. +# Back the finished .env up with the database: without ENCRYPTION_KEY the +# stored notes cannot be read again. -# Signs session tokens. Changing it logs everyone out. -# openssl rand -hex 32 +# Both: openssl rand -hex 32 JWT_SECRET= - -# Encrypts note bodies at rest. LOSING THIS MEANS LOSING THE NOTES. -# Must be exactly 32 bytes as hex. -# openssl rand -hex 32 ENCRYPTION_KEY= -# The first administrator. -# -# There is no public sign-up — accounts are created by an admin inside the app -# under Account → Administration. These two settle who the first one is: -# -# empty database → the account is created from both values -# existing users, → INKNOTE_ADMIN_USER names which of them becomes admin. -# no admin yet Without it, the oldest account is promoted. -# an admin exists → both are ignored entirely -# -# So they can never reset an existing password or take rights away; the worst -# they can do on a running instance is nothing. +# The first administrator. There is no public sign-up — every other account is +# created in the app under Account → Administration. Used only until an admin +# exists, then ignored, so they can never reset anything later. INKNOTE_ADMIN_USER= INKNOTE_ADMIN_PASSWORD= -# ── Where the data lives ── -# A plain directory on the Docker host — a bind mount, not a Docker volume, so -# that whatever backs up the host also backs up InkNote. Create it first; the -# container runs unprivileged and cannot create it for you: -# +# Where the notes are kept on the host. Defaults to /srv/inknote in production +# and ./data for development — leave it commented unless you want another path, +# since one .env serves both. Create the directory first, or Docker creates it +# as root and the server (uid 1000) cannot write: # mkdir -p /srv/inknote/{db,uploads} && chown -R 1000:1000 /srv/inknote +# INKNOTE_DATA_DIR=/srv/inknote # -# INKNOTE_DATA_DIR=/srv/inknote # dev compose defaults to ./data -# INKNOTE_UID=1000 # must own the directory above -# INKNOTE_GID=1000 -# -# The path must start with / or ./ — anything else Docker reads as the name of -# a named volume, and a name containing a slash simply fails. -# -# ── Putting it under /root ── -# /root is normally mode 0700, and uid 1000 cannot even pass through it, so the -# server would fail to write however the directory itself is owned. Two ways -# out; the first keeps the unprivileged container and is the better trade: -# -# A) let uid 1000 traverse /root, without making it readable: -# mkdir -p /root/inknote/{db,uploads} -# chown -R 1000:1000 /root/inknote -# chmod o+x /root # traverse only — ls /root still refused -# INKNOTE_DATA_DIR=/root/inknote -# -# B) run the container as root instead — no chown, no chmod, but a break-out -# lands on root rather than on an unprivileged user: -# INKNOTE_DATA_DIR=/root/inknote -# INKNOTE_UID=0 -# INKNOTE_GID=0 -# -# Neither is needed under /srv or /opt, which is why those are the default. +# Under /root also run "chmod o+x /root", or the server cannot reach it at all. +# That and the alternative: readme.md → Where the data lives. -# ── Optional tuning ── -# Leave unset to keep the defaults shown. -# MAX_VERSIONS_PER_NOTE=50 # versions kept per note -# VERSION_RETENTION_DAYS=90 # and for how long -# VERSION_COALESCE_MINUTES=5 # edits within this window share a version -# -# Only if the UI is served from a different host than the API. With a reverse -# proxy in front, or the bundled nginx, leave it empty — same-origin needs none. -# ALLOWED_ORIGINS=https://notes.example.com -# -# PORT is deliberately not here: the bundled nginx proxies to port 4000 by -# name, so changing it would only break that. +# ── Optional ── unset keeps the default shown +# INKNOTE_UID=1000 # must own INKNOTE_DATA_DIR +# INKNOTE_GID=1000 +# MAX_VERSIONS_PER_NOTE=50 +# VERSION_RETENTION_DAYS=90 +# VERSION_COALESCE_MINUTES=5 +# ALLOWED_ORIGINS= # only if the UI is on another host diff --git a/readme.md b/readme.md index dfc78e4..5e511a0 100644 --- a/readme.md +++ b/readme.md @@ -85,9 +85,42 @@ $INKNOTE_DATA_DIR/ ``` Default is `./data` for development and `/srv/inknote` in production; override -with `INKNOTE_DATA_DIR`. The container runs as uid/gid `1000` so the files stay -owned by a normal user — set `INKNOTE_UID` / `INKNOTE_GID` if yours differ, and -create the directory with matching ownership before the first start. +with `INKNOTE_DATA_DIR`. It is a default, not an extra path — set it and the +default location is never touched. + +**Create the directory before the first start.** Docker will happily create a +missing bind-mount source itself, but it creates it as `root:root`, and the +container runs as uid 1000 and then cannot write: + +```bash +mkdir -p /srv/inknote/{db,uploads} && chown -R 1000:1000 /srv/inknote +``` + +The path has to start with `/` or `./`. Anything else Docker reads as the name +of a named volume, and a name containing a slash fails outright. + +#### Keeping it under `/root` + +`/root` is mode `0700`, so uid 1000 cannot pass through it — the server fails to +write no matter who owns the directory underneath. Two ways out: + +```bash +# A) let uid 1000 traverse /root, without making it readable +mkdir -p /root/inknote/{db,uploads} +chown -R 1000:1000 /root/inknote +chmod o+x /root # traverse only — `ls /root` is still refused +# .env: INKNOTE_DATA_DIR=/root/inknote + +# B) run the container as root instead — no chown, no chmod, but a break-out +# lands on root rather than on an unprivileged user +# .env: INKNOTE_DATA_DIR=/root/inknote +# INKNOTE_UID=0 +# INKNOTE_GID=0 +``` + +A is the better trade: it keeps the unprivileged container, which is the layer +that matters if the app is ever exploited. Neither is needed under `/srv` or +`/opt`, which is why those are the default. > Back up the `.env` holding `ENCRYPTION_KEY` too. Note content is encrypted at > rest, so a copy of the database without that key cannot be read. @@ -111,14 +144,22 @@ touches production data. ### 2. Docker Deployment ```bash -# Generate a secure encryption key -export ENCRYPTION_KEY=$(openssl rand -hex 32) -export JWT_SECRET=$(openssl rand -base64 32) - -docker-compose up -d +mkdir -p /srv/inknote/{db,uploads} && chown -R 1000:1000 /srv/inknote +cp .env.example .env # then fill it in, see below +docker login gitea.honeyy.duckdns.org +docker compose pull && docker compose up -d +docker compose logs inknote-server | head # says which account became admin ``` -The server runs on port `4000`, the web UI on port `3000`. +`.env` needs `JWT_SECRET`, `ENCRYPTION_KEY` and — on a fresh install — +`INKNOTE_ADMIN_USER` plus `INKNOTE_ADMIN_PASSWORD`, since there is no sign-up to +create the first account with. On an install that already has accounts, +`INKNOTE_ADMIN_USER` alone decides which of them is promoted; without it the +oldest one is, which is rarely the one you meant. + +The web UI listens on port `3000`, the API only inside the compose network. If a +reverse proxy sits in front, bind the port to localhost (`"127.0.0.1:3000:80"`), +otherwise the app is reachable on the host's address without TLS. ---