Telecraft

Deploy with Compose

This is the smallest real deployment: one host, one Instance, and a reverse proxy in front of it. Everything the Instance reads is on the host, and nothing it needs comes from a network you do not run.

The files are in deploy/compose/, in the source at the release you are deploying:

File What it is
compose.yaml The Instance server, and an example terminator under a profile.
.env.example Every value the compose file reads. Copy it to .env.
proxy/telecraft.conf The terminator's configuration: the console on 443, and the OpAMP endpoint with its WebSocket upgrade carried through.

devenv/compose.yaml is a different file for a different job. It starts collectors to develop against and it deploys nothing.

Before you start

  • A host with a container runtime and the Compose plugin.
  • git on the host. The image carries none, so the host is what keeps the estate checkout current.
  • Two images: the Telecraft image, and the terminator's. Both are pulled once, and both can come from a mirror of your own.
  • An estate repository. Author and render is where one comes from; the steps below make a local one if you have no forge.

Copy the directory to wherever you keep deployment files, and work there:

mkdir -p /srv/telecraft
cp -r deploy/compose/. /srv/telecraft/
cd /srv/telecraft
cp .env.example .env

Create the estate repository

Two shapes work. If your estate lives on a forge, clone it and skip to Give somebody a way in:

git clone https://forge.example/acme/estate.git /srv/telecraft/estate

For a standalone or air-gapped Instance, the repository is a bare one beside the checkout, on the same host:

git init --bare /srv/telecraft/estate.git
git -C /srv/telecraft/estate.git symbolic-ref HEAD refs/heads/main
git clone /srv/telecraft/estate.git /srv/telecraft/estate

Push an estate into it. A fresh bare repository is empty, and an Instance serves what the renderer wrote, so the first push is the estate author and render produced: teams.yaml, the team directories, and the rendered/ tree.

Authors clone /srv/telecraft/estate.git over SSH, push branches to it, and merge in a checkout of their own. Nothing reaches a hosted service, and the Instance reads the checkout rather than the bare repository.

Point ESTATE_DIR in .env at the checkout:

ESTATE_DIR=/srv/telecraft/estate

Give somebody a way in

Sign-in starts with basic auth: one user the estate knows, hashed into a file under review. Hash a secret with the same image the Instance runs, where SECRET is the password you are setting:

printf %s "$SECRET" | docker run --rm -i ghcr.io/telecraft-dev/telecraft:release passwd

Put the hash in users.yaml at the root of your estate checkout, beside teams.yaml:

users:
  - email: [email protected]
    name: Jo Author
    owner: gateway-owners
    password: pbkdf2-sha256$600000$...

owner names an Owner in the team tree, and what Jo may author follows from it. Commit the file and push it, so the Instance serves a user the repository records:

git -C /srv/telecraft/estate add users.yaml
git -C /srv/telecraft/estate commit -m "Add Jo to the estate"
git -C /srv/telecraft/estate push

Place the session key

A session is a signed token. Draw a key so sessions survive a restart, and make it readable by the user the image runs as, 65532:

mkdir -p /srv/telecraft/secrets
head -c 32 /dev/urandom > /srv/telecraft/secrets/session-key
chown 65532 /srv/telecraft/secrets/session-key
chmod 0400 /srv/telecraft/secrets/session-key

Point SECRETS_DIR in .env at that directory. Every secret the Instance reads is a file in it, named by whatever names it.

Start it

docker compose up -d

The Instance answers on the loopback address:

curl -sS http://127.0.0.1:4321/readyz
ready

/readyz answers starting until the first snapshot is held, and /healthz answers while the process runs. Read the start-up lines with docker compose logs telecraft:

console and API on http://0.0.0.0:4321
OpAMP on 0.0.0.0:4320

Open http://127.0.0.1:4321 and sign in with the email and the secret you hashed. Collectors reach the OpAMP endpoint at ws://HOST:4320/v1/opamp, where HOST is this host's address. Serve configurations is what to put on the collector side.

Keep the checkout current

The Instance reads the estate directory again on every poll, so a merge arrives without a restart. What brings the merge down to the host is a git pull outside the container:

git -C /srv/telecraft/estate pull --ff-only

Run it on a timer, as often as you want changes to arrive. The Instance re-reads the directory every TELECRAFT_FETCH_INTERVAL, so the two together are how long a merge takes to reach a collector.

Put TLS in front

