DBSheriff on your own Linux host

This folder is the on-prem deployment: DBSheriff on a Linux server you manage, in your data centre or in your own cloud account. Nothing here calls back to Sun AI and Data LLC; the product is pulled by you, from us.

What you need

Minimum Notes
Host Linux x86_64: Ubuntu 22.04+, RHEL/Rocky 9+, Debian 12+, Amazon Linux 2023 one VM or bare-metal host; on GCP and Azure pick an Ubuntu 22.04+ image
Sizing 4 vCPU, 32 GB RAM, 100 GB disk (AWS r6i.xlarge, GCP e2-highmem-4, Azure Standard_E4s_v5) — sustains ~30 scored events/s with headroom (measured ceiling ~100); a 16 GB host (m6i.xlarge, e2-standard-4, Standard_D4s_v7) works if you enable only a few engines: Spark is an opt-in profile and stays off, and the Ollama container always runs but uses memory only once you pull a model into it, so on 16 GB leave the natural-language interface off or point it at an external endpoint in the Admin Console For 30-100 events/s use 8 vCPU / 32 GB (m6i.2xlarge, e2-standard-8, Standard_D8s_v7); for 100-200 events/s use 64 GB (r6i.2xlarge, e2-highmem-8, Standard_E8s_v5). Disk grows ~54 GB per 1 event/s of sustained rate at the default 90-day retention (images alone ~12 GB) — see sizing guide (inside the package) or installation guide (inside the package) to estimate your rate and size the disk
Software Docker Engine 24+ with the Compose v2 plugin, curl, tar, openssl cosign verifies the installer and the release signatures; without it the installer and the image check use cosign's container image, pinned by digest, through Docker (and with neither, the installer stops). Amazon Linux 2023: dnf install docker gives the engine but not the Compose plugin — install it from GitHub, see below
Network, connected install outbound HTTPS to get.dbsheriff.ai, releases.dbsheriff.ai, ghcr.io, Docker Hub (the third-party images, no login) and tuf-repo-cdn.sigstore.dev (Sigstore's trust root) none for an air-gapped install
From Sun AI and Data the install command with your download token, registry credentials (user name + read-only token), your .lic file all in your release e-mail
Root sudo for the install, or a user in the docker group that can write /opt/dbsheriff

Inbound: 443 for the bundled HTTPS front door (the default), plus 80 (it redirects to HTTPS, and Let's Encrypt acme certificates need it reachable from the internet), or, with internal / proxy access, the two consoles (8501 reporting, 8502 admin) and Grafana 3000, from wherever your users sit. Nothing else is published beyond the host itself: Kafka, Kafka UI, MLflow, the training database and Ollama listen on 127.0.0.1 (SIDE_BIND_ADDR). Docker's published ports bypass ufw and firewalld; see the installation guide, "Firewall". The host must reach every database you monitor on its normal port.

Public subnet, private subnet, or a private subnet with no internet, and how your people reach the consoles in each: the guide install-public-or-private-subnet (published beside this page, and in customer_docs/ of the package).

Installing Docker

bash sudo dnf install -y docker && sudo systemctl enable --now docker sudo mkdir -p /usr/local/lib/docker/cli-plugins U=https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-linux-x86_64 curl -fsSL "$U" -o /tmp/docker-compose # check the download against the checksum Docker publishes next to it; this must print "OK" curl -fsSL "$U.sha256" | awk '{print $1 " /tmp/docker-compose"}' | sha256sum -c - sudo install -m 0755 /tmp/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose docker compose version

Every DBSheriff container carries a memory limit (sizing guide (inside the package), Limits), so a runaway service is killed and restarted instead of taking the host down. Three host settings keep the host reachable, and its logs on disk, when memory does run short. Run them once as root:

# 1. never let the kernel's OOM killer pick sshd (or a cloud agent, where one runs)
for u in ssh sshd amazon-ssm-agent google-guest-agent walinuxagent; do
  systemctl cat "$u.service" >/dev/null 2>&1 || continue
  mkdir -p "/etc/systemd/system/$u.service.d"
  printf '[Service]\nOOMScoreAdjust=-1000\n' > "/etc/systemd/system/$u.service.d/10-dbsheriff.conf"
done
systemctl daemon-reload
# 2. a 4 GB swap file, used only when memory is really short (the containers cannot reach it)
fallocate -l 4G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
echo 'vm.swappiness = 10' > /etc/sysctl.d/90-dbsheriff.conf && sysctl -p /etc/sysctl.d/90-dbsheriff.conf
# 3. keep the system journal on disk, bounded — a kernel OOM message then survives a reboot
mkdir -p /var/log/journal /etc/systemd/journald.conf.d
printf '[Journal]\nStorage=persistent\nSystemMaxUse=1G\n' > /etc/systemd/journald.conf.d/10-dbsheriff.conf
systemctl restart systemd-journald

install.sh warns at pre-flight when the swap or the persistent journal is missing. The container health check (scheduled every minute: see the next section) mails when a container is at its limit, when one was OOM-killed, or when the host itself is short of memory (CONTAINER_HEALTH_MEM_PCT, 10 %), naming the containers and the .env variable that raises a limit. If the installer ended with "NOT scheduled: container-health" there is no such mail: check with deploy/onprem/stack.sh health and free -h.

Scheduled jobs: what a customer package schedules, and python3-venv

install.sh schedules six jobs on the host (systemd timers as root, cron otherwise). Three of them run Python: the container health check (every minute), the daily report (07:00) and the weekly model retrain (Sunday 02:00). All three are scheduled wherever the host can run them (the conditions are below). The customer package carries no application Python except the health check's: that job runs on the host's own python3 (the next paragraph), and the daily report and the weekly retrain run in containers of the stack and need only docker. The installer ends its run with one line for each job it could not schedule, and says what that job's absence costs.

The container health check (every minute) is scheduled. It reads the Docker socket and the host's memory and disks, so no container can run it for the host: the package carries its Python as plain source (scripts/container_health_check.py and six small modules under common/: the mail sender with its URL and TLS checks, the log-line cleaner and the audit-chain report), and the installer runs it with the host's own python3, in the virtual environment described below. Every minute it fills the System Health tab, and it mails the addresses in CONTAINER_HEALTH_ALERT_EMAILS (set it in /opt/dbsheriff/state/.env; with none it logs a warning, sends nothing, and the tab still fills), through the SMTP_* settings of .env, when a container is down, crash-looping, unhealthy, at its memory limit or killed for running out of memory, when the host runs short of memory or disk, when Redis starts evicting, or when the audit-chain check goes stale. Its log is /opt/dbsheriff/state/logs/container_health.log. What the host needs: python3 3.9 or later with its venv module (python3-venv on Debian and Ubuntu: see below); the docker command for the account that runs the job (root for the systemd timers, the account that ran the installer for the cron fallback, which must be allowed to use Docker); and, while the installer builds the virtual environment, a route to a Python package index (PyPI or your own mirror) for psycopg2-binary, requests and cryptography, installed from a hash-locked list. When the host cannot run the job the installer does not schedule one that would fail every minute: it ends with "NOT scheduled: container-health" and the reason. An air-gapped host has no package index (the image bundle carries no Python packages): build the environment once from your own mirror (pip reads your usual pip.conf or PIP_INDEX_URL), then run the installer's job step again:

sudo python3 -m venv /opt/dbsheriff/venv
sudo /opt/dbsheriff/venv/bin/pip install psycopg2-binary requests cryptography
sudo /opt/dbsheriff/current/deploy/onprem/scripts/install_cron_jobs.sh

On a customer host the log tail of a crashed container that goes into the mail is cleaned without the console's redaction module: a log line that holds a quote or a credential word appears as [line withheld: redaction failed] instead of being redacted, which keeps a literal out of your inbox at the cost of some context.

The daily report (07:00) is scheduled. Its script is not on a customer host, so scripts/daily_email_report_cron.sh runs it inside the admin console container (docker exec dbsheriff-<ENV_NAME>-admin python scripts/daily_email_report.py): the image carries the script and the container already holds the training-database login (ops_rw) and the SMTP_* settings. Only REPORT_RECIPIENTS, GRAFANA_URL and REPORT_DATE are read from .env by the wrapper and handed in, by name, so no secret is on a command line or in the log. A full-package host, where the script is on disk, runs it on the host with the ops venv as before. The installer schedules the job on a customer package when docker is on the PATH, and otherwise ends with "NOT scheduled: daily-email-report". The container uses the SMTP_* values it was created with, so after editing them in .env run deploy/onprem/stack.sh up. If the admin console container is not running at 07:00 the wrapper says so in state/logs/daily_email.log (exit 2) and no report goes out that day.

The weekly retrain (Sunday 02:00) is scheduled. model_training/ is not on a customer host, so retrain_and_deploy.sh runs each Python step in a throwaway docker run --rm container of the admin image (the image carries the training libraries, the compiled common/ and the four files of model_training/ the retrain needs; deploy/common/host/retrain_runner.sh decides this, RETRAIN_RUNNER in .env: auto, host, container). The Admin Console's Run Retrain button goes through the same script, handed over by the watcher job. The container runs as the user running the job, with every capability dropped, on the training database's network only, with a memory limit of 4g (RETRAIN_MEMORY_LIMIT; exit 137 in the log means it was hit) and the ops_rw login of the services; the model integrity key reaches one container, the signer's, by name. The model registry (state/mlruns, started on the first run from mlruns_seed.tar.gz, i.e. the model that shipped) and the record of the deployed version (state/.deployed_model_version) live in the state directory, so an upgrade keeps them. Step 1 of the run checks the admin container, the database network, a writable registry and that the image carries the retraining code (an image from before 1.4.0 does not), before anything is applied. The installer schedules the job when docker is on the PATH and otherwise ends with "NOT scheduled: weekly-retrain". A full-package host, where model_training/ is on disk, trains on the host in its virtual environment as before.

Detection, kill and lock, the audit trail and alerts run in the containers and are unaffected, and the nightly statistics refresh and retention pruning are scheduled. You can always check the stack yourself, and must when the container health check shows "NOT scheduled": deploy/onprem/stack.sh health (exit 1 if anything is not healthy), df -h /var/lib/docker and free -h. The nightly retention pruning runs the PostgreSQL client psql on the host, so install your distribution's PostgreSQL client package; without it that job writes "psql not found on PATH" to /opt/dbsheriff/state/logs/prune_training_db.log each night and prunes nothing.

The virtual environment and python3-venv. The container health check (and, on a host installed from the full package, the daily report) runs Python on the host, in a small virtual environment the installer builds next to the releases (/opt/dbsheriff/venv, from a hash-locked requirements file). Debian and Ubuntu ship python3 -m venv in a separate package, python3-venv, that minimal and cloud images lack. When the environment cannot be created, and the installer runs as root on a host with apt-get, it installs python3.X-venv (else python3-venv) with apt-get and tries once more, for at most five minutes in all (DBSHERIFF_OPS_VENV_APT_SECONDS, default 300; an air-gapped or firewalled host would otherwise wait out several download time-outs). That is the only package the installer installs.

If your change control does not allow that, install the package yourself before the installer runs (sudo apt-get install -y python3-venv), or switch the installer's apt-get off: DBSHERIFF_OPS_VENV_APT=false, either in the environment (sudo env DBSHERIFF_OPS_VENV_APT=false bash install-v1.4.0.sh ...; a plain sudo drops variables) or as a line in /opt/dbsheriff/state/host.env (<install dir>/state/host.env when you installed with --dir; create the file if the host has none), which every later run reads too. With the switch off and no virtual environment, the container-health timer stays off (and, on a full-package host, the daily report), and the container health check is what mails when a container is at its memory limit, was OOM-killed, or the host is short of memory; the installer prints that, with the command to run, and nothing else is affected. Re-run deploy/onprem/scripts/install_cron_jobs.sh once the package is there.

Install (connected host)

The commands below name the release v1.4.0 (1.4.0 without the v). Use the tag from your release e-mail; the copy of this page published at https://get.dbsheriff.ai/docs/<tag>/install.html has it filled in.

set -e
curl -fsSLO https://get.dbsheriff.ai/install-v1.4.0.sh
curl -fsSLO https://get.dbsheriff.ai/install-v1.4.0.sh.sigstore.json
cosign verify-blob --bundle install-v1.4.0.sh.sigstore.json \
  --certificate-identity https://github.com/dbsheriffai/dbsheriff/.github/workflows/release.yml@refs/tags/v1.4.0 \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  install-v1.4.0.sh
sudo bash install-v1.4.0.sh --version v1.4.0 --token '<download token>'

The installer runs as root, so it is verified before it runs (customer_docs/verify-the-installer.md, also published beside this page as verify-the-installer.html: the identity to expect, how to confirm it through a second channel, the same check through Docker without cosign, and a host that cannot reach Sigstore). The one-line form curl -fsSL https://get.dbsheriff.ai | sudo bash -s -- --version v1.4.0 --token '<download token>' is for a throw-away evaluation host only: it runs the script without checking it.

The installer then downloads and verifies the release (it stops when a signature is missing or does not verify, or when there is no cosign and no Docker to check it with; --skip-signature goes on without the check and says so), unpacks it to /opt/dbsheriff/releases/v1.4.0, points /opt/dbsheriff/current at it and starts this folder's install.sh, which asks:

  1. an environment name (default prod);
  2. passwords for Redis, the training database, Grafana and Kafka UI (press Enter to generate them);
  3. the Admin Console user name and password;
  4. the path to your .lic file (blank runs a 30-day demo; without a licence it also asks how many monitored servers the demo may register, blank = no limit);
  5. which engines you monitor (postgresql,mysql,sqlserver,oracle,mongodb,snowflake,databricks,bigquery,hana,redshift,spanner,synapse) — a licence that names engines only lets those run: their producers refuse to start and the Admin Console refuses their servers (contact sales@dbsheriff.ai to add one);
  6. how users reach the consoles: caddy (the default: bundled HTTPS on 443 with your certificate, Caddy's own CA, or Let's Encrypt), internal (plain HTTP on this host, unencrypted: VPN or tunnel users only) or proxy (your own reverse proxy in front). In caddy mode it then asks for the name or IP address users will type, the certificate source (internal, file, acme or acme-dns, each with the details it needs) and an optional allow-list of client networks. Details: docs/console-access.md;
  7. registry credentials, only if the image pull needs them;
  8. whether to start DBSheriff at boot (a systemd unit).

Then it pulls the images, starts the core services, applies the schema migrations, starts the rest of the stack, creates the console accounts, checks that every service is healthy and prints the URLs. A failed migration stops it before the scorer, the audit consumer and the producers are started.

Unattended install, same thing without questions:

sudo bash install-v1.4.0.sh --version v1.4.0 --token '<token>' -- \
  --yes --license /root/acme.lic --engines postgresql,mysql --access-mode caddy --hostname db-guard.acme.internal \
  --registry-user acme-pull --registry-token '<ghcr token>' --admin-password '<password>'

install.sh --help lists every option.

Install (air-gapped host)

On a connected machine, download from the links in your release e-mail: the customer package, SHA256SUMS, SHA256SUMS.sigstore.json, the image bundle dbsheriff-v1.4.0-images.tar.gz (about 13 GB) with its signed checksum dbsheriff-v1.4.0-images.tar.gz.sha256 (+ .sigstore.json), and the signed image manifest IMAGES-v1.4.0.txt (+ .sigstore.json). Verify (the full guide with every check is customer_docs/verify-your-download.md):

ID=https://github.com/dbsheriffai/dbsheriff/.github/workflows/release.yml@refs/tags/v1.4.0
ISS=https://token.actions.githubusercontent.com
cosign verify-blob --bundle SHA256SUMS.sigstore.json --certificate-identity "$ID" --certificate-oidc-issuer "$ISS" SHA256SUMS
sha256sum -c --ignore-missing SHA256SUMS
cosign verify-blob --bundle dbsheriff-v1.4.0-images.tar.gz.sha256.sigstore.json --certificate-identity "$ID" --certificate-oidc-issuer "$ISS" dbsheriff-v1.4.0-images.tar.gz.sha256
sha256sum -c dbsheriff-v1.4.0-images.tar.gz.sha256
cosign verify-blob --bundle IMAGES-v1.4.0.txt.sigstore.json --certificate-identity "$ID" --certificate-oidc-issuer "$ISS" IMAGES-v1.4.0.txt

Copy the package, the bundle and IMAGES-v1.4.0.txt (next to the bundle) to the host, then:

sudo mkdir -p /opt/dbsheriff/releases/v1.4.0
sudo tar --no-same-owner -xzf dbsheriff-1.4.0-v1.4.0-customer.tar.gz -C /opt/dbsheriff/releases/v1.4.0
sudo ln -sfn /opt/dbsheriff/releases/v1.4.0 /opt/dbsheriff/current
sudo /opt/dbsheriff/current/deploy/onprem/install.sh --airgap /path/dbsheriff-v1.4.0-images.tar.gz

The bundle is complete: the six DBSheriff images plus every third-party image the stack runs (Kafka, ZooKeeper, Redis, PostgreSQL, Grafana, Kafka UI, Ollama), each at the digest pinned in deploy/common/images.lock at release time, as one docker save archive per image inside the bundle (the installer unpacks it next to itself, so keep as much free space again). The installer checks every loaded image's id against the signed manifest before starting anything, and stops before loading if IMAGES-v1.4.0.txt is not in the bundle's directory. The host needs no registry access at all. Only the optional local language models (Ollama, used by the console's assistant) are not in the bundle; on an air-gapped host, load a model with ollama pull on a connected machine and copy the ollama-models volume, or leave the assistant on a hosted model.

Where everything is

Path What
/opt/dbsheriff/current link to the running release
/opt/dbsheriff/releases/<tag> one directory per installed release (none are removed for you; delete old ones yourself once you no longer need to roll back to them)
/opt/dbsheriff/state/.env your configuration; the release's .env is a link to it. Mode 600, holds secrets
/opt/dbsheriff/state/dbsheriff.lic your licence (mode 644: the services read it as an unprivileged user; a signed token, not a secret)
/opt/dbsheriff/state/installation_id the id your licence is bound to; keep it with backups
/opt/dbsheriff/state/install.log every install and upgrade run
Docker volumes dbsheriff-<env>_* databases, Kafka, Redis, the model, Grafana. Back these up: docs/backup-and-recovery.md

How secrets are handled after the install

The installer creates every secret DBSheriff needs and keeps nearly all of them in one file. This section says where each kind lives, who can read it, and the few things you should do once the install has finished.

Where each secret lives

Secret Where it is kept Shown to you
Admin Console and Reporting Console passwords Not kept in /opt/dbsheriff/state/.env once the installer has created the accounts (only the two user names are). The installer prints a generated password once, at the end of the run; a password you typed is not printed again. On an upgrade of an install whose accounts already exist, or if the seeding failed, INITIAL_ADMIN_PASSWORD and INITIAL_REPORT_PASSWORD stay in .env: blank those lines once you have signed in Once, at the end of the install
Passwords of the services (Redis, the training database, Grafana, Kafka UI, and with Kafka SASL one per service) /opt/dbsheriff/state/.env, mode 600, readable by root. Generated by the installer unless you typed one Never
Session and gatekeeper secrets of the consoles /opt/dbsheriff/state/.env Never
The encryption keys: SERVER_CREDENTIALS_KEY, CONSOLE_SECRETS_KEY, SCOPE_CREDENTIALS_KEY /opt/dbsheriff/state/.env. Each key is 32 random bytes, generated by the installer Never
The audit and evidence keys: AUDIT_ANCHOR_KEY, VAULT_KEK, SUBJECT_HMAC_KEY, EVIDENCE_SIGNING_KEY /opt/dbsheriff/state/.env Never
The credentials of the databases you monitor The registry in DBSheriff's own database, encrypted with AES-256 under SERVER_CREDENTIALS_KEY Never again after you save them
Your licence /opt/dbsheriff/state/dbsheriff.lic: a signed token, not a secret Not applicable

The three encryption keys, and who holds them

Key Encrypts Held by
SERVER_CREDENTIALS_KEY Every credential of every database you monitor: passwords, kill, lock, posture and classification accounts, connection strings, service-account keys The Admin Console, the services that connect to your databases (producers and collectors), the scorer and the analysers. Not the Reporting UI
CONSOLE_SECRETS_KEY What the consoles use themselves: the single sign-on client secret, the LLM API key, Slack webhook addresses, and the write credential of a cloud scope Both consoles and a few back-end services (the scorer, the query-performance analyser, the audit archiver, the identity profiler, the SIEM exporter). No producer and no collector holds it. The Reporting UI holds this key and not the first one, so a flaw in the console people reach from outside cannot open a database credential
SCOPE_CREDENTIALS_KEY The read credential you store for a cloud scope (Discovered Databases, the Azure Activity Log) The Admin Console and the one service that reads the Azure Activity Log, and nothing else

What to do after the install

  1. Copy /opt/dbsheriff/state/.env somewhere only you can reach, a password manager or your secrets vault, and keep it separate from the data backups. It is the only place the keys exist: a restored database without the key that sealed it still holds the credentials, but nobody can open them, and you would enter every monitored server's password again. Keep /opt/dbsheriff/state/installation_id next to it.
  2. Never replace a key by editing it. Changing SERVER_CREDENTIALS_KEY (or the other two) in .env makes everything already stored under the old value unreadable. Changing a key is a procedure that re-encrypts the stored values under the new one (sudo docker exec dbsheriff-<env>-admin python scripts/rotate_credentials_key.py ..., described in docs/runbooks/credential-keys.md in your release package; ask support@dbsheriff.ai if you do not have it). The installer never replaces AUDIT_ANCHOR_KEY, VAULT_KEK or SUBJECT_HMAC_KEY once it has set them, and you must not overwrite one by editing it either: each has its own rotation procedure, which puts the new value in the variable and keeps the old value in the matching _PREVIOUS variable (VAULT_KEK every 30 days, docs/runbooks/vault-keys.md section 3; AUDIT_ANCHOR_KEY, docs/runbooks/audit-integrity.md; SUBJECT_HMAC_KEY the same way, its old value staying in SUBJECT_HMAC_KEY_PREVIOUS for as long as an erasure tombstone names it). Replacing one without keeping the old value leaves what it protected unreadable or unverifiable.
  3. Change an ordinary setting by editing /opt/dbsheriff/state/.env and running sudo /opt/dbsheriff/current/deploy/onprem/stack.sh up, which recreates only what changed.
  4. Change a console password after your first sign-in. If you lose one, put INITIAL_ADMIN_PASSWORD (and INITIAL_REPORT_PASSWORD) back in .env and run sudo /opt/dbsheriff/current/deploy/onprem/stack.sh seed-users --reset.
  5. Change a monitored database's password in the database first, then in the Admin Console: Monitored Servers, edit the server, type the new password. A blank password field keeps the stored one. Or run create_reset_dbuser in reset mode: it sets a new password on the database account and updates the stored credential in one step.
  6. Do not put secrets in tickets. The setup tools print no secret value (a generated password goes into the registry, or into a new file with mode 600, never to the screen or a log). Send support stack.sh ps output and the log tail, never .env.

What the product does with a secret you give it

Day to day

cd /opt/dbsheriff/current
sudo deploy/onprem/stack.sh ps            # what is running
sudo deploy/onprem/stack.sh health        # exit 1 if anything is not healthy (on a customer package no timer runs this for you)
sudo deploy/onprem/stack.sh logs serve    # the streaming scorer
sudo deploy/onprem/stack.sh stop          # stop everything, keep data
sudo deploy/onprem/stack.sh up            # start again
sudo systemctl status dbsheriff      # if you chose the boot-time unit

Change a setting: edit /opt/dbsheriff/state/.env, then sudo deploy/onprem/stack.sh up (it recreates only what changed). Turning on automatic kill is not an .env setting: tick kill enabled for the server in the Admin Console's Monitored Servers tab; leave it off for a day or two first and watch the alerts.

Until the first database is registered, query-perf-scanner restarts every few seconds; stack.sh health reports it as waiting, not as a fault. Add databases in the Admin Console, Monitored Servers tab. Each needs a least-privilege monitoring account: docs/connecting-engines.md.

Upgrade

Download and verify the new release's installer (see Install, above) and run it with the new --version (and the new token from your e-mail). It unpacks the new release next to the old one, re-enters install.sh, which keeps your configuration, pulls the new images, switches current, starts the core services, applies the schema migrations, starts the rest and checks health. A failed migration stops the installer before the scorer, the audit consumer, the producers and the consoles are started or recreated, so those keep running. The core services (ZooKeeper, Kafka, Redis, the training database) were started from the new release and may have been recreated, and a container that depends on one can be left down: the installer prints what is not running (stack.sh health lists it again). Fix the cause and run the installer again (docs/runbooks/failed-migration-deploy.md). Roll back (the link alone starts nothing: install.sh --yes does, then check):

sudo ln -sfn /opt/dbsheriff/releases/<previous tag> /opt/dbsheriff/current
sudo /opt/dbsheriff/current/deploy/onprem/install.sh --yes
sudo /opt/dbsheriff/current/deploy/onprem/stack.sh health

Schema migrations are additive, so an older release runs against a newer schema.

Uninstall

cd /opt/dbsheriff/current && deploy/onprem/stack.sh stop
docker compose -f deploy/onprem/docker-compose.yml --project-directory . --profile '*' down -v   # -v deletes the data
sudo systemctl disable --now dbsheriff 2>/dev/null; sudo rm -f /etc/systemd/system/dbsheriff.service
sudo rm -rf /opt/dbsheriff

Getting help

docs/troubleshooting-and-logs.md first; then support@dbsheriff.ai with the output of deploy/onprem/stack.sh ps and the tail of /opt/dbsheriff/state/install.log.