Install & upgrade troubleshooting

What to do when the installer stops, an upgrade does not take, or the service will not come back up. Search the exact error text — every entry here is a failure path that really exists in the installer or that a release actually changed.

Start here

The installer stopped. Where do I look?

The installer prints a red ✗ line naming what failed. That line is the thing to search for on this page.

If the install finished but the service is unhealthy, the service's own log is the next place:

sudo journalctl -u jt-proxense -n 50 --no-pager

Check which version is actually installed:

jt-proxense version

Install fails

This installer must run as root

The one-liner needs sudo. It creates a system user, writes a systemd unit and installs into /opt.

curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-proxense/main/install.sh | sudo bash

Note the sudo goes on bash, not on curl.

No supported package manager found / Installing the prerequisites with … failed

The installer resolves system packages through apt, dnf, yum, pacman or zypper. On a distribution it does not recognise, or when that step fails (an unreachable mirror, a package your repositories do not carry), install the prerequisites by hand and then re-run it — it skips what is already present:

git, python3 (3.10+), python3-pip, python3-venv, systemd

Installers before v1.2.0 also asked for curl, which on RHEL / Rocky / Alma 9 conflicts with the default curl-minimal and stopped the install at step 2 with no message. v1.2.0 skips it when curl is already there.

Python 3.10+ required (found ...)

Most common on RHEL / Rocky / AlmaLinux 8, which ship Python 3.6 as python3:

sudo dnf install python3.11 python3.11-pip
curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-proxense/main/install.sh | sudo bash

The installer finds the newer interpreter and builds its virtual environment from it, so you do not need to change the system default.

/opt/jt-proxense is non-empty but not a git checkout. Refusing to overwrite.

Something is already in the target directory that the installer did not put there — an old manual copy, or an extracted archive. It refuses rather than deleting your files.

Move it aside and re-run:

sudo mv /opt/jt-proxense /opt/jt-proxense.old
curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-proxense/main/install.sh | sudo bash

Or install somewhere else:

curl -fsSL … | sudo JT_PROXENSE_INSTALL_DIR=/opt/jtp bash

Your data is not in that directory — users, audit log and settings live in /var/lib/jt-proxense and /etc/jt-proxense, and survive this.

Smoke test failed — a runtime module did not import

Python dependencies installed but one of them will not load. See the exact module name in the line above the failure. Since v1.2.0 the dependencies live in the installer's own virtual environment, /opt/jt-proxense/.venv; rebuilding it from scratch fixes a half-finished install:

sudo rm -rf /opt/jt-proxense/.venv
curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-proxense/main/install.sh | sudo bash

Do not install them into the system Python with --break-system-packages instead: see the entry below for what that breaks.

Cannot uninstall cryptography … no RECORD file / pip could not install the Python dependencies

Installers before v1.2.0 put the dependencies into the system Python. On a system whose distribution ships an older copy of a dependency as its own package, pip then has to replace it and cannot:

error: uninstall-no-record-file
× Cannot uninstall cryptography 46.0.5
hint: The package was installed by debian.

Seen on Ubuntu 26.04: the current asyncssh needs cryptography 48 or newer, and the distribution ships 46. Since v1.2.0 the installer uses its own virtual environment and never touches the system's packages, so running the one-liner again is the whole fix:

curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-proxense/main/install.sh | sudo bash

Do not work around it with pip install --ignore-installed or by removing python3-cryptography: a newer copy in /usr/local shadows the distribution's and breaks its own pyOpenSSL (module 'lib' has no attribute 'GEN_EMAIL'), and removing the package removes everything that depends on it.

Any other pip failure is usually the network: the host must reach pypi.org and files.pythonhosted.org (see the network entry for a proxy).

Could not upgrade pip to 26.2.2 or later / pip … is older than 26.2.2

The installer refuses to download anything with a pip older than 26.2.2. In October 2026 the private keys of eight eMudhra / emSign intermediate certificate authorities leaked, and pip carries its own copy of the CA list (certifi) besides the system one: every older pip still trusts certificates signed with those keys, so someone in the network path could pose as PyPI. Updating the operating system's certificates does not change the copy inside pip.

