Skip to content

Containers

Khora’s native deployment goal is a small, self-contained executable that does not require a language VM or tracing garbage collector in the container image. The compiler is large — it carries LLVM inside itself — and none of it belongs in the image you ship, so the build is two stages and the second one copies a single file.

This example pins the stable 0.4.0 release and builds after it is published. For a published release candidate, set KHORA_VERSION=0.4.0-rc.2 and pin the application and its Khora-repository dependencies to the same candidate.

# --- build ------------------------------------------------------------------
FROM debian:trixie-slim AS build
# clang links the executable; the toolchain download cannot bring a linker with
# it. git is only needed if khora.toml has a git dependency.
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates clang curl git tar \
&& rm -rf /var/lib/apt/lists/*
# Pinned. A plain `curl | sh` takes the newest stable release, which makes the
# image depend on the day it was built.
ARG KHORA_VERSION=0.4.0
RUN curl -fsSL https://raw.githubusercontent.com/codyspate/khoralang/main/install.sh \
| sh -s -- --version "${KHORA_VERSION}" --to /opt/khora --no-modify-path
ENV PATH="/opt/khora/bin:${PATH}"
WORKDIR /src
COPY . .
RUN khora install \
&& khora build . --release --out /out/myservice
# --- run --------------------------------------------------------------------
FROM debian:trixie-slim
# ca-certificates only if the service opens outbound TLS: a TlsClient verifies
# against the machine's own trust store, and a slim image has none.
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --system --uid 10001 --home /var/lib/myservice --create-home myservice
COPY --from=build /out/myservice /usr/local/bin/myservice
USER 10001
WORKDIR /var/lib/myservice
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/myservice"]
Terminal window
docker build --build-arg KHORA_VERSION=0.4.0 -t myservice:1 .
docker run --rm -p 8080:8080 myservice:1

The published Linux artifact is dynamically linked against the system C library, and static and musl builds are not produced or tested (Supported targets). So the runtime image has to supply a glibc at least as new as the one the binary was linked against. Same distribution and same release in both stages is the way to stop thinking about it, and which release is not free to choose: the published toolchain needs glibc 2.39 or newer, so bookworm cannot run it and trixie can. That is why both stages above are trixie and not the more familiar bookworm; the table on Supported targets is the list. A distroless runtime image works for the same reason and is smaller, as long as it is the Debian 13 variant; Alpine does not, because it is musl.

Nothing else from the build stage is copied. There is no std/, no compiler, and no toolchain in the final image: the executable carries the Khora runtime inside it, which the release workflow checks on every release by running a packaged toolchain’s output with nothing else on the path.

There is none, and this is the part to plan around. Cross-compilation is not supported: the compiler can emit for another triple under KHORA_TARGET, which checks code generation, but the linker and sysroot story is unfinished, so a deployable artifact means building on the platform it runs on. linux/arm64 is not built or tested by CI at all.

In practice that means docker build on an x86_64 Linux machine or runner — --platform linux/amd64 if your daemon would otherwise pick something else — and not docker buildx fan-out across architectures.

Expose the readiness and liveness endpoints your orchestrator expects as ordinary routes; there is nothing Khora-specific in them. A probe that sends HEAD — a Docker HEALTHCHECK running curl -I, a load balancer target group configured that way — is answered by the GET route without a second mount, with the headers GET would have sent and none of the body. So a health route is one route however the orchestrator asks for it.

Shutdown works, with edges worth stating plainly. The SIGTERM a runtime sends before its grace period becomes a cancellation at the root of the program: nursery cancellation runs, scoped finalizers run, in-flight requests unwind, and the process exits 130. The grace period stays the orchestrator’s — --stop-timeout, or Kubernetes’ terminationGracePeriodSeconds — because a second deadline inside the runtime would be the one number nobody configured. A second SIGTERM restores the default disposition and re-raises, so an operator who means it is not waiting on a slow finalizer.

Two shapes are not covered and a drain at the layer above is still the answer for them: a main that reaches no cancellation point at all – no loop, no blocking call, no call to a function with one – has nowhere to stop, so the runtime kills the process outright; and a fiber inside a blocking connect_to reaches no cancellation point until the kernel gives up. Writing handlers so that being killed between two instructions is recoverable at the next start remains worth doing — SIGKILL at the end of the grace period has not gone anywhere.

Khora 0.4.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource