All docsStellar ControlMission control · by Stellar Systems v1.1.0

Deployment

Docker Compose

Stellar Control on one host with Docker Compose: NATS with JetStream and its volume, the configuration published at start, the core, a simulated target and the console; upgrades and backups.

Docker Compose runs Stellar Control on one host from a single file: NATS with JetStream on a volume, the configuration repository of the host compiled and published at each start, the services of the core in one container (stellar-control), a simulated target, and the web console on port 8080. It suits a lab, a test bench, or an air-gapped laptop (see Deployment Options). For a first try with plain docker run, see Docker.

Files#

text
stellar/
├── compose.yaml
├── .env              STELLAR_IMAGE and STELLAR_TAG
├── stellar.yaml      the global configuration
└── config/           the configuration repository

The image comes from the registry given with the delivery of Stellar Systems:

Shell
cat > .env <<'ENV'
STELLAR_IMAGE=<registry>/stellar-control
STELLAR_TAG=<version>
ENV

The global configuration (see Global Configuration):

YAML
nats:
  url: nats://nats:4222
api:
  listen: 0.0.0.0:8080
  web_dir: /usr/share/stellar/web
observability:
  log_format: json
  log_level: info

config/ is your configuration repository, a clone of its Git repository. To start from the example of the image (targets sim-1 and psu-sim-1):

Shell
docker create --name stellar-example <registry>/stellar-control:<version>
docker cp stellar-example:/usr/share/stellar/examples/config ./config
docker rm stellar-example

The files are mounted read only into containers running as the user stellar (uid 10001): keep them readable by others (chmod -R a+rX stellar.yaml config).

compose.yaml#

YAML
# Stellar Control on one host: NATS with JetStream, the configuration published at start, the
# core in one process, a simulated target, the console on http://127.0.0.1:8080/.
name: stellar

x-stellar: &stellar
  image: ${STELLAR_IMAGE:?set STELLAR_IMAGE in .env}:${STELLAR_TAG:?set STELLAR_TAG in .env}
  read_only: true
  tmpfs: [/tmp]
  cap_drop: [ALL]
  security_opt: [no-new-privileges:true]
  environment: &env
    HOME: /tmp
    STELLAR_CONFIG: /etc/stellar/stellar.yaml
    STELLAR_NATS_URL: nats://nats:4222
    STELLAR_REPOSITORY: /srv/stellar/config
  volumes:
    - ./config:/srv/stellar/config:ro
    - ./stellar.yaml:/etc/stellar/stellar.yaml:ro

services:
  nats:
    image: docker.io/library/nats:2.12.15-alpine
    command: ["-js", "-sd", "/data", "-m", "8222"]
    volumes:
      - jetstream:/data
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8222/healthz"]
      interval: 5s
      timeout: 2s
      retries: 12
    restart: unless-stopped

  # One shot: compiles ./config and makes it the current configuration, then exits.
  publish-config:
    <<: *stellar
    command: ["stellar", "compile", "/srv/stellar/config",
              "--publish", "nats://nats:4222", "--output", "/tmp/snapshot.ir"]
    depends_on:
      nats:
        condition: service_healthy
    restart: "no"

  control:
    <<: *stellar
    command: ["stellar-control"]
    environment:
      <<: *env
      STELLAR_INSTANCE: "1"
    ports:
      - "127.0.0.1:8080:8080"
    depends_on:
      nats:
        condition: service_healthy
      publish-config:
        condition: service_completed_successfully
    healthcheck:
      test: ["CMD", "bash", "-c",
             "exec 3<>/dev/tcp/127.0.0.1/8080 && printf 'GET /v1/openapi.json HTTP/1.0\\r\\n\\r\\n' >&3 && head -n1 <&3 | grep -q ' 200 '"]
      interval: 10s
      timeout: 2s
      retries: 6
      start_period: 10s
    stop_signal: SIGTERM
    restart: unless-stopped

  # The target sim-1 of the example configuration; one service per simulated target.
  sim-1:
    <<: *stellar
    command: ["stellar-simulator", "--simulation", "platform-v3-sim", "--target", "sim-1",
              "--gateway", "sim-gw-1", "--driver", "platform-v3-sim-1"]
    environment:
      <<: *env
      RUST_LOG: info
    depends_on:
      nats:
        condition: service_healthy
    # The simulator stops on SIGINT.
    stop_signal: SIGINT
    restart: unless-stopped

