Skip to main content

Installing Tagaris

Tagaris runs as two containers, the app and a PostgreSQL database, and is comfortable on a small VPS with 1 GB of memory. This guide takes you from an empty server to a signed-in administrator, then covers configuration, storage and upgrades.

Requirements

  • Docker and the Docker Compose plugin.
  • About 1 GB of memory and a little disk. More assets and photos need more disk.

Install with Docker Compose

1. Create a directory

mkdir -p /opt/tagaris && cd /opt/tagaris

Any directory works. Compose derives volume names from the directory name, so pick one and stay with it.

2. Create docker-compose.yml

Save the following as docker-compose.yml. It is complete: two services, three volumes, and every setting read from the .env file next to it.

services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s

app:
image: goodhallsolutions/tagaris:latest
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: ${DATABASE_URL}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
BETTER_AUTH_URL: ${BETTER_AUTH_URL}
BETTER_AUTH_TRUSTED_ORIGINS: ${BETTER_AUTH_TRUSTED_ORIGINS:-}
LICENCE_ENFORCED: ${LICENCE_ENFORCED:-true}
API_ENABLED: ${API_ENABLED:-true}
LICENCE_PUBLIC_KEY: ${LICENCE_PUBLIC_KEY:-}
LICENCE_SERVER_URL: ${LICENCE_SERVER_URL:-}
KEYGEN_VERIFY_KEY: ${KEYGEN_VERIFY_KEY:-}
SSO_ISSUER: ${SSO_ISSUER:-}
SSO_CLIENT_ID: ${SSO_CLIENT_ID:-}
SSO_CLIENT_SECRET: ${SSO_CLIENT_SECRET:-}
SSO_PROVIDER_NAME: ${SSO_PROVIDER_NAME:-}
NODE_ENV: production
UPLOADS_DIR: /app/uploads
PHOTOS_DIR: /app/data/photos
ports:
- "3000:3000"
volumes:
- uploads:/app/uploads
- photos:/app/data/photos
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/api/health || exit 1"]
interval: 15s
timeout: 5s
retries: 5
start_period: 40s

volumes:
pgdata:
uploads:
photos:

latest tracks the current release. To pin a release, use a version tag instead, for example goodhallsolutions/tagaris:2.2.0; the available tags are listed on Docker Hub.

3. Create .env

Save this as .env in the same directory, then change the two secrets and the URL. Each setting is explained in the configuration reference below.

# --- Database ---------------------------------------------------------------
# Letters and digits only in the password (it is spliced into DATABASE_URL).
# Generate one with: openssl rand -hex 24
POSTGRES_USER=tagaris
POSTGRES_PASSWORD=change_me_letters_and_digits_only
POSTGRES_DB=tagaris
DATABASE_URL=postgresql://tagaris:change_me_letters_and_digits_only@postgres:5432/tagaris?schema=public

# --- Auth -------------------------------------------------------------------
# BETTER_AUTH_URL is the public address people reach the app on, not localhost.
# Generate the secret with: openssl rand -base64 32
BETTER_AUTH_SECRET=change_me_generate_with_openssl_rand_base64_32
BETTER_AUTH_URL=https://assets.example.com
BETTER_AUTH_TRUSTED_ORIGINS=

# --- Licensing and API ------------------------------------------------------
# The official image bakes in working licence keys; leave the three blank.
LICENCE_ENFORCED=true
API_ENABLED=true
LICENCE_PUBLIC_KEY=
LICENCE_SERVER_URL=
KEYGEN_VERIFY_KEY=

# --- Single sign-on ---------------------------------------------------------
# Normally blank: configure single sign-on in the app instead.
SSO_ISSUER=
SSO_CLIENT_ID=
SSO_CLIENT_SECRET=
SSO_PROVIDER_NAME=

4. Start the stack

docker compose up -d

The app waits for the database healthcheck, applies any pending migrations, then listens on port 3000. The first start also pulls the images, so allow a minute or two.

5. Sign in

Open http://your-host:3000 in a browser. The first visit opens the setup wizard, which creates your organisation and the first administrator account. That account is the install owner: it holds the Application settings and is the break-glass login, so give it a strong password and store it safely.

6. Put it behind HTTPS

For anything beyond a first look on a private network, serve Tagaris through a reverse proxy that terminates TLS (Cloudflare, nginx or Caddy), and set BETTER_AUTH_URL to the https:// address. Session cookies and sign-in depend on it.

Install without Compose (plain docker run)

Compose is the recommended install, but the same two containers run with plain docker run if your host works that way (an appliance UI, for example):

docker network create tagaris
docker volume create tagaris_db tagaris_photos tagaris_uploads

docker run -d --name tagaris-db --network tagaris \
-e POSTGRES_USER=tagaris -e POSTGRES_PASSWORD=change_me -e POSTGRES_DB=tagaris \
-v tagaris_db:/var/lib/postgresql/data postgres:16-alpine

docker run -d --name tagaris --network tagaris -p 3000:3000 \
-e DATABASE_URL="postgresql://tagaris:change_me@tagaris-db:5432/tagaris?schema=public" \
-e BETTER_AUTH_SECRET="a_long_random_secret" \
-e BETTER_AUTH_URL="http://your-host:3000" \
-v tagaris_photos:/app/data/photos -v tagaris_uploads:/app/uploads \
goodhallsolutions/tagaris:latest

Migrations run automatically at start, and the first visit opens the setup wizard. There is no demo data in this image; the separate demo at demo.tagaris.co.uk is a different image with a dataset baked in.

Configuration