The usual cause is that the upgrade itself could not be downloaded: the host cannot reach pypi.org, or an internal package mirror or proxy has not picked up pip 26.2.2 yet. Fix that (see the network entry), update the system certificates, then re-run:

sudo apt-get update && sudo apt-get install --only-upgrade ca-certificates   # or: sudo dnf upgrade ca-certificates
curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-proxense/main/install.sh | sudo bash
Could not create a Python virtual environment (venv/ensurepip missing)

The installer keeps the dependencies in /opt/jt-proxense/.venv, and Debian / Ubuntu ship the module that creates one as a separate package. The installer asks for it, but a minimal image or a locked-down mirror can still lack it. Install it for the Python version the error names, then re-run:

sudo apt-get install python3-venv python3.12-venv
curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-proxense/main/install.sh | sudo bash
Could not fetch … / git clone or curl fails / the host cannot reach GitHub

The installer needs github.com and raw.githubusercontent.com. Behind a proxy, export it for the whole command — a proxy set only for your shell will not reach the sudo environment:

curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-proxense/main/install.sh \
  | sudo https_proxy=http://proxy:3128 bash

For a fully air-gapped host, clone the repository elsewhere, copy it to /opt/jt-proxense, and run install.sh from inside it.

Upgrade fails

How do I upgrade? Is git pull enough?

Re-run the same one-liner. It is idempotent and it is the upgrade path:

curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-proxense/main/install.sh | sudo bash

A bare git pull is not enough. It fetches code but does not reinstall the systemd unit, refresh Python dependencies, or apply the file ownership and permission repairs — so you end up running new code under an old unit. If you have already done that, just run the one-liner afterwards; it repairs the rest.

Which version does the installer put on? Can I pin one?

Since v1.1.0 it installs the newest release tag, not the tip of main — so the same command run on two different days gives the same code, and there is something to roll back to.

Pin a specific release, or deliberately follow the development branch:

# a specific release
curl -fsSL … | sudo JT_PROXENSE_REF=v1.1.0 bash

# the development branch
curl -fsSL … | sudo JT_PROXENSE_REF=main bash
Your local changes would be overwritten / dubious ownership

The installer resets the checkout to the release it is installing, so edits made inside /opt/jt-proxense are discarded by design. Keep changes in config.yaml, which is never overwritten.

If git refuses with detected dubious ownership, it is because the checkout is root-owned (deliberate since v1.1.0 — the service must not be able to modify its own code). Run git as root, or let the installer do it:

sudo git -C /opt/jt-proxense status
It says it upgraded, but the UI still shows the old version

Check what the server actually runs first:

jt-proxense version
sudo systemctl status jt-proxense --no-pager | head -5

If that is the new version, your browser is holding the previous index.html. The page detects a stale bundle and reloads itself, but a hard refresh settles it immediately: Ctrl+Shift+R (⌘+Shift+R on macOS).

How do I roll back to the previous version?

Install the earlier tag. Your database and settings are outside the install directory and are not touched:

curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-proxense/main/install.sh \
  | sudo JT_PROXENSE_REF=v1.1.0 bash

Database migrations are forward-only. Rolling back across a release that added one is not supported — take a copy of /var/lib/jt-proxense/jt-proxense.db before a major upgrade if you want a way back.

The service will not start

REFUSING TO START: config.yaml could not be parsed

The file is not valid YAML. The daemon refuses rather than falling back to defaults, because those defaults turn authentication off and bind every interface — a broken file must never quietly open an instance up.

Restore the copy taken before the last change:

ls -lt /opt/jt-proxense/config_backups/
sudo cp /opt/jt-proxense/config_backups/config_YYYYMMDD_HHMMSS.yaml \
        /opt/jt-proxense/config.yaml
sudo chown jt-proxense:jt-proxense /opt/jt-proxense/config.yaml
sudo chmod 600 /opt/jt-proxense/config.yaml
sudo systemctl restart jt-proxense

