kineuroExperimental Neuroradiology Research
Karolinska Institutet

Behind a proxy, and in a container

TLS in front, the desk as a service, and the image if you want one.

The wizard does this for you. This page is what it does, for when you would rather do it yourself.

Behind a reverse proxy

The desk does not terminate TLS. Put your own proxy in front, give the desk the public URL as its origin, and pass the request through unchanged:

bind = "127.0.0.1:7200"
origin = "https://nils.example.org"

Nothing else is required: no path rewriting, no header injection. The desk reads the request’s Origin and compares it with its own; a proxy that rewrites it will break the desk’s defences, so leave it alone.

With Traefik, a router to http://127.0.0.1:7200 is enough. With nginx:

location / {
    proxy_pass http://127.0.0.1:7200;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 300s;
}

The long read timeout is for the live update stream and the assistant’s long polls.

As a service

# ~/.config/systemd/user/nils-desk.service
[Unit]
Description=NILS desk
After=nils-engine.service

[Service]
ExecStart=/usr/local/bin/nils-desk serve --config %h/nils/desk/nils-desk.toml
WorkingDirectory=%h/nils/desk
Restart=on-failure

[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user enable --now nils-desk

In a container

Every release publishes an image, for x86-64 and arm64, and it needs no account to pull:

podman pull ghcr.io/kineuro/nils-desk:v1.0.0-beta.1

The tag is the desk’s release tag, so it begins with a v, and it is the desk’s own version, not the engine’s: take the newest desk release whose contracts.json your engine meets, as install the desk says. The image holds the desk binary and nothing else: the configuration, the sign-in store and the signing key all live in the volume you mount.

To build it yourself instead:

FROM docker.io/library/debian:trixie-slim
RUN groupadd -g 1500 nils && useradd -u 1500 -g 1500 -M -d /srv/nils nils \
 && mkdir -p /srv/nils/desk && chown -R nils:nils /srv/nils
COPY nils-desk /usr/local/bin/nils-desk
USER nils
WORKDIR /srv/nils/desk
EXPOSE 7200
ENTRYPOINT ["nils-desk"]
CMD ["serve", "--config", "/srv/nils/desk/nils-desk.toml"]
podman run -d --name nils-desk -p 127.0.0.1:7200:7200 \
  -v ~/nils/desk:/srv/nils/desk:U \
  ghcr.io/kineuro/nils-desk:v1.0.0-beta.1

The desk must reach the engine. On one machine, put both containers in a pod so they share a loopback:

podman pod create --name nils -p 127.0.0.1:7200:7200
podman run -d --pod nils --name nils-engine -v ~/nils/registry:/srv/nils/registry:U \
  ghcr.io/kineuro/nils:v1.0.0-beta.1
podman run -d --pod nils --name nils-desk -v ~/nils/desk:/srv/nils/desk:U \
  ghcr.io/kineuro/nils-desk:v1.0.0-beta.1

Inside a pod they reach each other on 127.0.0.1, so [engine] url = "http://127.0.0.1:8437" is right as written.

Start the engine first and it will say that the desk did not answer for its keys yet, then carry on and ask again when the first token arrives. That is expected in local mode, where the desk is the issuer the engine trusts: the two need each other, so neither waits for the other.

Next: Home and Settings.

Updated 17 October 2026