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 versionInstall 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 bashCould 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 bashCould 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 bashYour 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 statusIt 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 reportingstatus=203/EXECbecause the unit names an interpreter such as/usr/bin/python3.10that the upgrade removed. Thejt-proxensecommand 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 bashThe 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-proxenseCannot 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 adminCompletely 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 listUninstall
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 -- --yesSomething 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.
No entry matches that. Try fewer words, or open an issue.