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 entrypointThat’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 serviceWhy 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 + waitNode 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 →