xunx · Multi-user shared service¶
xunx uses one image to maintain a long-lived container per user, then forwards requests by URL prefix through a single gateway process. Users see http://gateway:18960/alice/chat/, while the container still runs xuns --base-path /alice internally.
xunx user-add alice
xunx serve --host 0.0.0.0 --port 18960
Concepts¶
| Concept | Description |
|---|---|
| User store | SQLite database at {XUN_HOME}/x/xunx.db, fields name / token / generation / paused (databases from an older version get the missing columns added automatically) |
| Token | one random token per user (secrets.token_urlsafe(24)), which also serves as xuns --token=<token> for their container; joining it with = is what keeps a token starting with - from being parsed as an option |
| URL prefix | fixed to /{username}, so the container generates the correct relative links |
| Container name | xunx-<first 8 chars of instance ID>-<username>; the instance ID is a hash of the user store path |
| Port | a free port picked at random within --port-range, bound to 127.0.0.1 only (host port = container port) |
| generation | incremented by every upgrade; during reconciliation a stale generation triggers a container rebuild |
| paused | one persistent flag per user; reconciliation freezes or thaws the container with docker pause/unpause (the cgroup freezer) and never removes it, so process memory and saved sessions survive. The STATE column of user-list shows this flag (the desired state), not the live container state |
| reconcile | runs once when serve starts, then every --interval seconds: adopt existing containers, start missing ones, clean up out-of-range containers, sync the paused flag; removing orphaned containers happens only in the first reconciliation |
Subcommands¶
xunx user-add alice
User names may contain only letters, digits, _ and -; adding a duplicate raises an error. The output is a table of users and tokens, where the token is the access password.
xunx user-del alice # delete the user; their container is cleaned up on the next reconciliation
xunx user-list # list USER / TOKEN / BASE PATH / STATE (the desired state stored in the database)
xunx pause alice # freeze this user's container
xunx resume alice # thaw it; the container picks up exactly where it left off
Both commands only rewrite the paused flag in the database and print "The container state changes on the next serve reconciliation"; the running serve performs the actual docker pause/unpause on its next reconciliation, and a missing user is an error. Unlike upgrade, pausing does not discard anything inside the container.
xunx upgrade alice # or xunx upgrade --all
This only increments the generation in the database and prints "Queued upgrade"; the actual rebuild happens on the next serve reconciliation, and the data inside the container (workspace, saved sessions) is discarded with it.
| Parameter | Default | Description |
|---|---|---|
--host |
0.0.0.0 |
gateway listen address |
--port |
18960 |
gateway port (excluded from the available port pool) |
--port-range |
format START-END; the built-in default covers 17960-18958 |
port range available to containers; the default in code is range(17960, 18959), whose upper bound is exclusive, so 18959 is not in the default pool |
--image |
xun |
image to use |
--env |
empty | same meaning as xunc --env: NAME=VALUE assignment or wildcard forwarding; XUN_*/_XUN_* are always forwarded |
--interval |
5 |
reconciliation interval in seconds (hidden in the help output), must be greater than 0 |
How requests are forwarded¶
flowchart LR
U["browser<br/>/alice/chat/"] --> G["xunx gateway<br/>:18960"]
G --> R["user store<br/>names and tokens"]
G --> C["xuns in container<br/>--base-path /alice"]
C -.->|"relative links keep the /alice prefix"| G
- Paths are forwarded verbatim (including the
/aliceprefix and the query string), with no rewriting:xunsinside the container already knows it lives under/alicethanks to--base-path. - Unknown user name →
404 User not found; upstream unavailable →502 Upstream unavailable. - While a user's container is paused the gateway answers
503itself withRetry-After: 60: front-end paths (/<user>,/<user>/login,/<user>/chat/*,/<user>/docs/*) get a "Temporarily unavailable" HTML notice, every other path gets the plain textService paused. - WebSockets (the event stream) are supported, forwarded by two pumps with a 20 second heartbeat; hop-by-hop headers (
connection,transfer-encoding,upgrade, etc.) are stripped in both directions. - What the container prints is its own address,
http://0.0.0.0:<container port>/alice/chat/?session=%2F&token=<token>(plus thelocalhost:<container port>equivalent; host and container port are the same). To go through the gateway, swap inhttp://<gateway>:18960and keep the/aliceprefix.
Persistence and reconciliation¶
Restarting serve does not rebuild containers: during reconciliation it first adopts containers that are already running (or paused), matched by container name (reading their published port and labels), and rebuilds only when the token or generation is stale; containers belonging to deleted users or containers that have exited are stopped; pruning only touches containers labelled with this instance's xunx.instance label (the first reconciliation also clears orphaned containers this instance left behind), so containers of other instances are never disturbed. Logs of newly created containers are forwarded to serve's stderr, prefixed with the container name (containers adopted after a serve restart do not get their logs forwarded). Containers are created with auto_remove=True, so a docker daemon restart takes them with it: serve can only rebuild them at the next reconciliation, and any conversation stored inside the container goes with them.
Every reconciliation converges the containers onto the flags stored in the database: token and generation decide whether a container is rebuilt, paused decides whether it is frozen.
flowchart LR
S([serve starts]) --> R["running"]
R -->|"xunx pause"| P["paused"]
P -->|"xunx resume"| R
R -->|"xunx upgrade: rebuild"| R
P -->|"xunx upgrade: rebuilt, still frozen"| P
R --> E(["user deleted / container exited"])
P --> E
upgrade rebuilds the container from the newer image: if the user is marked paused, the freshly created container is frozen again straight away.
xunx user-list # the STATE column is the desired state in the database; containers catch up on the next reconciliation
xunx upgrade --all # after an image update, have all users rebuilt with the new image on the next reconciliation
Publishing it externally
When the gateway listens on 0.0.0.0, the token is the only credential; in production, put it behind an HTTPS reverse proxy and pass through X-Forwarded-Proto so that the login cookie gets Secure. By default uvicorn only trusts X-Forwarded-* coming from 127.0.0.1 / ::1; when the reverse proxy is not on this machine, FORWARDED_ALLOW_IPS has to be set as well.