Container Orchestrator from scratch
Poseidon is a single-binary container orchestrator built in Go. It manages Docker containers across a cluster of worker nodes, with optional service discovery (VIP allocation, DNS, TCP proxy) and Raft-based high availability.
- Language: Go 1.26.4
- CLI framework: Cobra (
spf13/cobra) - HTTP router: chi (
go-chi/chi/v5) - Containerization: Docker SDK (
docker/dockerv20.10) - Consensus / KV store: Zodiac + Raft (embedded)
- Data Storage: BoltDB (
boltdb/bolt) or in-memory - DNS: miekg/dns
- Metrics: goprocinfo (
/procon Linux) - Scheduling: RoundRobin or E-PVM (energy-aware)
| Feature | Status |
|---|---|
| Start, stop, delete Docker containers | ✔ |
| Task lifecycle (Pending → Scheduled → Running → Done/Failed) | ✔ |
| State transition validation | ✔ |
| Collect & expose worker metrics (CPU, memory, disk) | ✔ |
| Scheduler: RoundRobin and E-PVM placement | ✔ |
| Manager control loop | ✔ |
| Worker API + Manager API | ✔ |
| Persistent storage (BoltDB) | ✔ |
| Task health checks & auto-restart (max 3 retries) | ✔ |
| Service Discovery: VIP pool, DNS, TCP proxy | ✔ |
| High availability (Raft-based replicated manager) | ✔ |
| Self-hosting agent (static pod manifests) | ✔ |
| CLI interface | ✔ |
main.go → cmd/ (Cobra subcommands)
├── manager/ — control loops (ProcessTasks, UpdateTasks, DoHealthChecks),
│ scheduling, task-worker mapping, VIP allocation, registry sync
├── worker/ — Docker container lifecycle, /proc stats collection
├── task/ — Task/TaskEvent types, Docker SDK wrapper, state machine
├── scheduler/ — RoundRobin or E-PVM placement algorithm
├── store/ — Store interface; in-memory, BoltDB, or Raft-backed (Zodiac)
├── node/ — worker node representation (Manager-side stats collection)
├── stats/ — Linux /proc/meminfo, /proc/stat, /proc/loadavg (goprocinfo)
├── utils/ — HTTP retry helper
├── dns/ — embedded UDP DNS server (miekg/dns), resolves
│ <name>.svc.poseidon.cluster (A, SRV, PTR)
├── network/ — VIP pool — atomic sequential IP allocation from a CIDR via
│ Zodiac transactions
├── proxy/ — userspace TCP proxy, watches registry, listens on VIP:port,
│ forwards to worker host
├── registry/ — Zodiac-backed service registry with health awareness
├── system/ — self-hosting agent + Docker manifest reconciler
└── cmd/ — CLI entrypoints: manager, worker, agent, node, run,
status, stop
┌──────────┐ ┌──────────┐ ┌──────────┐
│ CLI │────▶│ Manager │────▶│ Worker │────▶ Docker
│ (cobra) │ │ (chi) │ │ (chi) │
└──────────┘ └────┬─────┘ └──────────┘
│
┌───────┴────────┐
│ Zodiac KV │
│ (Raft cluster)│
└───────┬────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
Registry VIP Pool Mappings
(services) (allocations) (task→worker)
│
┌────┴────┐
▼ ▼
DNS Proxy
(miekg) (TCP fwd)
Pending → Scheduled → Running → Completed
↘ Failed
Transitions are validated by task.ValidStateTransition(). Invalid transitions are silently rejected.
- Pending — a task is submitted via the API or CLI. It lands in the pending queue (
/poseidon/tasks/pending/in Zodiac KV). - Scheduled — the manager's scheduler picks a worker (RoundRobin or E-PVM) and assigns the task.
- Running — the worker starts a Docker container and monitors it.
- Completed — the container exits successfully, or the user stops the task.
- Failed — the container fails; the manager will auto-restart up to 3 times.
The manager runs three concurrent loops, each only on the Raft leader:
| Loop | Interval | Purpose |
|---|---|---|
ProcessTasks |
10s | Pull tasks from the pending queue, select a worker, allocate a VIP if needed, dispatch to worker |
UpdateTasks |
15s | Poll all workers for task state changes, sync registry on transitions |
DoHealthChecks |
60s | HTTP health check on running tasks; restart unhealthy tasks (max 3) |
- RoundRobin — cycles through workers in order.
- E-PVM — Energy-aware Preferential VM placement using the LIEB square ice constant. Scores workers based on CPU load, memory allocation, and task count, picking the lowest-cost candidate.
| Backend | Type | Persistence |
|---|---|---|
memory |
In-memory maps | None |
persistent |
BoltDB files (*.db) |
Local file |
raft |
Zodiac KV (Raft consensus) | Distributed, survives leader changes |
- Linux (reads
/procfilesystem) - Docker daemon running (
DOCKER_HOSTor default socket) - Go 1.26+
# Terminal 1: start a worker
go run main.go worker --port 5556 --name worker-1
# Terminal 2: start a manager, pointing at the worker
go run main.go manager --port 5555 --workers localhost:5556 --scheduler epvm
# Terminal 3: submit a task
go run main.go run --filename data/task1.json
# List tasks, stop a task, list nodes
go run main.go status
go run main.go stop <uuid>
go run main.go node# Terminal 1: Worker
go run main.go worker --port 5556 --name worker-1
# Terminal 2: First manager (bootstraps the Raft cluster)
go run main.go manager \
--dbType raft --node-id 1 --raft-port 9001 \
--data-dir /tmp/poseidon/m1 \
--workers localhost:5556 \
--scheduler epvm \
--service-cidr 10.42.0.0/16
# Terminal 3: Second manager (joins the cluster)
go run main.go manager \
--dbType raft --node-id 2 --raft-port 9002 \
--data-dir /tmp/poseidon/m2 \
--raft-join localhost:9001 \
--workers localhost:5556
# Terminal 4: Start a proxy+DNS node
go run main.go node --raft localhost:9001 --dns-addr 0.0.0.0:5353# Build system container images
docker build -t poseidon-worker:latest -f Dockerfile.worker .
docker build -t poseidon-proxy:latest -f Dockerfile.proxy .
docker build -t poseidon-dns:latest -f Dockerfile.dns .
# Copy manifests and start the agent
mkdir -p /tmp/poseidon/manifests
cp manifests/*.json /tmp/poseidon/manifests/
go run main.go agent \
--node-id 1 --raft-port 9001 \
--data-dir /tmp/poseidon/agent \
--manifest-dir /tmp/poseidon/manifests \
--interval 10s# Traditional mode (manager + worker with Raft and service discovery)
./run.sh traditional
# Self-hosted mode (builds Docker images, starts agent)
./run.sh self-hosted| Flag | Default | Description |
|---|---|---|
--manager / -m |
localhost:5555 |
Manager address for run, status, stop commands |
Start a manager node.
| Flag | Default | Description |
|---|---|---|
--port / -p |
5555 |
API listen port |
--host / -H |
0.0.0.0 |
API listen address |
--workers / -w |
localhost:5556 |
Comma-separated worker addresses |
--scheduler / -s |
epvm |
Scheduler: epvm or roundrobin |
--dbType / -d |
memory |
Store type: memory, persistent, or raft |
--node-id |
0 |
Raft node ID (required for raft mode) |
--raft-port |
9001 |
Zodiac KV HTTP API port |
--data-dir |
/tmp/poseidon |
Raft log/snapshot directory |
--raft-join |
Comma-separated Zodiac HTTP addresses to join | |
--service-cidr |
CIDR for VIP pool (e.g. 10.42.0.0/16) |
Start a worker node.
| Flag | Default | Description |
|---|---|---|
--port / -p |
5556 |
API listen port |
--host / -H |
0.0.0.0 |
API listen address |
--name / -n |
worker-<uuid> |
Worker name |
--dbtype / -d |
memory |
Store type: memory or persistent |
Start a self-hosting agent (reads manifests, ensures system containers run).
| Flag | Default | Description |
|---|---|---|
--node-id |
1 |
Raft node ID |
--raft-port |
9001 |
Zodiac KV HTTP API port |
--data-dir |
/tmp/poseidon/agent |
Raft log directory |
--raft-join |
Comma-separated Zodiac addresses to join | |
--manifest-dir / -m |
/etc/poseidon/manifests |
Directory of JSON manifests |
--interval / -i |
10s |
Reconciliation interval |
Start a proxy + DNS node for service discovery.
| Flag | Default | Description |
|---|---|---|
--raft / -r |
localhost:9001 |
Zodiac KV HTTP addresses |
--listen-addr / -l |
0.0.0.0 |
Proxy listen address |
--dns-addr |
0.0.0.0:53 |
DNS listen address (UDP) |
Submit a task from a JSON file.
| Flag | Default | Description |
|---|---|---|
--filename / -f |
./data/task1.json |
Task JSON file |
--manager / -m |
localhost:5555 |
Manager address |
List all tasks from the manager.
| Flag | Default | Description |
|---|---|---|
--manager / -m |
localhost:5555 |
Manager address |
Stop a running task.
| Flag | Default | Description |
|---|---|---|
--manager / -m |
localhost:5555 |
Manager address |
| Method | Route | Description |
|---|---|---|
GET |
/tasks |
List all tasks |
POST |
/tasks |
Submit a new task (JSON body is a TaskEvent) |
DELETE |
/tasks/{taskID} |
Stop a task |
GET |
/nodes |
List registered worker nodes |
GET |
/status |
Returns {"isLeader": bool, "taskCount": int} |
| Method | Route | Description |
|---|---|---|
GET |
/tasks |
List all tasks on this worker |
POST |
/tasks |
Start a task on this worker |
DELETE |
/tasks/{taskID} |
Stop a task on this worker |
GET |
/stats |
Get worker metrics (CPU, memory, disk) |
Task files use a TaskEvent envelope wrapping a Task. See data/task*.json for examples:
{
"ID": "event-uuid",
"State": 2,
"Task": {
"State": 1,
"ID": "task-uuid",
"Name": "my-task",
"Image": "strm/helloworld-http"
}
}State values: 0=Pending, 1=Scheduled, 2=Running, 3=Completed, 4=Failed.
Service discovery requires --dbType raft and --service-cidr on the manager.
Atomic sequential IP allocation from a CIDR (e.g. 10.42.0.0/16). Uses Zodiac transactions for consistency. Index 0 is reserved for the network address, index 1 is the first allocatable IP.
Zodiac-backed service registry. The manager auto-registers tasks when they reach Running state (allocating a VIP) and deregisters on Completed/Failed. Each instance stores its name, VIP, ports, worker, and health status.
Embedded UDP DNS server using miekg/dns. Resolves <name>.svc.poseidon.cluster:
- A records — returns the VIP(s) for healthy instances
- SRV records — returns port and hostname for healthy instances
- PTR records — reverse lookup from VIP to service name
Results are cached for 30 seconds.
Userspace TCP proxy that watches the registry and listens on VIP:port for each healthy service instance. Forwards incoming connections to the worker host's corresponding port. On Linux, adds the VIP to the loopback interface (lo).
The agent command reads JSON manifests from a directory (like Kubernetes static pods) and ensures the defined Docker containers are always running. Manifests are sorted by filename and reconciled on a configurable interval.
Pre-built manifests in manifests/:
| File | Container | Purpose |
|---|---|---|
01-worker.json |
poseidon-worker |
Worker node |
02-proxy.json |
poseidon-proxy |
TCP proxy |
03-dns.json |
poseidon-dns |
DNS server |
Each manager embeds a Zodiac Raft KV node. Only the leader runs scheduling/health-check control loops; all replicas accept API writes which land in the shared KV store.
Key prefixes stored in Zodiac:
| Prefix | Content |
|---|---|
/poseidon/tasks/ |
Task definitions |
/poseidon/events/ |
Task events |
/poseidon/tasks/pending/ |
Pending tasks (only leader processes) |
/poseidon/mappings/task-worker/ |
Task-to-worker assignments (survives leader changes) |
/poseidon/vips/next |
Next VIP index |
/poseidon/vips/allocations/ |
VIP-to-task mappings |
/poseidon/vips/ips/ |
Task-to-VIP mappings |
/poseidon/registry/services/ |
Service registry entries |
/poseidon/registry/instances/ |
Instance-to-service index |
# Start a second manager that joins the cluster bootstrapped above
go run main.go manager \
--dbType raft --node-id 2 --raft-port 9002 \
--data-dir /tmp/poseidon/m2 \
--raft-join localhost:9001 \
--workers localhost:5556The --raft-join flag takes one or more Zodiac HTTP addresses of existing cluster members. The joining node finds the leader and sends a join request.
# Run all tests
go test ./...
# Run package-level tests only (network, registry — use embedded Zodiac)
go test ./network/ ./registry/
# Run e2e tests (require embedded Zodiac, no Docker daemon needed)
go test -run TestE2E -v .Unit tests for network/ and registry/ use an embedded 3-node Zodiac test harness — no external dependencies. The e2e test at the root level exercises full task round-trips, leader failover, VIP pool allocation, DNS resolution, and registry service discovery.
Simple HTTP server task submitted via the CLI:
{
"ID": "6be4cb6b-61d1-40cb-bc7b-9cacefefa60c",
"State": 2,
"Task": {
"State": 1,
"ID": "21b23589-5d2d-4731-b5c9-a97e9832d021",
"Name": "test-chapter-5",
"Image": "strm/helloworld-http"
}
}Agent-managed system container (like a static pod):
{
"name": "poseidon-worker",
"image": "poseidon-worker:latest",
"restartPolicy": "always",
"ports": [
{ "containerPort": 5556, "protocol": "tcp" }
],
"env": {
"POSEIDON_WORKER_PORT": "5556",
"POSEIDON_WORKER_DBTYPE": "memory"
},
"cpus": 0.25,
"memory": 134217728
}