volumes:
  jetstream:
  • publish-config runs once at each docker compose up: it compiles config/ with the CLI of the image and makes it the current configuration, then exits. control starts once it has succeeded; a repository that does not compile stops there, with the errors in docker compose logs publish-config.
  • Health checks. NATS answers /healthz on its monitoring port 8222; control is healthy once the API answers /v1/openapi.json (the image has no curl: the check uses bash).
  • Hardened containers. Read-only root file system, no capability, no-new-privileges, the user of the image; /tmp is the only writable path (HOME=/tmp for the cache of the CLI).
  • Only the console is published, on the loopback interface of the host. Publish 0.0.0.0:8080:8080 behind a reverse proxy with HTTPS for other machines, and NATS (127.0.0.1:4222:4222) for the CLI of the host or edges on the host.
  • SELinux. Where it is enforced, add ,z to the options of the mounted files (./config:/srv/stellar/config:ro,z).
  • More simulated targets: one service each, like sim-1; the second target of the example is --simulation lab-psu-sim --target psu-sim-1 --gateway psu-sim-gw-1 --driver lab-psu-sim-1.

Running#

Shell
docker compose up -d                         # NATS, publish, the core, the simulator
docker compose ps                            # control (healthy), publish-config exited 0
docker compose logs -f control               # logs of the services
curl -fsS http://127.0.0.1:8080/v1/topology  # the targets of the configuration

The console answers on http://127.0.0.1:8080/.

A change of the configuration#

Update config/ (a git pull, or an edit), then publish it again:

Shell
docker compose run --rm publish-config

The services pick the new revision up at the next safe point of each target: no restart. See Compilation, Snapshots and Locks.

Stop#

Shell
docker compose stop          # SIGTERM to the services, SIGINT to the simulators
docker compose down          # also removes the containers and the network; the volume stays

Upgrade#

Every container of a deployment uses the same tag. Set the new one and recreate:

Shell
sed -i 's/^STELLAR_TAG=.*/STELLAR_TAG=<new version>/' .env
docker compose pull
docker compose up -d

Compose recreates the containers whose image changed, publishes the configuration again with the new CLI, and restarts the core once it is published. The streams, the KV buckets and the state of the targets stay in the volume. Read the release notes first: a release may ask for a change of the configuration repository or of stellar.yaml. To go back, set the former tag and run the same commands.

NATS is upgraded the same way, by its image tag in compose.yaml; keep to the 2.12 series unless the release notes say otherwise.

Backup#

JetStream holds every state that is not in Git: the published configurations, the streams of telemetry and telecommands, the runs and their reports, the alarms, the files. The configuration repository itself lives in Git. Back up the volume stellar_jetstream with NATS stopped, so that its files are consistent:

Shell
docker compose stop
docker run --rm -v stellar_jetstream:/data:ro -v "$PWD":/backup \
    docker.io/library/alpine:3 tar czf /backup/jetstream-$(date +%F).tar.gz -C /data .
docker compose start

To restore, into the empty volume of a stopped deployment:

Shell
docker compose down
docker volume rm stellar_jetstream && docker volume create stellar_jetstream
docker run --rm -v stellar_jetstream:/data -v "$PWD":/backup \
    docker.io/library/alpine:3 tar xzf /backup/jetstream-<date>.tar.gz -C /data
docker compose up -d

Without a stop, nats stream backup (the nats CLI, in the nats-box image) saves the streams one by one from a running server. See NATS and JetStream for what each stream and bucket holds.

Separate services#

The same file runs one container per service of the core instead of stellar-control: replace the service control with these (and keep nats, publish-config and the simulators):

YAML
x-core: &core
  image: ${STELLAR_IMAGE}:${STELLAR_TAG}
  read_only: true
  tmpfs: [/tmp]
  cap_drop: [ALL]
  security_opt: [no-new-privileges:true]
  environment:
    HOME: /tmp
    STELLAR_CONFIG: /etc/stellar/stellar.yaml
  volumes:
    - ./stellar.yaml:/etc/stellar/stellar.yaml:ro
  depends_on:
    nats:
      condition: service_healthy
    publish-config:
      condition: service_completed_successfully
  restart: unless-stopped

services:
  # nats, publish-config and sim-1 as above; control replaced by:
  reconciler: {<<: *core, command: ["stellar-reconciler"]}
  compute:    {<<: *core, command: ["stellar-compute"]}
  executor:   {<<: *core, command: ["stellar-executor"]}
  alarms:     {<<: *core, command: ["stellar-alarms"]}
  scheduler:  {<<: *core, command: ["stellar-scheduler"]}
  transfers:  {<<: *core, command: ["stellar-transfers"]}
  api:
    <<: *core
    command: ["stellar-api"]
    ports:
      - "127.0.0.1:8080:8080"

Each service has a default instance name, <kind>-<random>. With Compose, docker compose up -d --scale api=2 does not work with a fixed host port: scale the services that need no port (executor, scheduler, or standby instances of the others), and see Running the Services for how each one shares the work.

↑↓ to moveEnter to open