London DataTrading systems technology

Nexus explainer · companion to One host, many tenants

Nexus and Kubernetes

Nexus runs on systemd units and cgroup slices, not Kubernetes. That is a choice for its current scale: one client and one production host. Most controls Nexus relies on have a Kubernetes counterpart, and a few behave differently enough to matter for a trading system. This page maps one onto the other, sets out what Kubernetes would add and what it would cost, and says when I would move.

Services9926 long-running, 73 oneshot jobs → Deployments and Jobs
Timers62calendar timers, most catching up after downtime → CronJobs
Service accounts10one Linux user per service → ServiceAccounts
Memory floors2Postgres 18 GiB, CI 8 GiB → requests
Containerised1the alert service, with compose on its own server
Kubernetes objects0today; the right-hand column below is design, not measurement

Today's figures read from the production host on 2026-09-30, read-only, as on the One host and Scheduling pages.

Two layouts, side by side

On the left, how Nexus separates its workloads today. On the right, a sketch of the same workloads on a small cluster. The boundaries move from Linux users and cgroup slices to namespaces, nodes and resource requests.

Today: one host, systemd, cgroup v2 system.slice · services and the database PostgreSQL MemoryMin=18G, no max nexus-*.service ~24 · 10 service users Backtests CPUWeight=20 62 timers oneshots + OnFailure= user-1001.slice · agents and CI · Max 36G ci.slice 18 runners · Min 8G agentwork.slice capped jobs · swap 0 On Kubernetes: a sketch Control plane API server · etcd · scheduler · controllers Database node PostgreSQL + TimescaleDB StatefulSet and an operator, or a host outside the cluster App node ns: data ns: trader ns: research Deployments and CronJobs trader: 1 pod Burst node backtest workers scaled on queue depth by KEDA CI runners (ARC) can be cloud machines Isolation by Linux user, cgroup slice and database login. Isolation by namespace, RBAC, NetworkPolicy, requests.
Nexus figures as read on 2026-09-30. The cluster on the right is one reasonable layout, not a plan in progress. Namespaces span the cluster; they are drawn on the node where their pods would usually run. A production cluster would have three control-plane nodes or a managed control plane.

The same controls in two vocabularies

Each row is something Nexus does today, the Kubernetes way to do it, and what would change. Most map cleanly. The ones that don't are about single instances, memory floors and start order.