A setting this version does not recognise is not an error — since v1.1.1 it is ignored with a warning naming the key, and everything around it is kept.

systemd: Failed to start / Unit ... failed with a ReadWritePaths error

Since v1.1.0 the unit names each writable path individually, and systemd refuses to start a unit whose ReadWritePaths mentions something that does not exist. Re-running the installer creates them:

curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-proxense/main/install.sh | sudo bash

The paths are /opt/jt-proxense/config.yaml, /opt/jt-proxense/config_backups, /opt/jt-proxense/.ssh, /var/lib/jt-proxense and /etc/jt-proxense. This happens if the unit was updated by hand rather than through the installer.

After an OS upgrade: No module named 'aiohttp' / 203/EXEC / Python package not installed for this interpreter

An operating-system release upgrade (for example Ubuntu 22.04 → 24.04, Debian 12 → 13) replaces the system Python with a newer version. The dependencies were installed for the old one (into the system Python before v1.2.0, into /opt/jt-proxense/.venv since) and the new one cannot use them, so jt-proxense loses all of them at once. It usually shows only after the reboot that finishes the upgrade: until then the old process is still running.

What you see depends on the version that installed it:

  • v1.2.0 and later: the service stops once with the Python package 'aiohttp' is not installed for this interpreter, naming the Python the dependencies were installed for.
  • Older installs: ModuleNotFoundError: No module named 'aiohttp' (or 'yaml'), or systemd reporting status=203/EXEC because the unit names an interpreter such as /usr/bin/python3.10 that the upgrade removed. The jt-proxense command fails the same way.

Re-run the installer. It rebuilds the dependencies for the current Python, rewrites the unit and the jt-proxense command, and keeps your configuration, users and data:

curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-proxense/main/install.sh | sudo bash

Do it again after every OS release upgrade. The packages left under the old version's directory (for example /usr/local/lib/python3.10) are no longer used.

Address already in use / port 8098 is taken

Find what holds it, then either stop that or move jt-proxense:

sudo ss -tlnp | grep 8098

To move it, edit server.http_port in /opt/jt-proxense/config.yaml and restart. On a fresh install you can set it up front:

curl -fsSL … | sudo JT_PROXENSE_PORT=8099 bash
The page loads but has no styling, or the layout is flat

The browser got the JavaScript but not the stylesheet, usually because the files are not readable by the service account after a manual copy:

sudo chmod 644 /opt/jt-proxense/dist/index.html \
                /opt/jt-proxense/dist/assets/index-*.js \
                /opt/jt-proxense/dist/assets/index-*.css
sudo systemctl restart jt-proxense

Re-running the installer fixes this too, and is the safer choice.

Adding a cluster from the UI fails to save

The service writes config.yaml and takes a backup first, so both must be writable by the service account. Editing the file with a root shell redirect changes its owner:

sudo chown jt-proxense:jt-proxense /opt/jt-proxense/config.yaml
sudo chmod 600 /opt/jt-proxense/config.yaml
sudo chown -R jt-proxense:jt-proxense /opt/jt-proxense/config_backups
sudo systemctl restart jt-proxense

Cannot sign in

Too many attempts, please try again later

That IP hit the login rate limit: five failed attempts in five minutes. It clears itself after the cool-down, or immediately:

sudo jt-proxense unlock            # list what is locked
sudo jt-proxense unlock --all      # clear every lockout

This only clears the counter. It never changes a password.

Forgot the admin password, or lost the authenticator

The CLI works without the service running — that is what it is for:

sudo jt-proxense reset-password admin
sudo jt-proxense user reset-totp admin
Completely locked out after an auth misconfiguration

Turn authentication off, fix the configuration, turn it back on:

sudo jt-proxense auth disable       # also binds to 127.0.0.1 only
sudo systemctl restart jt-proxense

Re-enable it with sudo jt-proxense auth set-local.

After upgrading to v1.1.0 or later

Everyone gets locked out after a few failed logins

