Docker¶
The image runs indiserver and the INDIkit web bridge in one container, so a host
with Docker installed needs nothing else: no libindi, no Python, no Node.
docker run --rm -p 8000:8000 -p 7624:7624 \
-e WEB_TOKEN=choose-a-secret \
ghcr.io/sidereal-software/indikit
Then open http://localhost:8000/?token=choose-a-secret.
The token is not optional. The container publishes its port, so the bridge always
starts with one. Leave WEB_TOKEN unset and it generates a random token instead, then
prints the only URL that works to the container log:
indikit: generated a web token. Set WEB_TOKEN to keep it stable across restarts.
indikit: panel on http://localhost:8000/?token=Xb3...
Read that URL out of the log, or set WEB_TOKEN yourself as above and keep it stable
across restarts. Plain http://localhost:8000/ loads the panel either way, but its
WebSocket handshake is refused with close code 1008 and no devices ever appear.
The token covers the rest, including WEB_ALLOW_ANONYMOUS.
Which tag to pull¶
Images are published to the GitHub Container Registry under
ghcr.io/sidereal-software/indikit.
| Tag | Points at | Built for |
|---|---|---|
latest |
the newest release | linux/amd64, linux/arm64 |
0.2.0 |
that exact patch release, forever | linux/amd64, linux/arm64 |
0.2 |
the newest 0.2.x release |
linux/amd64, linux/arm64 |
edge |
the tip of main, rebuilt on every merge |
linux/amd64 |
sha-a1b2c3d |
one exact commit on main |
linux/amd64 |
Pin the patch tag for anything pointed at real hardware. An observatory that pulls
latest gets a different version the day you cut a release, usually in the dark, usually
not on purpose.
latest is the newest release, not the newest commit, which is why the commands on this
page can safely leave the tag off.
edge is for tracking development or reproducing a bug report against a known commit, and
it is opt-in by name for that reason. It is amd64 only: arm64 has to be emulated on a CI
runner, a release is the thing worth spending that on, and a Raspberry Pi wants a release
tag anyway. Release images are native on both, so an Apple Silicon Mac and an observatory
Pi each get a real build.
To build it yourself instead, from a checkout:
git clone https://github.com/sidereal-software/indikit
cd indikit
docker compose up --build
compose.yaml leaves WEB_TOKEN commented out, so the container generates one and
prints the URL that carries it:
indikit: panel on http://localhost:8000/?token=Xb3...
Open that URL. Plain http://localhost:8000/ loads the panel but never connects. Uncomment
WEB_TOKEN in compose.yaml and set it to keep one URL across restarts.
indiserver is on localhost:7624 at the same time, so KStars, PHD2 or any other INDI
client can drive the same hub while the panel is open. Those are INDI clients rather than
browsers, so they talk to indiserver directly and the bridge's token does not apply to
them.
With nothing configured the container runs libindi's telescope simulator, so there is a device on screen before you have written anything.
Running your own drivers¶
compose.yaml mounts the repository's drivers/ directory read-only at /drivers.
Everything in there is launched under indiserver alongside whatever INDI_DRIVERS
names:
cp my_driver.py drivers/
docker compose up
Two kinds of file are recognised:
- A
.pydriver written against the INDIkit SDK. It does not need to be executable: the container writes a shim that runs it under the interpreter the package is installed into, which is both howindiservergets something it canexecand how your driver getsindikiton its import path. - Any other executable program, meaning a compiled driver or a script with a
#!line.
Anything else is skipped, with a line in the log saying so.
To run a driver already in the image, or one you mounted somewhere else, name it in
INDI_DRIVERS. The example drivers are at /opt/indikit/examples, and indikit
below is the image tag docker compose builds:
docker run --rm -p 8000:8000 -p 7624:7624 \
-e INDI_DRIVERS="indi_simulator_ccd /opt/indikit/examples/focuser_device.py" \
indikit
Configuration¶
Two sets of variables reach this container, and which one to use follows from who reads it.
The container's own, read by docker/entrypoint.sh, which turns them into an
indiserver command line and indikit serve flags. They exist because the entrypoint
composes two processes, and no single indikit invocation can be configured to do that:
| Variable | Default | Effect |
|---|---|---|
INDI_DRIVERS |
indi_simulator_telescope |
Space-separated drivers: a name on PATH, a path to an executable, or a path to a .py file. |
INDI_DRIVER_DIR |
/drivers |
A directory whose entries are appended to that list. |
INDI_PORT |
7624 |
The port indiserver listens on. |
WEB_HOST, WEB_PORT |
0.0.0.0, 8000 |
Where the bridge binds. |
WEB_TOKEN |
generated | The shared token /ws and /api require. Left unset, one is generated and printed with the panel's URL on startup; set it to keep that URL stable across a restart. INDIKIT_TOKEN is accepted as the same setting. |
WEB_ALLOW_ANONYMOUS |
unset | Set to any value to serve with no token: the entrypoint then passes --allow-insecure-bind and no --token at all. Ignored when a token is set, which wins. Same as INDIKIT_ALLOW_INSECURE_BIND. |
WEB_ALLOWED_ORIGINS |
unset | Space-separated browser origins to accept in addition to the bridge's own. Same as INDIKIT_ALLOWED_ORIGINS, same format. |
INDIkit's own, read by indikit itself wherever it runs: in this container, on a
host install, or in a driver process indiserver launched. Docker passes them straight
through, so setting one on the service is all it takes:
| Variable | Default | Effect |
|---|---|---|
INDIKIT_LOG_LEVEL |
INFO |
Level for INDIkit and uvicorn: CRITICAL, ERROR, WARNING, INFO or DEBUG. |
INDIKIT_WIRE_LOG |
unset | Set to 1 for one log line per INDI message in each direction. BLOB payloads are reported by size, never printed. |
INDIKIT_CONNECT_TIMEOUT |
10.0 |
Seconds the bridge waits for each attempt to reach indiserver. |
INDIKIT_RECONNECT_DELAY |
2.0 |
Seconds between a lost upstream connection and the next attempt. |
INDIKIT_MESSAGE_HISTORY |
100 |
INDI message frames replayed to a browser that has just connected. |
INDIKIT_MAX_BACKLOG |
512 |
Live frames a browser may fall behind by before the bridge drops it and it reconnects. |
INDIKIT_TOKEN |
unset (generated in this image) | The shared token /ws and /api require. In this image, WEB_TOKEN. |
INDIKIT_ALLOWED_ORIGINS |
unset | Space-separated browser origins to accept. In this image, WEB_ALLOWED_ORIGINS. |
INDIKIT_ALLOW_INSECURE_BIND |
unset | Permit the non-loopback bind with no token anyway. It does not turn off a token that is set. In this image, WEB_ALLOW_ANONYMOUS, which also drops --token - see below. |
INDIKIT_CONFIG_DIR |
~/.indikit, else nowhere |
Where a driver's CONFIG_PROCESS saves and loads its properties. Set it here - see below. |
The last three have two names here, and they are the same setting. Set one of each
pair, not both. Prefer WEB_* in this image, and INDIKIT_* when you run indikit
yourself.
The entrypoint has to decide something about them before serve runs: it generates a
token when none was given, and prints the panel's URL with it. So it resolves them itself
and passes them as flags, which beat the environment. It reads WEB_TOKEN first and falls
back to INDIKIT_TOKEN, and likewise for the other two, so either spelling works and
the URL printed at startup is the one that opens.
ALLOW_INSECURE_BIND is narrower than its container spelling suggests. Being exact about
it matters, because it is the one variable here that gives something away.
On its own it only permits a non-loopback bind that has no token, which serve
otherwise refuses. It never disables a token that is configured.
What serves the panel anonymously in this image is the entrypoint. When
WEB_ALLOW_ANONYMOUS is set and no token is, it starts serve with
--allow-insecure-bind and no --token, so there is no token to check. Set a token
as well and the token wins, on a bind that no longer needs the permission.
A driver's saved configuration lives in the container's filesystem and dies with it.
An INDIkit driver that publishes CONFIG_PROCESS writes under INDIKIT_CONFIG_DIR,
and a libindi driver writes under $HOME/.indi. Neither survives docker run --rm or an
image upgrade.
Keep both by mounting a volume and pointing the variable into it:
docker run --rm -p 8000:8000 -p 7624:7624 \
-v indi-config:/config -e INDIKIT_CONFIG_DIR=/config \
ghcr.io/sidereal-software/indikit
Set it even if you do not care where the file lands. The default is computed rather than
fixed: ~/.indikit, else nothing at all. A container is exactly where that second
case turns up, because HOME is routinely unset or points somewhere read-only.
XDG_CONFIG_HOME will not move it either; this variable is the only thing that does.
With nowhere to write, every Save, Load and Purge comes back as a ConfigError naming
INDIKIT_CONFIG_DIR as the fix, and the panel reports the failure on the property.
That is deliberate. The alternative is a temporary directory that accepts every Save and
loses the lot on restart.
Turn the log up when a driver is not appearing. That is the usual first move:
docker run --rm -p 8000:8000 -p 7624:7624 \
-e INDIKIT_LOG_LEVEL=DEBUG -e INDIKIT_WIRE_LOG=1 \
ghcr.io/sidereal-software/indikit
The token¶
The container publishes its port, so the panel and /ws are reachable by anything that
can reach the host. /ws is the write surface: a frame sent there becomes an INDI new*
that moves hardware.
The bridge therefore always starts with a token, and the startup log prints the URL that carries it:
indikit: panel on http://localhost:8000/?token=Xb3...
Open that URL and the panel connects. It carries the token from its own address to the
WebSocket, which is the only place a browser can put one and the only route that accepts
?token=.
/api takes Authorization: Bearer <token> and nothing else, so curl and other
non-browser clients use the header there:
curl -H 'Authorization: Bearer Xb3...' http://localhost:8000/api/devices
/health needs no token, because the image's health check calls it. It reports liveness,
the upstream link and a few counters, and deliberately carries no release version, no
addresses and no device names.
The bridge's HTTP surface
has the body.
Browsers apply neither the same-origin policy nor CORS to WebSockets, so the bridge
also checks the handshake's Origin against its own and refuses anything else. A
front end served from another origin needs WEB_ALLOWED_ORIGINS.
The container exits when either process ends, so a driver that takes the hub down with
it is restarted by Docker rather than leaving half a stack still answering on :8000.
The health check reports healthy only once the bridge is answering and connected to
the hub.
Hardware¶
A container sees no USB device unless it is given one. Pass the port through on the
indikit service in compose.yaml:
devices:
- /dev/ttyUSB0:/dev/ttyUSB0
A driver that finds its instrument by broadcasting on the local network needs
network_mode: host instead. That cannot be combined with ports, so delete the port
mapping when you add it. The container is then on the host's network and its ports are
already the host's.
Neither is on by default, because the first thing most people run here is a simulator.
Building the image¶
docker build -f docker/Dockerfile -t indikit .
The build compiles the TypeScript panel with pnpm and installs the resulting wheel, so
the image serves the real panel and not the fallback debug page. The image runs as a
non-root user, and builds on amd64 and arm64.
Running the interop suite¶
The same Dockerfile has a test target holding the repository, the development
dependencies, a browser and libindi. That makes tests/interop/ runnable on a machine
with no libindi, which includes every Mac:
docker compose --profile interop run --rm --build interop
That runs pytest tests/interop against a real indiserver and real C++ drivers, which
otherwise happens only in the nightly CI job. Pass a command to narrow it:
docker compose --profile interop run --rm interop pytest tests/interop/test_smoke.py -q