No description
  • Go 49.4%
  • Vue 37.6%
  • TypeScript 7%
  • CSS 4.8%
  • Shell 0.5%
  • Other 0.7%
Find a file
NhProGamer 984bfe1d3c
All checks were successful
CI / frontend (push) Successful in 30s
CI / backend (push) Successful in 1m5s
Release / goreleaser (push) Successful in 1m28s
Release / docker (push) Successful in 1m34s
feat(stacks): re-attach to a running deploy's live log
A streamed deploy runs detached, so reloading the page (or a webhook-triggered
deploy) left the client unable to follow its log — only cancel it. Add an
in-memory fan-out hub per in-flight deploy that buffers emitted lines; the
initiating stream and any later subscriber attach to it (replay so far, then
live). New GET .../deploy/attach SSE endpoint re-attaches, emitting an "idle"
frame when nothing is running so the UI settles into history. The editor
auto-attaches on mount when the stack is "deploying".
2026-07-20 23:22:43 +02:00
.forgejo/workflows ci: run goreleaser with GITHUB_TOKEN stripped (runner auto-injects it) 2026-07-20 18:28:50 +02:00
cmd/agent ci: Forgejo CI + release pipelines 2026-07-20 18:20:02 +02:00
deploy chore: project scaffolding, tooling and docs 2026-06-27 11:00:04 +02:00
internal feat(stacks): re-attach to a running deploy's live log 2026-07-20 23:22:43 +02:00
web feat(stacks): re-attach to a running deploy's live log 2026-07-20 23:22:43 +02:00
.dockerignore build: Dockerfiles for agent and web dev 2026-06-27 13:58:05 +02:00
.env.dev.example chore: dev docker compose for the full stack 2026-06-27 13:58:05 +02:00
.env.example test: compose validation and stack integration; docs for stacks 2026-06-27 12:38:17 +02:00
.env.prod.example feat(deploy): move Postgres and Traefik data to host bind-mounts 2026-07-20 22:09:37 +02:00
.gitignore chore: project scaffolding, tooling and docs 2026-06-27 11:00:04 +02:00
.goreleaser.yaml ci: fix pnpm activation (packageManager) and goreleaser config 2026-07-20 18:24:47 +02:00
docker-compose.dev.yml feat(proxy): materialize manual TLS certs into the shared dynamic dir 2026-07-06 21:18:40 +02:00
docker-compose.prod.yml feat(deploy): move Postgres and Traefik data to host bind-mounts 2026-07-20 22:09:37 +02:00
Dockerfile ci: Forgejo CI + release pipelines 2026-07-20 18:20:02 +02:00
go.mod feat(auth): built-in local admin (setup + login) alongside OIDC 2026-07-20 18:03:07 +02:00
go.sum feat(settings): schedule the image poller with a cron expression 2026-06-30 14:34:44 +02:00
install.sh feat(deploy): move Postgres and Traefik data to host bind-mounts 2026-07-20 22:09:37 +02:00
Makefile chore: dev docker compose for the full stack 2026-06-27 13:58:05 +02:00
README.md feat(proxy): agent-managed Traefik, removed from prod compose 2026-07-20 21:27:50 +02:00
sqlc.yaml chore: project scaffolding, tooling and docs 2026-06-27 11:00:04 +02:00

Orion Harbor

A per-node agent that exposes a stable, protected REST API over Docker and Podman (via their Docker-compatible Unix socket), so multiple specialised frontends (CI/CD dashboards, app deployment panels, …) can drive containers without ever touching the raw engine socket.

It is, in spirit, a self-hostable building block for a Dokploy / Portainer-style platform: full container lifecycle, managed (declarative) applications, real-time logs/stats/events, OIDC auth and RBAC — with Docker Compose stacks planned next.

Why an agent in front of the runtime?

Docker and Podman both warn that direct socket/API access is equivalent to arbitrary code execution on the host. Orion Harbor sits in between:

frontends ── HTTP/JSON ──> Orion agent ── Unix socket ──> Docker / Podman
                              │
                    OIDC/JWT · RBAC · audit · normalized API

The public API is ours, not a 1:1 mirror of the engine API — frontends never couple to Docker- or Podman-specific shapes.

Architecture

cmd/agent            entrypoint; subcommands: serve, migrate, dev-token
internal/
  config             env-driven config; auto-detects docker/podman sockets
  runtime            Adapter interface + normalized types (Workload, Event, …)
    dockercompat     one Docker-SDK adapter driving BOTH Docker and Podman
  auth               JWT verification: JWKS (OIDC, provider-agnostic) or dev HMAC
  rbac               role -> permission matrix
  db                 pgx pool + embedded goose migrations
  store              sqlc-generated, type-safe queries
  service            managed application workflow (persist spec + deploy/reconcile)
  audit              audit event + sinks (slog + async Postgres)
  api                Gin router, middleware, handlers