The Instance server holds no certificate. Both of its addresses speak plain HTTP, and something in front terminates TLS. An external URL naming a host that is not a loopback address, over plain HTTP, sends passwords and sessions across a network in clear text, and the process refuses to start on one.

Put the certificate and its key where the proxy reads them:

mkdir -p /srv/telecraft/tls
cp fullchain.pem privkey.pem /srv/telecraft/tls/

Then change four values in .env, so the Instance publishes on loopback and the proxy holds the public addresses:

TELECRAFT_EXTERNAL_URL=https://telecraft.example
TLS_DIR=/srv/telecraft/tls
CONSOLE_ADDRESS=127.0.0.1:4321
OPAMP_ADDRESS=127.0.0.1:4320

Start the profile that adds the terminator:

docker compose --profile tls up -d

The console is now at https://telecraft.example, and collectors connect to wss://telecraft.example/v1/opamp. Certificates come from whatever already issues them for this host: nothing in the deployment fetches one.

Any terminator you already run replaces this one. What the Instance needs from it is the console on one address, and the OpAMP endpoint with its WebSocket upgrade carried through.

Point at an identity provider

Basic auth is the bootstrap. Declare the providers your Instance offers in auth.yaml, at the root of the estate beside users.yaml:

providers:
  - kind: oidc
    name: staff
    issuer: https://issuer.example
    client_id: telecraft
    secret: staff-oidc
  - kind: basic

No field takes a value. secret names the secret, and the deployment places a file of that name beside the session key:

printf %s "$CLIENT_SECRET" > /srv/telecraft/secrets/staff-oidc
chown 65532 /srv/telecraft/secrets/staff-oidc
chmod 0400 /srv/telecraft/secrets/staff-oidc

Add an entry of that name to the secrets: block in compose.yaml, and list it on the service:

services:
  telecraft:
    secrets:
      - session-key
      - staff-oidc

secrets:
  staff-oidc:
    file: ${SECRETS_DIR}/staff-oidc

Commit auth.yaml and push it, the way you pushed users.yaml: who may sign in is under review like everything else the Instance reads. Register https://telecraft.example/api/v1/auth/staff/callback as the redirect URI with your provider, replacing staff with the name you gave the entry. Then docker compose --profile tls up -d again.

A file the estate names and the deployment never placed stops the start, and the message names the file, the name, and the directory searched. Drop - kind: basic when everybody signs in through the provider, and keep it while you are still proving the round trip.

Run an Instance has the same providers for a host process, SAML included.

Deploy without a network

Nothing in this deployment reaches past the host and the estate you point it at. Two images have to travel, and after that nothing is fetched.

Save them where you have a network, where VERSION is the release tag, such as v0.8.0:

docker save ghcr.io/telecraft-dev/telecraft:VERSION -o telecraft-VERSION.tar
docker save nginx:1.29-alpine -o proxy.tar

Load them where they are going:

docker load -i telecraft-VERSION.tar
docker load -i proxy.tar

If you push them into a registry of your own instead, TELECRAFT_IMAGE and PROXY_IMAGE in .env are the two values that change.

The console is inside the binary and reaches no other origin, and the image carries the Catalogue for the collector version it was built against. What the air gap still supplies from inside is the estate, the secrets, the certificate, and any Catalogue newer than the one the image carries. Run the container image copies the Catalogue out of the image, and activate a version is the rest of it.

Move to a new release

Set TELECRAFT_IMAGE in .env to the release you are moving to, then run the command you started with:

docker compose pull telecraft
docker compose up -d

Add --profile tls to both if that is how it runs.

The Instance holds nothing that outlives it: everybody is signed out for the length of the restart, collectors keep running the configuration they already hold, and the record is in the estate repository throughout. Every release names a digest, and TELECRAFT_IMAGE takes one in place of a tag when you want the bytes named rather than resolved:

TELECRAFT_IMAGE=ghcr.io/telecraft-dev/telecraft@sha256:DIGEST

DIGEST is the value the release notes carry.

What next

  • Stage a Rollout moves one Tier's population onto a new Blueprint version in cohorts.
  • Place a licence covers what an Enterprise Edition licence changes, and what happens without one.
  • Run the container image is the same image without Compose, and the whole CLI from the same artefact.
  • Deploy on Kubernetes is the same image on a cluster, where the pull that keeps the checkout current runs in a sidecar rather than on a timer.