* refactor(db): port operations, settings, audit, activity, and misc service metadata to sqlc
Covers the cross-cutting surface: operations board reads, app settings,
notification producers, activity/audit inserts, builds upsert/cancel,
git clone credential resolution, invites, environments, registry auth,
project/service variables, HA resolver, boot sweeps, and admin user ops.
Dynamic filtered lists and Better Auth table probes stay raw by design.
Authored By: TDvorak <[email protected]>
* refactor(db): port apwhy gateway handlers and proxy data plane to sqlc
Final tranche — every static call site now uses generated queries. Raw SQL
remains only where the statement is assembled at runtime (filtered lists,
sparse updates, admin count closure) or probes Better Auth's auth_users
table, which lives outside the local schema.
Authored By: TDvorak <[email protected]>
services.traefik_labels (jsonb) holds operator-defined Traefik
middlewares keyed 'middlewares.<name>.<type>[.<field>]', validated
against an allowlist at write time. edgeLabels (extracted from
createReplica) scopes each definition to the service router as
<router>-<name> and auto-attaches it to the router's middleware chain
after builtins. Router/service/provider-level keys are rejected.
Surfaces: create/update API fields, services create/update
--traefik-label (repeatable, key=value), MCP arg docs, OpenAPI +
regenerated types, service-page Access editor (Routing overrides
card + textarea).
Also marks three stale ROADMAP2 items shipped: notification producers
round-out, build-on-node (PR #60), mobile env/domains/scaling (PR #61).
Generated with [Devin](https://devin.ai)
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- Wake placeholder fired its wake callback unconditionally at boot, so
sleeping services flapped sleep→wake on every idle timeout. The wake
call now lives in a CGI script the busybox httpd runs on inbound hits;
E404 covers non-root paths. URLs/page travel via env, no quoting.
- Wake endpoint tries ordered candidate addresses (self-name on shared
net, host.docker.internal, edge gateway, probe) — Docker Desktop's VM
can't reach the bridge gateway, so single-URL wake was dead there.
- CreateServiceRequest accepts sleep_enabled/sleep_idle_minutes and the
INSERT persists them — previously update-only, silently dropped at
create. CLI `services create` gains --sleep/--sleep-idle; MCP doc lists
both; OpenAPI node_id description updated for build-on-node.
- Wake goroutine logs spec/reconcile failures instead of a bare return,
and marks the service failed when spec build errors.
- scripts/build-cli.sh ships containr-agent-linux-{amd64,arm64} so the
install-agent.sh GH-releases fallback actually resolves.
Verified live: create-time flags persist and validate (1-1440), service
sleeps on idle and stays asleep, an inbound hit on the placeholder wakes
it (~15s to running), and the placeholder is pruned after reconcile.
Generated with [Devin](https://devin.ai)
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Operator-set node labels (node_agents.tags) gate which nodes a service
may schedule onto: services.placement_tags requires every listed tag.
auto and spread filter eligible agents by tags; tags are re-evaluated
each reconcile so retagging a node re-spreads replicas. Pin + tags and
local + tags rejected; empty array clears the requirement.
Surfaces: --placement-tags on services create/update, nodes update
--tags, MCP args, OpenAPI, node-detail tag editor, service-detail
placement-tags input.
Generated with [Devin](https://devin.ai)
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
services.spread distributes replicas across every online, schedulable
agent (replica i → agents[i mod n], deterministic). auto placement now
orders by real headroom — memory utilisation, cpu usage, then container
count — instead of count alone.
Lifecycle (start/stop/restart/delete) and GET /runtime are now driven
by container_instances inventory, so remote replicas are visible and
tear down wherever they run — covers spread, pins, and stale pin
leftovers. Spread and explicit pins are mutually exclusive.
Generated with [Devin](https://devin.ai)
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat(scheduling): multi-node service placement + remote container lifecycle
services.node_id pins a service to a node agent: explicit id, "auto"
(least-loaded online node), or "local"/empty for the local Docker host.
- DeploymentEngine gains a NodeRunner seam; ReconcileService routes
pinned services to the agent's command queue (create_container with
image/env/command/ports/volumes/labels/resource limits), tracked in
container_instances with deterministic per-replica ids.
- Stop/start/restart/delete fan out to the pinned node — no more leaked
remote containers on service delete.
- Agent create_container is idempotent (rm -f first) and honors restart
policy, labels, memory/cpus, published+ephemeral ports, volumes, env.
- GET /nodes/options exposes id/name/status to authed users; full agent
management stays admin-only.
- CLI services create/update --node, MCP node_id docs, OpenAPI
node_id/node_name + /nodes/options, service-detail Placement section.
Ceilings: remote nodes run registry-pulled images only — git builds,
Traefik domains, and remote sleep detection stay local until
build-on-node and per-node ingress land.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat(scheduling): node cordon/drain + schedulable gate
- node_agents.schedulable column; "auto" placement and explicit pins skip
cordoned nodes; already-pinned services keep running (reconcile is
unaffected — cordon is not eviction).
- POST /agents/:id/cordon|uncordon|drain (admin). Drain cordons, removes
every service container via the agent queue, and clears the services'
pins so they redeploy on the local host. Requires the node online —
an offline node can't acknowledge removals.
- CLI nodes cordon|uncordon|drain (drain confirm-gated), MCP
containr_nodes_cordon|uncordon|drain, OpenAPI paths + schedulable
field, node-detail page cordon badge + drain/cordon controls.
- Fix: re-registering an agent no longer resets auto_prune.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat(scheduling): remote registry pulls + post-run health check
- Dispatch payload carries the project owner's registry credentials for
the image host; the agent docker-logins for the pull and logs out
after, so private images deploy to remote nodes.
- Completed command payloads get their registry password scrubbed —
secrets don't linger in agent_commands rows.
- Agent verifies the container is still running ~1.5s after docker run;
crash-loops now fail the deployment with the container's last log
lines instead of reporting "running" optimistically.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Services gain sleep_enabled + sleep_idle_minutes. A 30s sweeper tracks
container NetworkIO deltas; a flat counter for the configured window
scales the service to zero (status=sleeping, workload containers removed).
While slept, a busybox wake container takes over the service's Traefik
host rule at priority 100: it serves a self-reloading "waking" page and
calls the unauthenticated idempotent GET/POST /api/v1/internal/wake/:id,
which reconciles the service back to running. Wake placeholders carry a
dedicated label (containr.wake-for) so runtime state never counts them;
the sweeper prunes them once the workload is up. The wake URL resolves
the API via its container name when containerized, else the edge
network's IPAM gateway — bare-metal installs need HOST=0.0.0.0 for the
callback.
Surfaces: PUT fields on services, POST /services/:id/sleep|wake,
CLI --sleep/--sleep-idle + services sleep|wake, 2 MCP tools (89 total),
OpenAPI paths/schema, and a Settings > Sleep card with toggle, idle
minutes, and manual sleep/wake plus a Sleeping badge.
Verified live: idle sweep scaled whoami-web to zero; the wake container
served the page on the edge network and its callback woke the service
(container recreated, status running, placeholder pruned).
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
services.builder picks the build strategy per service: auto detects from
repo contents (Dockerfile → railpack → nixpacks), explicit overrides force
a builder, and static builds assets in a node stage then serves them from
an nginx image (static_build_cmd/static_dir, port 80 default).
- cpu_reserve/memory_reserve → Docker CPU shares + MemoryReservation;
cpu/memory stay hard caps (NanoCPUs/Memory). Empty string clears.
- app_settings.default_cpu/default_memory override service-create
fallbacks; capacity_policy=block fails deploys whose requested memory
exceeds node capacity (default warn logs via slog).
- Resource specs are validated — "512mb" fails instead of silently
dropping the limit.
- Fixes: deployments.go hardcoded nixpacks (auto-detect was dead code);
DockerfileBuilder passed the Dockerfile *path* as build-context content;
BuildImage never drained the daemon stream so builds could abort and
errors went unseen — stream is now consumed and errorDetail surfaced.
- CLI: --builder/--cpu-reserve/--memory-reserve/--static-cmd/--static-dir
on create+update; MCP args on services create/update; UI Build &
Resources card; OpenAPI + llms.txt + CLI.md.
Verified live: static build of a real repo → nginx image → deployed;
reserves reach HostConfig (shares=256, MemoryReservation=128Mi);
validation rejects bad specs; builder persists + clears.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
service_domains table holds the full hostname set; services.domain stays
the derived default and is synced on every domain write. Runtime spec
carries all domains into a multi-Host Traefik rule plus access gates.
- GET|POST /services/:id/domains, DELETE + POST /:domain_id/default
- GET /services/:id/domains/check — DNS preflight vs public_ip/base_url
(ok | wrong-target | pending), stamps last_checked_at
- maintenance_mode → Traefik redirectregex to /api/v1/maintenance
(base_url/BASE_URL setting; falls back to noop@internal offline)
- basic_auth [{username,password}] → bcrypt htpasswd on the label;
responses return usernames only
- First domain auto-defaults; domain="" via PUT clears all domains
- CLI: services domains list|add|remove|default|check, update
--maintenance/--basic-auth; MCP: 5 domain tools + update args
- Settings UI: Domains card (CRUD + DNS check badges) + Access card
- Fixes latent C1 bug: PUT without volumes 500'd — nil []byte sent as
bytea, not NULL; COALESCE needs untyped nil + ::jsonb cast
Verified live: domain CRUD + default sync, pending/wrong-target checks,
bcrypt htpasswd and both middlewares on the deployed container's labels.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
services.volumes JSONB [{type: volume|bind, source, target, read_only}]
mapped into every replica's container config at deploy and reconcile
time, hashed into the runtime spec so mount changes trigger
reconciliation. Mounted via existing docker.HostConfig.Mounts.
- RuntimeSpec.Volumes + specHash + mount.Mount mapping in createReplica
- Create/update service accepts volumes; PUT replaces the whole list
- Replica-count reconcile now goes through the deploy queue — the last
raw `go` reconcile left that could race a build
- GET /admin/volumes inventories node volumes with in_use flags;
DELETE /admin/volumes/:name removes unused ones (409 on in-use)
- CLI: services update --volume/--bind/--clear-volumes, volumes
list|delete; MCP: services_update + volumes_list/delete
- Settings UI: Volumes section with mount editor
- Migration normalizes the nullable baseline column to NOT NULL [].
Verified live: deploy applies mounts to the container, docker
auto-creates named volumes, in-use deletion is refused.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix: e2e QA hardening — honest runtime URLs, health, and read-only anon UI
Backend:
- managed DB provisioning: attach DB containers to the platform network and
probe the container IP, fixing the 127.0.0.1 reachability failure inside
the backend container; log provisioning failures
- stop fabricating connection URLs when provisioning produced none
- template deploys now carry port + health_check into service runtime fields
- service health probed over the project network (backend joins it) instead
of an in-container wget that fails on shell-less images
- runtime/public/connection URLs rewrite loopback to the request Host so
links are usable for LAN/remote clients
Frontend:
- service Networking panel shows real runtime URLs/ports/health instead of
hardcoded containr.dev + 8080->443
- anonymous views hide all mutation controls (new project, add service,
group, context menu)
- project cards: remove decorative svcN labels; badge says DOWN only when
zero services run, IDLE when empty, DEGRADED for partial
- mobile projects header no longer clips (fluid search width)
- unknown routes render a not-found page instead of silently redirecting
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix: telemetry scope label, chunk split, remove stale vite artifacts
- /system/host now reports scope=host|docker-desktop-vm and the daemon OS;
Projects dashboard labels cards VM.* when the daemon is a Desktop VM so
memory/core numbers aren't mistaken for physical host values
- manualChunks: split @xyflow/react and recharts out of the main bundle;
removes the >1MB chunk warning
- drop tracked vite.config.js/.d.ts build artifacts that shadowed the real
vite.config.ts
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix: persist service host port; demo-data scope for telemetry
Public URLs were ephemeral: every restart/redeploy rebound a random host
port, so endpoint links rotted. Persist published_port on the service and
prefer it for replica 0 on create. If the port is taken, fall back to
ephemeral and repersist the new assignment. Post-start the binding is
verified — Docker Desktop accepts a conflicting publish silently, which
previously left services running with no endpoint at all.
demo-data.ts gains the required scope field for HostMonitoring (CI fix).
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Services now reconcile into actual Docker containers instead of
bookkeeping rows:
- Per-project bridge network with service-name DNS aliases and
${{service.KEY}} env expansion (HOST/PORT builtins)
- Replicas, published ports, Traefik domain routing, health checks,
restart policies persisted on the service spec; spec-hash drift
recreates containers on redeploy
- Lifecycle API: start/stop/restart/redeploy, live runtime state,
real image rollback (replaces the fake sleep)
- Manual scale and PUT replicas reconcile live containers; service
and project delete remove their containers and the project network
- Canvas: real URLs, replica badges, env filter, right-click
deploy/restart/stop/delete menu, empty state
- Service detail: real lifecycle buttons, live replica/URL strip,
Networking editor, ${{ }} variable helper
- Fix prebuilt-image pulls racing the tag (drain the pull stream)
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Backend:
- GET /services/:id/metrics with real Docker container stats
- DELETE /git/providers/:id and GET repo branches endpoint
Frontend:
- Service detail shows real telemetry or honest empty states
- Live status distinguishes idle/polling/reconnecting
- Git providers section in Settings; repo+branch pickers in Add Service
- Real logo, compact Projects header, canvas fitView fix
- index.css deduplicated (1725 -> 1384 lines)
- Fixed smoke tests, lint errors, ToastProvider/context unification
Repo hygiene:
- Added LICENSE (MIT), CONTRIBUTING.md, SECURITY.md; rewrote README
- Untracked .env.prod and data/letsencrypt secrets; fixed .gitignore
- Removed legacy root src/internal/cmd and stale configs/docs/artifacts
- Moved guides and design references under docs/
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>