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#
stellar/
├── compose.yaml
├── .env STELLAR_IMAGE and STELLAR_TAG
├── stellar.yaml the global configuration
└── config/ the configuration repositoryThe image comes from the registry given with the delivery of Stellar Systems:
cat > .env <<'ENV'
STELLAR_IMAGE=<registry>/stellar-control
STELLAR_TAG=<version>
ENVThe global configuration (see Global Configuration):
nats:
url: nats://nats:4222
api:
listen: 0.0.0.0:8080
web_dir: /usr/share/stellar/web
observability:
log_format: json
log_level: infoconfig/ 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):
docker create --name stellar-example <registry>/stellar-control:<version>
docker cp stellar-example:/usr/share/stellar/examples/config ./config
docker rm stellar-exampleThe 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#
# 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-configruns once at eachdocker compose up: it compilesconfig/with the CLI of the image and makes it the current configuration, then exits.controlstarts once it has succeeded; a repository that does not compile stops there, with the errors indocker compose logs publish-config.- Health checks. NATS answers
/healthzon its monitoring port 8222;controlis healthy once the API answers/v1/openapi.json(the image has nocurl: the check usesbash). - Hardened containers. Read-only root file system, no capability,
no-new-privileges, the user of the image;/tmpis the only writable path (HOME=/tmpfor the cache of the CLI). - Only the console is published, on the loopback interface of the host. Publish
0.0.0.0:8080:8080behind 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
,zto 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#
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 configurationThe 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:
docker compose run --rm publish-configThe services pick the new revision up at the next safe point of each target: no restart. See Compilation, Snapshots and Locks.
Stop#
docker compose stop # SIGTERM to the services, SIGINT to the simulators
docker compose down # also removes the containers and the network; the volume staysUpgrade#
Every container of a deployment uses the same tag. Set the new one and recreate:
sed -i 's/^STELLAR_TAG=.*/STELLAR_TAG=<new version>/' .env
docker compose pull
docker compose up -dCompose 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:
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 startTo restore, into the empty volume of a stopped deployment:
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 -dWithout 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):
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.