Data layer: goose (migrations) + sqlc (type-safe queries) + pgx — no ORM. Persistence is optional: with no ORION_DATABASE_URL the agent runs stateless (power/logs/events/raw create still work); applications & persistent audit need the DB.

Quick start

# 1. (optional) a local Postgres for managed applications + audit
make db-up
export ORION_DATABASE_URL=postgres://orion:orion@localhost:5432/orion?sslmode=disable
make migrate

# 2. dev auth secret, then run (auto-detects your docker/podman socket)
export ORION_AUTH_DEV_SECRET=dev-secret
make run

# 3. mint a token and call the API
TOKEN=$(make -s token ROLES=deployer)
curl -H "Authorization: Bearer $TOKEN" localhost:8080/api/v1/nodes

See .env.example for all settings, including production OIDC/JWKS.

Production install

One guided command installs the full stack — Traefik + Postgres + agent + web UI — under /etc/orion-harbor:

curl -fsSL https://git.nhsoul.fr/nhpro/orion-harbor/raw/branch/master/install.sh | sudo bash

It detects your engine (podman/docker) and socket, then prompts for the public domain, Let's Encrypt e-mail and Let's Encrypt staging (test certs). It generates POSTGRES_PASSWORD and ORION_SECRET_KEY, writes /etc/orion-harbor/.env + docker-compose.yml, pulls the (public) images and starts everything.

Prerequisites: DNS ORION_DOMAIN → this server's IP. First visit to https://<domain> creates the local admin (or use OIDC).

Ports — only Traefik binds the host; everything else stays on the internal orion-proxy network. The installer opens 80/443 (ufw/firewalld) automatically.

Port Service Exposure
80/443 Traefik host (open in firewall)
8080 agent API internal only
80 (web), 5432 (postgres) web / db internal only

TCP/UDP stack routes need a dedicated Traefik entrypoint + published port: with the compose-managed Traefik, add them to its command and ports, and open the port. (The in-app agent-managed Traefik does this dynamically.)

Manual (equivalent):

cp .env.prod.example .env      # fill ORION_DOMAIN, ACME_EMAIL, ORION_SECRET_KEY, ...
podman compose -f docker-compose.prod.yml up -d

Traefik is managed by the agent, not the compose: at first boot the agent auto-provisions it (shared orion-proxy network, a default le ACME resolver from ACME_EMAIL), binds host 80/443, serves the UI over HTTPS on ORION_DOMAIN and routes every stack you deploy. Configure resolvers (incl. DNS-01 wildcard), staging, log level and TCP/UDP entrypoints from the Reverse proxy menu — the agent reconfigures Traefik live. The web UI is also exposed directly on WEB_PORT (default 8088) as an admin fallback.

Update in place:

sudo /etc/orion-harbor/install.sh update   # re-fetch compose, pull, up -d

Auth

The agent ships a built-in local admin (created on first run) that coexists with OIDC. For more than one user, wire an OIDC provider (ORION_OIDC_JWKS_URL / ISSUER / AUDIENCE) instead of adding local users.

API (v1)

Unauthenticated probes:

Method Path Purpose
GET /healthz liveness
GET /readyz readiness (runtimes reachable)

Everything below is under /api/v1, requires a bearer token, and is guarded by a permission. :node selects a runtime node.

Method Path Permission
GET /nodes nodes:read
GET /nodes/:node/capabilities nodes:read
GET /nodes/:node/workloads ?all=true containers:read
POST /nodes/:node/workloads (raw create) containers:write
GET /nodes/:node/workloads/:id containers:read
DELETE /nodes/:node/workloads/:id containers:write
POST /nodes/:node/workloads/:id/power containers:power
GET /nodes/:node/workloads/:id/exec (WebSocket) containers:exec
GET /nodes/:node/workloads/:id/logs (SSE) logs:read
GET /nodes/:node/workloads/:id/stats (SSE) containers:read
GET /nodes/:node/events/stream (SSE) events:read
POST /nodes/:node/images/pull (SSE) images:pull
GET /nodes/:node/volumes volumes:read
POST /nodes/:node/volumes volumes:write
DELETE /nodes/:node/volumes/:name ?force=true volumes:write
GET /nodes/:node/networks networks:read
POST /nodes/:node/networks networks:write
DELETE /nodes/:node/networks/:id networks:write
GET /nodes/:node/images images:read
DELETE /nodes/:node/images ?ref=...&force=true images:write
POST /nodes/:node/images/prune images:write
GET /registries registries:read
PUT /registries (body {host,username,password}) registries:write
DELETE /registries/:id registries:write
POST /nodes/:node/applications ?deploy=true containers:write
GET /nodes/:node/applications containers:read
GET /nodes/:node/applications/:appId containers:read
POST /nodes/:node/applications/:appId/deploy containers:write
DELETE /nodes/:node/applications/:appId ?removeVolumes=true containers:write
GET /nodes/:node/applications/:appId/deployments containers:read
POST /nodes/:node/stacks ?deploy=true | ?dryRun=true stacks:write
GET /nodes/:node/stacks stacks:read
GET /nodes/:node/stacks/:stackId stacks:read
PUT /nodes/:node/stacks/:stackId stacks:write
POST /nodes/:node/stacks/:stackId/deploy stacks:write
POST /nodes/:node/stacks/:stackId/stop stacks:write
DELETE /nodes/:node/stacks/:stackId ?removeVolumes=true stacks:write
GET /nodes/:node/stacks/:stackId/deployments stacks:read
GET /nodes/:node/stacks/:stackId/services stacks:read
GET /nodes/:node/stacks/:stackId/services/:service/logs (SSE) stacks:read

