Deploying with Docker
The telorun/node image ships a Telo kernel and CLI on top of a Node.js base. Your Dockerfile adds the manifest and a pre-warmed controller cache; the container runs telo <manifest> on start.
Two-stage Dockerfile
The canonical pattern: a build stage runs telo install to materialize .telo/, a production stage copies the warmed tree and does no network I/O at boot.
FROM telorun/node:1.4.2-slim AS build
WORKDIR /srv
COPY apps/my-app/ apps/my-app/
RUN telo install apps/my-app/telo.yaml
FROM telorun/node:1.4.2-slim AS production
WORKDIR /srv
COPY --from=build /srv /srv
CMD ["apps/my-app/telo.yaml"]
The image's ENTRYPOINT is telo, so CMD is just the application path (and any flags). Override at run time with docker run … <image> --watch ./manifest.yaml if you need to.
Warm the cache with telo install
telo install walks the manifest's Telo.Import graph, downloads every controller package, and writes both to <manifest-dir>/.telo/:
.telo/npm/— controllernode_modulestree, one realm per manifest..telo/manifests/…— every importedtelo.yaml, registry-served or HTTP-fetched.
Running this in the build stage means the production image is a hermetic snapshot. The kernel resolves every controller and every Telo.Import from disk — boot does zero network I/O, which is what makes the image safe to run in airgapped, scale-out, and cold-start scenarios.
Skip the warm-up and your container will pull controllers on every boot, suffer slow start times, and break entirely if it has no outbound network.
Image variants
| Tag | Base | Rust toolchain |
|---|---|---|
telorun/node:<ver> | debian | no |
telorun/node:<ver>-slim | debian-slim | no — recommended for production |
telorun/node:<ver>-rust-<rust-ver> | debian | yes — for controllers compiling native deps at install time |
telorun/node:<ver>-rust-<rust-ver>-slim | debian-slim | yes |
<ver> accepts an exact CLI version (1.4.2), a major (1), a major.minor (1.4), or latest. Pin to an exact version in production — rolling tags move with each release.
The -rust-* variants only need to be present in the build stage if your controllers compile native code at install time. Use the slim variant for the production stage either way; copying the warmed /srv tree across is a single COPY --from=build.
Configuring at runtime
Telo.Application reads host env vars declared in its variables: / secrets: blocks — see Application Environment Variables. Pass them with -e or via your orchestrator:
docker run --rm \
-e PORT=8080 \
-e DATABASE_URL=postgres://… \
-p 8080:8080 \
my-registry/my-app:1.0.0
No Config.Env resource is needed — the binding is declarative on the Application.
Compose example
services:
api:
image: my-registry/my-app:1.0.0
environment:
PORT: 8080
LOG_LEVEL: info
DATABASE_URL: ${DATABASE_URL}
ports:
- "8080:8080"
restart: unless-stopped
One-shot vs long-running
The same image runs both shapes — the difference is what the manifest declares.
- A
Telo.Applicationwhosetargets:areTelo.Serviceresources keeps the process alive (HTTP servers, workers, schedulers). The orchestrator's restart policy (compose, Kubernetes, ECS) handles failover. - A manifest whose
targets:areTelo.Runnableresources runs to completion and exits. Good for batch jobs, migrations, CI tasks, scheduled cron units.
Building and pushing
Standard OCI flow — nothing Telo-specific:
docker build -t my-registry/my-app:1.0.0 .
docker push my-registry/my-app:1.0.0
For multi-arch builds (e.g. shipping both linux/amd64 and linux/arm64):
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t my-registry/my-app:1.0.0 \
--push .
See also
- Installation & CLI — full
telo installreference, including registry and package-manager overrides. - Application Environment Variables — declaring
variables:/secrets:against host env. - AWS Lambda — the serverless alternative, for event-driven workloads.