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.
| Variable | Purpose |
|---|---|
POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB | Database credentials and name. Letters and digits only in the password: it is spliced into DATABASE_URL, so characters like : @ / break the URL. |
DATABASE_URL | Connection string the app uses. Uses the postgres service host in Compose. Keep the password in step with POSTGRES_PASSWORD. |
BETTER_AUTH_SECRET | Signs session cookies and encrypts stored secrets. Set a strong random value, keep it out of version control, and store a copy safely. |
BETTER_AUTH_URL | The 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_ORIGINS | Optional extra origins allowed to make auth requests, comma-separated. Useful for reaching a test instance by IP. Leave blank in production. |
LICENCE_ENFORCED | Whether licensing is enforced. Default true. Set to false for development or evaluation only; see Licensing. |
LICENCE_PUBLIC_KEY, LICENCE_SERVER_URL, KEYGEN_VERIFY_KEY | Licence 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_ENABLED | Whether 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_NAME | Normally 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_DIR | Directories inside the container for uploaded files. Container paths, set in the compose file and backed by volumes. |
DEMO_MODE | Leave 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_REPLAY | Leave 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 byPHOTOS_DIR): uploaded asset photos, their thumbnails, and asset attachments such as receipts, invoices and manuals.uploads(mounted at/app/uploads, path set byUPLOADS_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_URLmust match the address in the browser's URL bar. Change it in.env, then rundocker compose up -dagain. - 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 keepDATABASE_URLin step withPOSTGRES_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.