Skip to content

Self-hosting

Nutria runs as two containers, the app and PostgreSQL, from the compose.yaml in the repository.

Nutria runs wherever Docker runs Linux containers: Linux, and macOS or Windows with Docker Desktop.

Clone the repository and check out the latest release. Replace vX.Y.Z with the newest version on the releases page.

Terminal window
git clone https://github.com/alvfdz/nutria.git
cd nutria
git checkout vX.Y.Z
cp .env.example .env

Three values are required. Compose refuses to start while the password or the secret is empty.

Variable What it is Requirements
POSTGRES_PASSWORD Database password Any strong password without @ / : #
BETTER_AUTH_SECRET Signs the session cookies A random string of at least 32 characters
BETTER_AUTH_URL The address you open Nutria at The full origin, e.g. http://localhost:3000 or https://nutria.example.com

Generate the password and the secret however you like. For example, with openssl:

Terminal window
openssl rand -hex 24 # POSTGRES_PASSWORD
openssl rand -base64 32 # BETTER_AUTH_SECRET

Optional settings:

Variable Default What it does
APP_PORT 3000 Port on the host
TZ UTC Your time zone (e.g. Europe/Madrid); decides when “today” starts
POSTGRES_USER, POSTGRES_DB — Database user and name
TRUSTED_PROXIES — Only behind a reverse proxy; see Reverse proxy
Terminal window
docker compose up -d --build --wait

Open BETTER_AUTH_URL (by default http://localhost:3000). A fresh install takes you to the setup screen to create your account; see First run.

  • The app container applies any pending database migrations every time it starts, then serves Nutria.
  • The database publishes no port: only the app can reach it, inside the compose network. Its data lives in a named volume and survives restarts and rebuilds.
  • Every response carries standard security headers and a strict Content Security Policy.

“The server refused this request” on the setup or login screen means Better Auth didn’t trust where the request came from. Check the app’s logs with docker compose logs app:

  • ERROR [Better Auth]: Invalid origin: https://nutria.example.com (or Invalid callbackURL: …): the address in your browser doesn’t match BETTER_AUTH_URL. Set it to exactly the address the line names (scheme, host and port) and restart with docker compose up -d --wait.
  • ERROR [Better Auth]: Blocked cross-site navigation login attempt: a page on another website tried to sign in to your Nutria, and Better Auth’s cross-site request forgery protection blocked it. Nutria’s own screens never trigger this, so there’s nothing to change.

“The server refused this request because the browser didn’t say where it came from” means the request reached Nutria without an Origin header, which leaves nothing in the logs. If Nutria is behind a reverse proxy, make sure it forwards the Origin header unchanged.

“The server ran into a problem” means the request reached Nutria but failed there, e.g. the database refused the new account. Check the app’s logs with docker compose logs app.