Skip to content
Architecture

bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8): No such file or directory

title: Architecture weight: 6

Overview

┌─────────────────────────────────────────────┐
│                     np CLI                    │
│    deploy · status · logs · scale · stop     │
└──────────┬──────────────────────────────────┘
           │
           ▼
┌─────────────────────────────────────────────┐
│                  Nomad Server                 │
│    scheduler · job state · rolling updates   │
└──────┬──────────────────┬───────────────────┘
       │                  │
       ▼                  ▼
┌─────────────┐   ┌─────────────┐
│ Nomad Client │   │ Nomad Client │   ...
│  (node 1)   │   │  (node 2)   │
│             │   │             │
│ ┌─────────┐ │   │ ┌─────────┐ │
│ │ alloc 1 │ │   │ │ alloc 3 │ │
│ │ alloc 2 │ │   │ │ alloc 4 │ │
│ └─────────┘ │   │ └─────────┘ │
└──────┬──────┘   └──────┬──────┘
       │                  │
       ▼                  ▼
┌─────────────────────────────────────────────┐
│                  Consul                       │
│    service registry · health checks · DNS    │
└─────────────────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────────┐
│                  Traefik                      │
│    ingress · routing · TLS · load balancing  │
└─────────────────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────────┐
│               OpenObserve                     │
│    logs · metrics · dashboards · alerts      │
└─────────────────────────────────────────────┘

Component Roles

Component Role Why this choice
Nomad Job scheduler Simple, single-binary, works with any workload
Consul Service discovery + health checks Battle-tested, DNS interface, no etcd needed
Traefik Reverse proxy + ingress Auto-discovery from Consul, automatic TLS, no config glue
OpenObserve Logs + metrics Single binary, socket-activated (0 MB idle), replaces Elasticsearch + Grafana, 10x cheaper
Podman + crun Container runtime Daemonless, rootless, OCI-native; zero background memory when idle

Container Runtime: Why Podman + crun

np defaults to Podman with crun as the low-level OCI runtime. This is not an arbitrary choice — it flows from first principles.

The four things a container runtime actually does

1. Unpack an OCI image
2. Create namespaces (pid, net, mnt, uts, ipc, user, cgroup)
3. Configure cgroup resource limits
4. Execute the entrypoint

That’s it. Docker wraps these four steps in a daemon architecture born in 2013 — dockerd (~150 MB resident), containerd (~80 MB), and containerd-shim (~10 MB per container). None of this is structurally necessary in a modern Linux environment with systemd socket activation.

Podman + crun vs Docker

Docker Podman + crun
Idle memory ~230 MB (daemons) 0 MB (no daemon)
Per-container overhead ~10 MB (containerd-shim) ~1 MB (conmon)
Startup path client → dockerd → containerd → runc client → podman → crun
Rootless Requires rootless kit, not default Native rootless, default
Binary size ~200 MB podman ~50 MB + crun ~3 MB
OCI compliance Compatible but heavily wrapped Pure OCI, no intermediate layers
Fault isolation Daemon crash = all containers affected Per-container fork-exec, independent

What this means for an SME node

On a typical 8 GB node running 50 services:

Runtime Memory consumed
Docker 230 MB daemon + 50 × 10 MB shim = 730 MB
Podman 0 MB daemon + 50 × 1 MB conmon = 50 MB

Net savings: ~680 MB — 8.5% of total memory. That memory runs 10-15 more services instead of infrastructure overheard.

The safety net

np does not lock you in. Set NP_CONTAINER_RUNTIME=docker to switch back. Per-service override is supported in np.yaml:

services:
  - name: legacy-app
    type: container
    source: my-image:latest
    runtime: docker   # use Docker for this specific service

Why this matters for security

Podman’s rootless-by-default model changes the threat equation. A container breakout under Docker means root on the host. Under Podman rootless, the attacker lands in an unprivileged user namespace. For SMEs without dedicated security teams, “secure by default” matters more than “configurable security.”

The dependency calculus

Podman introduces one external dependency: nomad-driver-podman, a third-party Nomad plugin. This is a real trade-off. But the cost is hedged: if the plugin ever becomes unmaintained or incompatible, np can fall back to Docker via one environment variable. No user workloads are affected.

How deployment works

np deploy my-app
  ├─→  Read np.yaml → generate Nomad job spec
  ├─→  Submit job to Nomad (PUT /v1/jobs)
  ├─→  Nomad schedules allocations to clients
  ├─→  Wait for all allocations → "running"
  ├─→  Health check: GET {health_check}
  │         ✅ 200 OK → done
  │         ❌ Error → report + wait

Node requirements

Component Min RAM Min CPU Notes
Nomad Server 256 MB 0.5 core Raft quorum needs 3 servers
Nomad Client 128 MB + workloads 0.5 core + workloads
Consul 128 MB 0.25 core Shared with Nomad Server
Traefik 64 MB 0.25 core Edge nodes only
OpenObserve 0 MB idle, 128 MB active 0 idle, 1 core active Socket-activated — starts on first request
Podman + crun 0 MB idle 0 Daemonless — fork-exec on demand

Next: FAQ →