All settings are environment variables, read when the container starts: change a value in .env, then run docker compose up -d again to apply it.

VariablePurpose
POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DBDatabase credentials and name. Letters and digits only in the password: it is spliced into DATABASE_URL, so characters like : @ / break the URL.
DATABASE_URLConnection string the app uses. Uses the postgres service host in Compose. Keep the password in step with POSTGRES_PASSWORD.
BETTER_AUTH_SECRETSigns session cookies and encrypts stored secrets. Set a strong random value, keep it out of version control, and store a copy safely.
BETTER_AUTH_URLThe public URL the app is served from, for example https://assets.example.com. Used for auth callbacks, invite links, SSO and the QR label codes.
BETTER_AUTH_TRUSTED_ORIGINSOptional extra origins allowed to make auth requests, comma-separated. Useful for reaching a test instance by IP. Leave blank in production.
LICENCE_ENFORCEDWhether licensing is enforced. Default true. Set to false for development or evaluation only; see Licensing.
LICENCE_PUBLIC_KEY, LICENCE_SERVER_URL, KEYGEN_VERIFY_KEYLicence verification keys and the licensing service URL. The official image bakes in working defaults, so leave all three blank unless you build your own image or run your own licensing service.
API_ENABLEDWhether the REST API at /api/v1 is available. Default true. Set to false to turn the programmatic API off install-wide; it overrides the in-app toggle.
SSO_ISSUER, SSO_CLIENT_ID, SSO_CLIENT_SECRET, SSO_PROVIDER_NAMENormally left blank: single sign-on is configured in the app. Scripted or air-gapped installs can set these instead; the in-app configuration takes precedence when both are set.
UPLOADS_DIR, PHOTOS_DIRDirectories inside the container for uploaded files. Container paths, set in the compose file and backed by volumes.
DEMO_MODELeave unset. Used only by our public demo server: it blocks outbound and account-changing actions and resets the data nightly.
UMAMI_URL, UMAMI_WEBSITE_ID, UMAMI_REPLAYLeave unset. Used only by our public demo server to load Umami analytics. Unset means the app loads no analytics and makes no third-party calls.

BETTER_AUTH_URL deserves care. Set it to the address people actually reach the app on (its hostname or IP and port), not localhost. It is the base for printed QR label codes and emailed invite links: left at the default http://localhost:3000, those codes and links point at localhost and will not open on other devices. The same value is the allowed origin for sign-in requests (CSRF protection), so it must match the address in the browser's URL bar.

Persistent storage

The containers are disposable. Everything that must survive a rebuild lives in three volumes:

  • pgdata (mounted at /var/lib/postgresql/data): the PostgreSQL data directory, which is the register itself.
  • photos (mounted at /app/data/photos, path set by PHOTOS_DIR): uploaded asset photos, their thumbnails, and asset attachments such as receipts, invoices and manuals.
  • uploads (mounted at /app/uploads, path set by UPLOADS_DIR): other uploaded files, including the backups folder written by the in-app scheduled backups.

Back up all three. Rebuilding or updating the images never touches the volumes.

Named volumes or bind mounts

The compose file above uses named volumes, which Docker manages and stores under its own data directory. That is the right default: nothing to create, correct permissions from the start.

Prefer a bind mount when you want the data at a path you control: on a NAS or Unraid box, or when host-level backup tooling should see the files directly. Replace the volume references with host paths and drop the matching names from the top-level volumes: block:

postgres:
volumes:
- /srv/tagaris/pgdata:/var/lib/postgresql/data

app:
volumes:
- /srv/tagaris/uploads:/app/uploads
- /srv/tagaris/photos:/app/data/photos

The app behaves identically either way; only where the bytes live on the host changes.

Upgrading

Pull the new tag and start again. Pending database migrations run automatically when the app container starts, so an upgrade is:

docker compose pull
docker compose up -d

If you pinned a version, edit the tag in docker-compose.yml first, then run the same two commands. Your data lives in the volumes and carries straight across. Taking a backup before a major upgrade costs little and makes a rollback trivial; see Backups and restore.

After installing

  • First steps in the app: First steps adds a location, a person and your first asset.
  • Backups: Tagaris backs itself up from Application settings, with schedules, encryption and offsite copies. Backups and restore covers it all, including manual host-level copies and moving to a new server.
  • Users and roles: the free tier is a single administrator; a licence that grants multi-user lets an admin invite people as Admin, Editor or Viewer. See Roles and access.
  • Licensing: applying a purchased key needs no environment change on the official image; see Licensing.
  • Single sign-on with Microsoft Entra ID is a paid feature, configured in the app by the install owner. See Single sign-on.
  • Device sync with Microsoft Intune and Jamf Pro is a free beta, configured under Settings then Integrations. See Device sync.

Troubleshooting

  • QR labels or invite links point at localhost, or sign-in fails behind a proxy: BETTER_AUTH_URL must match the address in the browser's URL bar. Change it in .env, then run docker compose up -d again.
  • Database connection error at start (Prisma error P1013): the database password contains characters that break the connection URL, such as : @ /. Use letters and digits only, and keep DATABASE_URL in step with POSTGRES_PASSWORD.
  • A changed setting has no effect: environment variables are read when the container starts, so apply changes with docker compose up -d.
  • Slow first start: the first start pulls the images and waits for the database healthcheck, so allow a minute or two.

Update check and privacy

Tagaris does not phone home. There is no telemetry and no automatic update check. Licence keys are verified offline apart from the activation and renewal checks described in Licensing. Watch the repository or the changelog for new versions.