Streaming endpoints use Server-Sent Events.

Compose stacks

A Stack is a stored Docker Compose project. Orion does not reimplement compose: it validates with compose-go and applies with the compose CLI (docker compose, docker-compose, or podman compose — auto-detected, overridable via ORION_COMPOSE_CMD), running it against the node's socket. Per-service status and logs are served from the runtime by filtering on the com.docker.compose.* labels. Requires a compose binary on the node; without one, deploys return 503.

# validate without persisting
curl -X POST -H "Authorization: Bearer $TOKEN" \
  "localhost:8080/api/v1/nodes/local-podman/stacks?dryRun=true" \
  -d '{"compose":"services:\n  web:\n    image: nginx:alpine\n"}'

# create and deploy
curl -X POST -H "Authorization: Bearer $TOKEN" \
  "localhost:8080/api/v1/nodes/local-podman/stacks?deploy=true" \
  -d '{"name":"site","compose":"services:\n  web:\n    image: nginx:alpine\n    ports:\n      - \"8080:80\"\n"}'

Private images work: stored registry credentials are rendered into a Docker config.json in the stack workdir and exported via DOCKER_CONFIG, so compose authenticates pulls automatically (requires ORION_SECRET_KEY and configured registries).

Private registries

Registry credentials are stored encrypted (AES-256-GCM, keyed by ORION_SECRET_KEY) and resolved automatically when pulling an image whose host matches a configured registry — for both raw pulls and managed deploys. Requires persistence and a secret key; without them the /registries endpoints return 503.

export ORION_SECRET_KEY=$(openssl rand -base64 32)   # 32 bytes, base64
curl -X PUT -H "Authorization: Bearer $TOKEN" localhost:8080/api/v1/registries \
  -d '{"host":"registry.example.com","username":"alice","password":"token"}'

Managed applications

An Application is the desired spec of a container, persisted in Postgres. Deploying it pulls the image, removes the previous instance (correlated via the orion.app.id label) and creates a fresh container, recording each attempt in the deployments history.

curl -X POST -H "Authorization: Bearer $TOKEN" \
  "localhost:8080/api/v1/nodes/local-podman/applications?deploy=true" -d '{
    "name":"web","image":"docker.io/library/nginx:alpine",
    "ports":[{"container":80,"host":8080}],
    "restartPolicy":"unless-stopped"
  }'

Roles

Role Permissions
viewer read containers, logs, events, nodes, volumes, networks, images
operator viewer + power (start/stop/restart/kill)
deployer operator + write (containers, volumes, networks, images, stacks) + exec + image pull + read registries
game-admin same as deployer (game features later)
node-admin everything (*), including managing registry credentials

Auth

  • Production: OIDC via JWKS — ORION_OIDC_JWKS_URL (+ optional issuer/audience). Provider-agnostic (Keycloak, Authentik, Authelia, …). Roles come from a configurable claim (ORION_OIDC_ROLES_CLAIM, defaults to roles; Keycloak's realm_access.roles is handled too).
  • Development: ORION_AUTH_DEV_SECRET enables HS256 dev tokens; mint with agent dev-token --sub alice --roles deployer.

Development

make build        # compile
make test         # unit + integration (integration needs ORION_DATABASE_URL)
make sqlc         # regenerate store/db from SQL
make db-up/db-down

Roadmap

  • V1 agent, runtime adapter, auth, RBAC, list/power/logs/events
  • V1.5 full container lifecycle (create/rm/recreate/stats/pull) + persistence (managed applications, deployments, audit)
  • V2 volumes / networks / images (list/remove/prune) + private registries (encrypted credentials, auto-resolved on pull) + test foundation
  • V3 Docker Compose stacks (compose-go validate + controlled CLI apply, per-service status & logs, private-registry auth, deployment history)
  • V4 notifications, backups, GitOps
  • V5 game-server panel (last)