Nexus todayKubernetesWhat changes
A serviceType=simple, Restart=on-failure, its own Linux userA Deployment; the kubelet restarts a failed containerLiveness probes also restart a process that hangs while staying alive. systemd's WatchdogSec= does the same, but no Nexus unit uses it today, so in practice this is a gain. Repeated crashes back off to five minutes between restarts by default, where systemd retries every 10 seconds.
One at a timeThe broker price capture and the trading scheduler each run as one unit on one hostreplicas: 1 with strategy: Recreate, or a StatefulSet; a Lease for leader electionThe default rolling update starts the new pod before stopping the old one, so replicas: 1 still gives two pods for a few seconds. Recreate covers rollouts only: a deleted or evicted pod is replaced while the old one is still terminating. A StatefulSet waits until the old pod is confirmed gone, unless someone force-deletes it. A lease is only safe with a fencing check where orders are placed. Two broker sessions, or two loops placing orders, even for seconds, is the failure a trading system most needs to rule out.
ScheduleOnCalendar=Mon..Fri 17:30 Europe/London; the 15-minute fetch fires 20 seconds past the quarter-hourA CronJob with timeZoneBoth are time-zone aware. Cron schedules have no seconds field, so the 20-second offset becomes a sleep at the start of the job. A pod also takes seconds to schedule and start, where a systemd oneshot starts almost at once.
OverlapA fire while the job still runs is absorbedconcurrencyPolicy: ForbidClose, not equal. Forbid defers rather than drops: unless a starting deadline has passed, the skipped run starts as soon as the last one finishes. A failed Job retries up to six times by default, where a failed oneshot stops and alerts, and a CronJob creates about one Job per run, so jobs must be idempotent.
Missed runsPersistent=true runs once at boot; false where a late run would be wrongstartingDeadlineSeconds: unset, the controller starts the most recent missed run however late; set, only inside the deadlineClose in intent: no deadline behaves like Persistent=true, a short one like false. The end-of-day snapshot that must not run late gets a short deadline, of at least 10 seconds as the documentation advises.
Start orderAfter=postgresql on 64 units, added after catch-up runs fired before the database was upNo start order between pods; init containers, or retries in the serviceEach service waits for its own dependencies. That is more robust, but it is code to write.
Failure alertsOnFailure= on every first-party service, to one template unit that posts the alertNothing built in beyond Events and Job status: Prometheus, kube-state-metrics and Alertmanager rules on failed Jobs and restarting podsMore parts to run. Alert rules become configuration rather than per-unit wiring.
CPU shareCPUWeight=20 keeps backtests behind tradingCPU requests set the cgroup weight; a PriorityClass orders scheduling and evictionEquivalent. CPU limits, unlike weights, throttle in 100 ms periods, so a latency-sensitive pod gets requests without limits, or whole cores under the static CPU manager. Whole cores need Guaranteed QoS: integer CPU, with requests equal to limits.
Memory floorPostgres MemoryMin=18G with no ceiling, held all the way up the treeRequests reserve memory when a pod is placed; a run-time floor through memory.min is not on by defaultThe closest fit is Guaranteed QoS: requests equal to limits for memory and CPU. That adds the memory ceiling Nexus left off on purpose, and a CPU limit unless it gets whole cores. In return the pod is evicted last and is the OOM killer's last choice, and on a node without swap its buffer pool can't be swapped out, the failure the floor guards against today. A pod that grows past its request is among the first evicted under node memory pressure.
Heavy jobsA capped transient scope per job, swap off; a memory gate refuses new work when headroom is lowA Job with memory limits; a pod waits as Pending until a node has room; ResourceQuota per namespacePartly a gain. The scheduler admits by declared requests, not measured memory or pressure, so it replaces Nexus's gate only if requests are honest. Kueue adds queueing and quotas for batch Jobs.
SwapMemorySwapMax=0 on agent jobs, after a host swapped for 100 minutes on 6 SeptemberNodes have traditionally run with swap off; swap is opt-in per nodeThe lesson Nexus learned the hard way is Kubernetes' default.
Identity, secretsOne Linux user per service; secrets in a file only it can read; ProtectSystem, NoNewPrivilegesServiceAccounts and RBAC; Secrets; securityContext and the restricted Pod Security profileClose, with two catches. Secrets sit unencrypted in etcd unless encryption at rest is configured; base64 in a manifest is encoding, not protection. And anyone who can create a pod in a namespace can mount its Secrets, so per-service namespaces do the job the per-service Linux users do now.
Privileged actionsAn agent drops a request file; a root path unit runs a job only if its name is on a root-owned allow-listRBAC that lets one account create Jobs in one namespace, plus an admission policy on images and commandsRBAC controls who may create a Job, not what it runs, and a Job can run as any ServiceAccount in its namespace. The allow-list moves into admission policy, such as a ValidatingAdmissionPolicy that accepts only listed images and commands.
DeployA queued deploy ships the merged tree, runs the service's deploy gate and a health check, then restarts its unitsCI builds an image and pushes it to a registry; Argo CD or Flux applies the manifest from gitThe deploy is an image digest, and rollback is the previous one, though a database migration does not roll back with it. Nothing is copied onto a host.
Logsjournald per unit, 30 days, 16 GB capstdout per container, rotated on the node; Loki or Elasticsearch for retentionOne more system to run for the same 30 days.
CI runners18 runners in their own slice with an 8 GiB floorActions Runner Controller: one short-lived runner pod per jobRunners scale to zero when idle and burst across nodes.
Work queuesPostgres FOR UPDATE SKIP LOCKED; a supervisor keeps N backtest workers aliveThe queue is unchanged; workers become a Deployment scaled on queue depth by KEDA's PostgreSQL scalerWorkers pull their work, so they can run on any node without a message broker. Two things change: the reset of orphaned runs at start is only safe with one instance, so runs need a heartbeat or lease; and a scale-down stops a worker mid-backtest, so a run must be requeued on SIGTERM.

The middle step: containers

Containers change how software is shipped. Kubernetes changes how it is run, and where. A container can run on one host without Kubernetes, in two common ways, and for Nexus they differ more than they look: one keeps the scheduling, ordering, alerting and cgroup controls in the first column of the table above, and the other hands most of them to a daemon.

Containers under systemd

Podman Quadlet

[Unit]
After=postgresql.service
OnFailure=nexus-unit-alert@%n.service

[Container]
Image=registry/nexus-data-api@sha256:…
EnvironmentFile=…/data-api.env
ReadOnly=true

[Service]
Restart=on-failure
CPUWeight=100

[Install]
WantedBy=multi-user.target

