Deployment
Deployment
Section titled “Deployment”Aether deploys as either a set of Docker containers or, for Docker-free targets, native systemd services. There are three paths: run the Docker Compose stack directly on a machine that can build images, package everything into a single self-extracting Docker-based installer and ship it to an edge device, or build a bare-metal installer that ships statically linked binaries and systemd units instead.
Docker Compose
Section titled “Docker Compose”cp .env.example .env # then edit: AETHER_BASE_PATH, HOST_UID/HOST_GID, RUST_LOG, ...
docker compose up -ddocker compose psThe default Compose application starts only the six Rust services, all with
network_mode: host. Redis and TimescaleDB start only when their explicit
redis and postgres-storage profiles are selected. The base file exposes the forecast sidecar only through the mutable
data-processing-dev profile; production requires the explicit override shown
below.
AetherEdge is a headless Kernel distribution. It does not build or install a browser client. The EMS operator console belongs to the independent AetherEMS distribution and connects through the authenticated application API.
| Container | Image | Role |
|---|---|---|
| aether-redis | redis:8-alpine | Optional non-authoritative state mirror infrastructure (redis profile) |
| aether-timescaledb | timescale/timescaledb:2.25.2-pg17 | Optional PostgreSQL history backend (postgres-storage profile) |
| aether-load-forecasting-processor | operator-supplied, digest-pinned image | Optional request-driven processor (data-processing profile) |
| aether-io | aetherems:latest | Communication service (privileged, mounts /dev for field buses) |
| aether-automation | aetherems:latest | Model service and rule engine |
| aether-history | aetherems:latest | SHM sampler with embedded SQLite history by default |
| aether-api | aetherems:latest | REST API, WebSocket, JWT auth |
| aether-uplink | aetherems:latest | MQTT cloud uplink, TLS certificates |
| aether-alarm | aetherems:latest | Alarm rules and notifications |
The production forecast profile requires an immutable image built from the
existing Load-Forecasting service plus Aether’s adapter, a commissioned
artifact bundle, a matching bearer token, and a validated runtime YAML under
${AETHER_BASE_PATH}/config/data-processing/runtime.yaml:
Copy the repository’s synthetic
runtime.example.yaml
and
covariates.example.json
into that deployment-owned directory, replace every logical/physical mapping,
artifact digest, and covariate row, and validate them against the site database.
The examples are not production values.
export AETHER_LOAD_FORECASTING_IMAGE=registry.example/load-forecasting@sha256:<digest>export AETHER_LOAD_FORECASTING_BEARER_TOKEN='<unique secret>'export AETHER_LOAD_FORECASTING_ARTIFACT_BUNDLES='<commissioned JSON array>'integrations/load-forecasting/deploy/validate-production-env.shdocker compose \ -f docker-compose.yml \ -f integrations/load-forecasting/deploy/docker-compose.data-processing.yaml \ --profile data-processing \ up -d aether-load-forecasting-processor aether-apiThe preflight is mandatory for this documented production path. It rejects a
non-@sha256 image reference, weak or malformed token, out-of-range
concurrency, and non-strict artifact-bundle JSON before Compose evaluates the
override.
Historian authority requires a separate operational check. A storage
PUT /hisApi/storage saves settings but does not reconnect the active writer.
Keep Data Processing disabled across a storage change, reconnect or restart
aether-history, verify its active SQLite backend and a commissioned sentinel
series, then restart aether-api with runtime history.path matching the
applied backend. The persisted history_config.storage_* rows alone do not
prove the live write target.
Direct SQLite history also needs a filesystem boundary. The API must receive
the historian database, WAL, and SHM through a dedicated read-only directory
mount (or an independently permissioned read-only OS account/ACL); its own
aether.db and audit writes stay on a separate writable path. The base Compose
currently mounts the whole /app/data directory read-write into aether-api,
so the documented production Data Processing route is blocked until a site
override provides and verifies that separation. SQLite mode=ro and
query_only=ON alone are not sufficient containment.
The /api/v1/data-processing/process call is non-idempotent and writes a
mandatory audit record even when work is rejected. The current API does not
provide an actor/IP request-rate limiter or an audit retention quota. A
production ingress must therefore enforce authenticated actor and source-IP
rates plus an in-flight ceiling, while operations monitor
command_audit_events growth and apply a retention/export policy that
preserves required evidence. Production enablement is blocked without those
controls; the per-route processor semaphore alone does not bound rejected-call
audit writes.
It binds to loopback and receives no Aether data-directory, configuration,
device, history-database, or SHM mount. The application sends a complete,
bounded ProcessingFrame over the processor port. See
../../integrations/load-forecasting/deploy/README.md
for the standalone systemd unit and commissioning requirements.
The Compose sidecar joins only the dedicated data-processing-local network,
which is declared internal: true; together with host-loopback publication,
this mechanically blocks container external egress and limits inbound access.
Native/systemd deployment still requires a host firewall or equivalent egress
policy. The examples also leave CPU, memory, and PID quotas
deployment-specific; set measured cgroup/systemd limits from a real artifact
benchmark so processor load cannot starve deterministic services.
The six Rust services share one aetherems:latest compatibility image, each
started with its own command. The image name is retained while downstream
release consumers migrate; it does not imply that this repository owns the EMS
product or Console.
Host networking does not make the unauthenticated process APIs public: IO,
automation, history, uplink, and alarm bind only to 127.0.0.1. Remote clients
must enter through aether-api on port 6005, where JWT and role checks apply.
The optional Redis and TimescaleDB listeners are also loopback-only.
Two mount classes matter for the runtime:
- Shared memory and local event sockets — the host’s
/dev/shmis bind-mounted at/shm/rtdbin all six Rust services. The mount is read-write because the SHM owner writes point slots while isolated consumers create their own subscription bitmaps and UDS endpoints beside the segment. Mounting the directory also avoids Docker auto-creating a stale file entry. - Optional external stores — no core service mounts a Redis socket, exports
REDIS_URL, or waits for Redis.docker compose --profile redis up -dstarts mirror infrastructure for a host that explicitly wiresaether-redis-bridge. PostgreSQL history remains opt-in through--profile postgres-storageand a PostgreSQL-enabled history build. Set a unique non-emptyTIMESCALEDB_PASSWORDbefore selecting that profile; the packaged extension installer generates one without printing it.
All Rust containers read the shared configuration SQLite database from
${AETHER_BASE_PATH:-./data}/aether.db (mounted at /app/data/aether.db)
and write logs to ${AETHER_LOG_PATH:-./logs}. aether-history stores samples in
/app/data/aether-history.db unless a PostgreSQL-enabled build and backend
configuration are explicitly selected.
The services remain six independent processes. SHM/UDS replaces a mandatory live-data broker; it does not collapse their restart or fault-isolation boundaries.
Edge installer
Section titled “Edge installer”scripts/build-installer.sh produces a single self-extracting .run file
containing everything an offline edge device needs — Docker image archives,
the compose file, configuration templates, the aether CLI binary, and an
install script:
./scripts/build-installer.sh [VERSION] [ARCH] [TARGET] [--services=...] [--enable-swagger]VERSION— version string, defaults to today’s date (YYYYMMDD)ARCH—arm64(default) oramd64TARGET— Rust target triple; defaults toaarch64-unknown-linux-muslfor arm64 andx86_64-unknown-linux-muslfor amd64--services/-s— comma-separated subset to include (service names:aether-io,aether-automation,aether-history,aether-api,aether-uplink,aether-alarm,redis,timescaledb; group shortcutrustexpands to all six Rust services). Every fresh-install package must include the Rust core; select extension variants as-s rust,redis,-s rust,timescaledb, or-s rust,redis,timescaledb. The default package contains only the Rust edge-runtime image; external-store images must be selected explicitly.--enable-swagger— compile the Rust services with their feature-gated Swagger UI enabled
# Full installer for an ARM64 edge device./scripts/build-installer.sh
# All Rust services only, with Swagger UI./scripts/build-installer.sh v1.2.0 arm64 -s rust --enable-swaggerThe script cross-compiles the six services and the aether CLI with
cargo zigbuild for the target triple, builds the aetherems Docker image
from those binaries, saves the images with docker save (plus the Redis and
TimescaleDB images when selected), and packages the result
with makeself into release/AetherEdge-<arch>-<version>.run (subset
builds via --services append a service-list suffix to the file name, and
--enable-swagger appends -swagger). The build host needs Docker,
cargo-zigbuild (auto-installed via cargo install if missing), and
makeself (auto-installed via Homebrew on macOS).
Ship and run:
scp release/AetherEdge-arm64-<version>.run root@192.168.30.21:/tmp/ssh root@192.168.30.21 'chmod +x /tmp/AetherEdge-arm64-<version>.run && /tmp/AetherEdge-arm64-<version>.run'The embedded installer supports a fresh deployment only. Its first step is
a read-only preflight: if it finds an Aether installation root, install context,
site configuration or database, Aether container, or Aether systemd unit, it
exits before stopping a service, loading an image, or writing a file. On an
accepted clean host it installs to /opt/AetherEdge, loads the bundled images
with docker load, activates the fail-safe template at
/opt/AetherEdge/data/config, records the layout in
/etc/aether/install.yaml, initializes a new database, and starts the six
containers with Docker Compose. The deployment is Docker-based — the installer
delivers images and compose configuration, not standalone service binaries.
In-place upgrade, rollback to an older release, and import of an old database or installation layout are not supported in this release. To replace an installation, first export and back up anything that must be retained, run the deployment-specific uninstall procedure, and manually relocate or remove every retained Aether footprint before invoking the new installer. Translating retained data into a new release is currently an operator-managed migration outside the installer; do not point a fresh installer at an old site directory.
/opt/AetherEdge is intentionally fixed for this release because packaged
service-management paths assume that composition root. The installer rejects
AETHER_INSTALL_DIR overrides rather than completing an installation whose
later lifecycle operations would target a different root. AETHER_BASE_PATH
may place a new, empty data/configuration tree in a dedicated child
directory on another disk, but it must be chosen before installation and is not
a migration switch. The installer rejects /, system roots, generic mount
roots, symlinked paths, the installation root, and any destination containing
an Aether site before any recursive permission operation. Paths are also
limited to characters that round-trip safely through Docker Compose .env.
An AETHER_TIMESCALE_DATA_PATH outside the site root and Docker’s optional
redis-data named volume are extension-owned storage. They must also be empty
for a fresh deployment. Reusing or migrating an extension store is outside the
installer’s supported workflow.
The installer generates
AETHER_BOOTSTRAP_ADMIN_PASSWORD, persists it only in the mode-0600 .env,
and never prints the value. The completion message provides a local retrieval
command. Sign in as admin, change the password immediately, then remove the
bootstrap variable. Anonymous registration remains disabled unless
AETHER_ALLOW_PUBLIC_REGISTRATION=true is explicitly set.
The API container runs as HOST_UID:HOST_GID. It has neither the Docker socket
nor the installation root mounted, and /etc/systemd/network is read-only.
Consequently host-network mutation requests fail closed. Remote runtime
upgrade is not supported; installing another release requires the explicit
fresh-deployment workflow above rather than expanding the API process’s
authority.
Pack-only artifact
Section titled “Pack-only artifact”A domain Pack is released separately from the fresh-install .run package. A
Pack bundle contains only pack-artifact.json and the declarative pack/
tree—never the aether CLI, a service binary, or a core crate. Build it from
the exact runtime manifest generated for the target Kernel composition:
./scripts/build-pack-artifact.sh \ packs/<pack-id> \ build/installer/runtime/runtime-manifest.json \ release/<pack-id>.bundleCopy that directory to an edge host which already has the matching Kernel, then install it with the host’s CLI:
aether packs install --artifact /tmp/<pack-id>.bundleThe command refuses a different Kernel version, target triple, or complete
runtime-manifest digest. It also rejects extra top-level entries, symlinks,
executables/source trees, payload tampering, unbounded files, and an
incompatible pack.yaml. After verification it publishes the data below the
installed data directory as packs/<id>/<version> and replaces global.yaml
atomically only after validating the complete candidate active Pack set. A
failed activation preserves the previous configuration and removes the newly
published version.
This command does not restart services or commission the Pack. Plan any
maintenance restart separately, then run aether doctor; enabling channels,
instances, rules, processors, or physical control remains a distinct audited
commissioning action. The repository can build and test this local format, but
does not yet claim an independently published/signed Kernel artifact, Pack
artifact, or downstream second-repository release gate.
Bare-metal Linux (systemd)
Section titled “Bare-metal Linux (systemd)”For edge devices that cannot or should not run Docker,
scripts/build-installer.sh --bare-metal produces a second kind of .run
package: a self-contained bundle of statically linked binaries and systemd
units, with zero container runtime dependency on the target machine. It
contains the six Rust services, the aether CLI, and the core systemd units.
Static redis-server/redis-cli and their unit are included only when Redis
is selected. scripts/build-static-deps.sh uses INCLUDE_REDIS=1 for that
extension bundle. The core services are grouped by aether.target. The pinned
Redis release also pins its source-archive SHA-256 value. Overriding the version
requires its matching REDIS_SHA256; a cached binary is reused only with a
matching provenance marker and after its static ELF linkage and target
architecture are checked.
The bare-metal runtime root is likewise fixed at /opt/aether, matching the
packaged systemd units. AETHER_INSTALL_DIR overrides are rejected. Its
bootstrap administrator credential is stored in /etc/aether/aether.env
(mode 0600) with the same retrieve-change-remove lifecycle as Docker.
Build:
# Core-only package (default)./scripts/build-installer.sh --bare-metal [VERSION] [ARCH]
# Core plus optional Redis mirror infrastructure./scripts/build-installer.sh --bare-metal [VERSION] [ARCH] -s rust,redisThis follows the same [VERSION] [ARCH] [TARGET] positional convention as
the Docker build — --bare-metal is an added flag, order of the other
arguments is unchanged. It cross-compiles the same six services plus the
aether CLI and packages them with makeself into
release/AetherEdge-baremetal-<arch>-<version>.run. Selecting Redis adds
-redis to the file name. A bare-metal package must include the Rust core.
TimescaleDB is an external bare-metal extension and is not bundled by this
builder.
Ship and run as root — the installer refuses to proceed without
systemctl on PATH:
scp release/AetherEdge-baremetal-arm64-<version>.run root@192.168.30.21:/tmp/ssh root@192.168.30.21 'chmod +x /tmp/AetherEdge-baremetal-arm64-<version>.run && /tmp/AetherEdge-baremetal-arm64-<version>.run'scripts/install-baremetal.sh (the script the .run archive extracts and
runs) lays out the install as:
| Path | Contents |
|---|---|
/opt/aether/bin/ |
Service binaries and aether CLI; Redis tools only in an explicitly selected extension bundle |
/etc/aether/config/ |
The activated configuration (from config.template/ on first install) |
/etc/aether/aether.env |
Explicit config/data/database paths, AETHER_LOG_DIR, RUST_LOG, and freshly generated secrets (mode 600) |
/etc/aether/install.yaml |
Non-secret installed layout used by the CLI (config_dir, data_dir, runtime mode, release channel, and enabled packs) |
/etc/aether/script-host/main.py |
The Python script host for aether-io custom transforms (matches the deployed-path lookup in services/io/src/protocols/core/script_runner.rs) |
/var/lib/aether/ |
Service logs (logs/) and optional Redis data (redis/) |
It also symlinks aether onto /usr/local/bin and drops a
/etc/profile.d/aether.sh PATH entry, installs the systemd units,
runs aether init and aether sync against /etc/aether/config, and
finishes with systemctl enable --now aether.target.
Day-to-day operation is native systemd:
systemctl status aether.targetjournalctl -u aether-io -faether services and aether doctor auto-detect this mode — see
CLI Reference: aether services and
aether doctor — with no flag needed.
Detection (tools/aether/src/deploy_mode.rs) checks for both
/etc/systemd/system/aether.target and systemctl on PATH; if either is
missing it falls back to the Docker Compose code path. In systemd mode,
aether services start/stop/restart/status pass canonical service names such
as aether-io directly to systemctl <verb> (or use aether.target when no service is named), and
aether services logs <service> shells out to
journalctl -u <service>. aether services build/pull/clean all
error in this mode — there are no container images in a bare-metal install,
and the .run package is not an in-place upgrader. aether services refresh --smart degrades to a plain systemctl restart, printing a note that
--smart has no effect, since there’s no image to diff against. Redis is not
part of the default health contract; operators who enable the extension can
inspect its unit or profile independently.
None of the six Rust service units declares Requires=aether-redis.service.
The default target starts and keeps its SHM/SQLite work independently; an
enabled Redis mirror cannot become a service-availability dependency.
The bare-metal installer has the same fresh-only contract as the Docker
installer. Re-running a .run package on a host with /opt/aether,
/etc/aether, /var/lib/aether, installed units, or runtime data fails during
read-only preflight, before aether.target is stopped or files are replaced.
There is no automatic binary replacement, configuration merge, optional-unit
migration, or previous-release rollback path. Back up/export required state,
uninstall the old runtime, and manually relocate every retained footprint
before installing a new release; importing that state into the new release is
not currently supported by the installer. This does not remove the
installer’s failure cleanup for a partially completed fresh installation.
Uninstall with the script the installer writes:
/opt/aether/uninstall.shIt stops and disables aether.target, removes the systemd units, the
aether symlink, the PATH entry, and /opt/aether itself. /etc/aether and
/var/lib/aether (configuration and runtime data) are left in place. Those
retained directories intentionally make a later
fresh install fail until an operator has exported, relocated, or removed them.
Runtime paths
Section titled “Runtime paths”The shared-memory segment path is resolved in this order
(crates/aether-dataplane/src/core/config.rs):
AETHER_SHM_PATHenvironment variable, if set/shm/rtdb/aether-rtdb.shm, if the/shm/rtdbdirectory exists (the Docker mount point)/dev/shm/aether-rtdb.shmon Linux/tmp/aether-rtdb.shmelsewhere (macOS development)
Inside containers, /shm/rtdb is the host’s /dev/shm, so both views name
the same file. Docker also places the aether-automation command socket and PointWatch
socket in this directory through AETHER_M2C_SOCKET and
AETHER_AUTOMATION_POINT_WATCH_SOCKET; native deployments keep the /tmp
defaults. Peripheral PointWatch socket names are derived from the resolved
SHM path, so each process binds a distinct endpoint.
Other state:
- SQLite —
aether.dblives in the data directory:/opt/AetherEdge/dataon an installed device,./datain a compose checkout (AETHER_BASE_PATH); containers see it as/app/data/aether.db(AETHER_DB_PATH). - Embedded history — aether-history writes
aether-history.dbin the same data directory by default (AETHER_HISTORY_DB_PATH). PostgreSQL/TimescaleDB is an opt-in storage adapter, not a base-runtime prerequisite. - Configuration — the
aetherCLI first honors flags andAETHER_*_PATHoverrides, then reads/etc/aether/install.yaml. Without an install context, a source checkout uses./data/configand./data; an unregistered old installation directory is never adopted implicitly. - Logs —
${AETHER_LOG_PATH:-./logs}on the host,/app/logsin the containers.
Service management on device
Section titled “Service management on device”The installed aether CLI wraps Docker Compose for day-to-day operations:
aether services start # start one or more services (or all)aether services stop # stop servicesaether services status # container statusaether services refresh # recreate containers from the installed compositionaether services logs # view service logs
aether doctor # Docker, core services, SQLite, config files, # shared memoryaether services refresh recreates containers from the composition and image
set already installed on the device. It is a same-release recovery operation,
not a supported path for replacing the installed release. See Getting
Started for what a healthy aether doctor run covers.
Related pages
Section titled “Related pages”- Getting Started — build, initialize, and verify a fresh checkout
- Connect Devices — add channels and map points once the stack is running
- System Architecture — the services these containers run