The most likely upgrade surprise. X-Forwarded-For is now trusted only from loopback or an address you name. If your reverse proxy runs on another host, every request now carries the proxy's address, so all users share one rate-limit bucket — five failed logins from anyone locks out everyone for fifteen minutes. The audit log records the proxy as the source too.

Name the proxy in /opt/jt-proxense/config.yaml:

auth:
  trusted_proxies:
    - 192.0.2.10        # or a CIDR: 192.0.2.0/24

Then sudo systemctl restart jt-proxense and sudo jt-proxense unlock --all. The log says ignoring X-Forwarded-For from … once per peer while this is happening.

The UI stops working when served from another origin

Cross-origin access is closed by default from v1.1.0. If you serve the SPA from a different host or port than the API, name that origin:

server:
  cors_origins:
    - https://ui.example.net

A * entry is ignored on purpose: a wildcard combined with credentials lets any site your browser visits drive this API with your session.

If the UI and API are the same origin — the default, because the app serves its own SPA — you need none of this.

The log says: ignoring unrecognised setting(s)

Informational. Your config.yaml contains a key this version does not know — usually a typo, or an option from a different version. The key is dropped and everything else is honoured; check the spelling against config.example.yaml.

Before v1.1.1 this stopped the service from starting. If you are on v1.1.0 and the daemon refuses to start over a keyword argument, upgrade.

A user with a per-cluster role was refused before, and works now

Expected. Until v1.1.0 the role check read only the global (*) grant, so jt-proxense user grant bob cluster1 operator was ignored at the door. It is now honoured for endpoints under that cluster. Grants scoped to one cluster still do not apply to user administration or global settings.

Review who has what:

sudo jt-proxense user list

Uninstall

How do I remove it completely?

One line. It stops and disables the service, then deletes the install directory, /var/lib/jt-proxense (users, audit log, cluster secrets), /etc/jt-proxense (the master key) and the service user:

curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-proxense/main/uninstall.sh | sudo bash

It asks you to type remove first. Add --yes to skip that in a pipeline: … | sudo bash -s -- --yes.

This is irreversible. Export first if there is any chance you want the instance back — see the next entry.

I want to remove it but keep my settings and users

Export before uninstalling. The bundle holds config.yaml, the SQLite database (users, roles, audit log, notes) and the master key that decrypts the stored cluster secrets, in one passphrase-encrypted file:

sudo jt-proxense export-config /root/jt-proxense-backup.enc

Copy it somewhere off the host, then uninstall. To bring it back on any machine:

curl -fsSL https://raw.githubusercontent.com/jasoncheng7115/jt-proxense/main/install.sh | sudo bash
sudo systemctl stop jt-proxense
sudo jt-proxense import-config /root/jt-proxense-backup.enc --force
sudo systemctl restart jt-proxense

The bundle contains the master key, so treat it as a credential and delete it once imported.

The uninstaller says: run as root, or no terminal for confirmation

It needs sudo for the same reasons the installer does. The second message means it was piped with no TTY to read your confirmation from, which is what --yes is for:

curl -fsSL … /uninstall.sh | sudo bash -s -- --yes
Something was left behind after uninstalling

The uninstaller tolerates missing pieces, so a partial state clears with the same commands run by hand:

sudo systemctl stop jt-proxense; sudo systemctl disable jt-proxense
sudo rm -f /etc/systemd/system/jt-proxense.service /usr/local/bin/jt-proxense
sudo systemctl daemon-reload
sudo rm -rf /opt/jt-proxense /var/lib/jt-proxense /etc/jt-proxense
sudo userdel jt-proxense

If you installed somewhere else, substitute that path — the uninstaller honours JT_PROXENSE_INSTALL_DIR too.

Still stuck

Nothing here matches. How do I report it?

Open an issue with enough to reproduce it:

jt-proxense version
cat /etc/os-release | head -2
python3 -V
sudo journalctl -u jt-proxense -n 100 --no-pager

Redact PVE hostnames and API tokens before pasting. Never paste config.yaml as-is — it contains credentials.

Open an issue on GitHub

No entry matches that. Try fewer words, or open an issue.