Podman has no daemon. A .container file like this one becomes an ordinary systemd service when systemd loads its units, and the container's processes run in a cgroup beneath that service's. Timers, OnFailure=, start order, memory floors and CPU weights apply exactly as they do today. What changes is what the unit starts: an image built once in CI and pinned by digest, rather than code on the host. This is a rootful unit, in /etc/containers/systemd. The per-service Linux user moves into the container, with User= and UserNS=auto, and unit sandboxing such as ProtectSystem gives way to the container's own options. Rootless Quadlet runs under each user's own systemd manager, which the system slices, After=postgresql.service and the alert template don't reach.

Containers under a daemon

Docker Compose

services:
  api:
    image: registry/app@sha256:…
    restart: unless-stopped
    env_file: .env
    read_only: true
    healthcheck:
      test: ["CMD", "/app/healthcheck"]
    deploy:
      resources:
        limits: { memory: 512M }
        reservations: { memory: 256M }

An illustrative file. The Docker daemon starts, restarts and owns the containers and their cgroups. One file describes a set of services, and docker compose up -d brings them to that state. Nexus's alert service runs on Compose on its own server, where it fits: a small stack, no timers, nothing running next to trading.

Nexus todayUnder systemd (Quadlet)Docker Compose
DeployThe merged tree is shipped to the host and units restartPull the image by digest, change the digest in the unit file, reload systemd and restart the unitPull an image and docker compose up -d, which stops the old container and starts the new one
RollbackDeploy the previous commitPoint the unit at the previous digestPoint the file at the previous digest
DependenciesInstalled on the host, on its Python and system librariesBaked into the image; the host's Python and libraries stop mattering
FilesystemProtectSystem makes the host read-only around one writable pathEach service sees only its own image and the volumes mounted into it
Scheduled jobs62 timers start oneshot servicesUnchanged: a timer starts a containerised oneshot (Type=oneshot in the file's [Service]) like any other serviceNo scheduler. Timers or cron on the host run docker compose run, or a scheduler runs in its own container; the job's container then runs in Docker's cgroup, not the timer's service, so that unit's limits don't reach it
Restarts, alertsRestart=, and OnFailure= on every first-party serviceUnchanged. HealthCmd= with HealthOnFailure=kill lets Restart= recover a hung process tooRestart policies retry with a back-off; unless-stopped never gives up, so nothing reaches a failed state. A failing health check only marks the container unhealthy, and outside Swarm nothing restarts it. No failure hook: alerting watches the daemon's events or the containers from outside
Memory floor, CPUMemoryMin=, MemoryMax=, CPUWeight= on each unit, set directlyUnchanged, on the unitLimits and a soft reservation (memory.low) per container, set through Docker, which has no hard-floor option. A floor means cgroup_parent into a slice configured in systemd, outside Compose
Logsjournald per unitjournald, under the unit's nameDocker's log driver: JSON files by default, unrotated unless max-size is set, or journald if configured
One instanceOne unit, one process treeUnchangedOne container per service unless scaled; replaced, not doubled, on deploy
NetworkServices listen on the host; the host firewall appliesHost networking keeps today's behaviour. On Podman's own networks, rootful published ports are NAT rules too, so name the bind address there as wellDocker's NAT rules run before the host firewall, so a published port must name its bind address
In NexusEvery production serviceThe likely next step for the production hostThe alert service, on its own server

For Nexus, Quadlet is the better middle step: it brings images, digests and rollbacks without giving up the cgroup controls the production host depends on. Compose stays where it already fits. Either way, the images built for this step are the same images Kubernetes would run.

What Kubernetes would add

What it would cost

When I would move

Stay on systemd

One client and one production host. The database dominates memory, and cgroups are controlled directly, next to trading. That describes Nexus today.

Containers first

When a second host joins the production path. Build an image per service in CI and run it under systemd with Podman Quadlet, as in the middle step. The deploy becomes an image, and the cgroup controls stay as they are.

Move to Kubernetes

When several clients need isolation from each other (namespaces with NetworkPolicy for soft separation, a cluster per client for hard), when backtests outgrow one machine, when trading needs failover to a second machine, or when a client's platform team already runs Kubernetes.

A migration path, in order

Much of the design already maps. Each service has its own account and its own database login. The work queues live in Postgres and need no broker. Configuration sits in environment files, which become ConfigMaps and Secrets. The order below moves the stateless parts first and the trader last.

Sources: Nexus figures from the One host, Scheduling, CI and Alerts pages, read from the production host on 2026-09-30 (systemctl, cgroupfs, pg_stat_database). Kubernetes behaviour from the Kubernetes documentation on workloads, CronJobs, resource management, Pod quality of service, node-pressure eviction, RBAC and Pod Security Standards, and from the KEDA, Actions Runner Controller and Podman Quadlet documentation. Nexus does not run Kubernetes; nothing in the Kubernetes column has been measured here.