Files
Containr/docs/guides/CLI.md
T
Tomáš DvořákandDevin 5eac4a382b feat: activity feed — enriched audit events + global/project timelines (#45)
* feat(branding): white-label — public /branding, admin write, shell theming

app_settings branding.* keys (product name, logo/favicon URLs, accent
color, docs/support links). Public GET /branding so login and shell
theme pre-auth; admin writes via PUT /settings.

useBranding() applies document.title, accent CSS var and favicon;
BrandWordmark replaces hardcoded wordmarks; admin Platform settings
gain a Branding form. MCP containr_branding_get + settings_update.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(invites): team invite links — admin create/revoke, public accept

Admins mint single-use invite links (sha256-hashed token at rest,
optional email binding, configurable TTL). Public endpoints let the
accept page validate the link and register — working even while
public signup is disabled.

- migration + sqlc queries/model for user_invites
- POST /admin/invites (returns one-time token+url), GET list, DELETE revoke
- GET /auth/invites/:token public validation, POST /auth/accept-invite
- CLI containr invites list|create|revoke; MCP invites tools
- /auth/accept-invite page (expired/used/invalid states, bound-email lock)
- Admin page Team invites section: create, copy link, revoke
- OpenAPI + generated types + CLI guide

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(admin): impersonate user — 15-min token, audit-logged

POST /admin/users/:id/impersonate mints a short-lived bearer token
(impersonated_by claim) for support/debugging. Admin accounts cannot
be impersonated; every mint is audit-logged with actor + target.

- CLI: containr admin impersonate <user-id> (works via CONTAINR_TOKEN)
- MCP: containr_admin_impersonate
- Admin UI: per-user Impersonate action → token modal with usage hint
- OpenAPI + generated types

Verified live: token resolves to target, admin target 403, audit row.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(git): GitHub App manifest self-provisioning + app-level webhook

Admins create the instance GitHub App in-browser instead of hand-
setting GITHUB_APP_* env vars:

- POST /admin/git/github-app/manifest builds the manifest + URL;
  the frontend auto-submits it to github.com
- /git/github-app/callback page exchanges the code via
  POST /admin/git/github-app/convert; credentials persist in
  app_settings (precedence over env)
- GET /admin/git/github-app reports configured/source
- POST /api/git/github-app/webhook — single app-level push receiver,
  HMAC-verified, dispatches via extracted dispatchPushToServices
- install-url + installation-token paths resolve settings→env
- CLI: containr admin github-app status|manifest|convert; MCP tools
- Git providers UI shows "Set up GitHub App" for admins when
  unconfigured

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat: activity feed — enriched audit events, global + per-project timelines

audit_logs gains severity/category/label columns derived from
resource+action at write time; legacy rows are classified on read and
backfilled at boot in batches.

- GET /activity: global feed scoped to the caller's visible projects
  (admins see all); severity/category/resource/project_id/since
  filters, page+limit
- GET /projects/:id/activity: timeline correlated to the project's
  services, deployments, databases and the project record; access
  mirrors project read rules
- Retention worker prunes rows older than AUDIT_RETENTION_DAYS
  (default 90, 0 disables), running once at boot then daily
- project.create/delete now audited so timelines aren't empty
- CLI `containr activity` with filter flags; MCP tools
  containr_activity_list + containr_project_activity
- Frontend: /activity page (severity chips, icon per category) under
  Operate nav; project workspace gains an Activity view tab
- OpenAPI + generated types + CLI docs

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>
2026-10-09 10:52:15 +02:00

11 KiB

Containr CLI

The CLI controls every part of a Containr instance — the same API surface the web UI uses. Build it with scripts/build-cli.sh or go build ./cmd/cli from app/backend.

Authentication

Create a personal access token in the web UI under Settings → Personal Access Tokens (or containr tokens create), then:

containr auth login cnp_...                      # verify + store
containr auth login cnp_... --url https://containr.example.com
containr auth login cnp_... --name production    # named profile
containr auth status                             # show active profile + identity
containr auth logout

Resolution order for every command: --token/--api-url flags > CONTAINR_TOKEN/CONTAINR_API_URL env > active profile > http://localhost:8080/api/v1. The active profile is --profile > CONTAINR_PROFILE > current_profile in ~/.containr.yaml.

Output and scripting

  • --json prints the raw API response — safe to pipe and parse.
  • --yes / -y skips confirmation on destructive commands (delete, revoke, restore, failover). Non-interactive shells refuse destructive commands without it.
  • Exit codes are stable: 0 ok, 1 error, 2 auth/permission, 3 not found, 4 validation, 5 unreachable.

Commands

auth login|logout|status         token auth and profiles
tokens list|create|revoke        personal access tokens
projects list|get|create|delete
services list|get|create|update|delete|start|stop|restart|redeploy
services clone|move              duplicate config or re-parent a service
services domains list|add|remove|default|check   multi-domain + DNS preflight
registries list|add|update|remove  private-image pull credentials
volumes list|delete              docker volume inventory + cleanup (admin)
deploy <service-id>              trigger a deployment (--no-cache, --commit-hash, --branch)
deployments list|get|logs|rollback|cancel
logs <service-id> [--follow]     runtime logs (SSE follow)
exec <service-id> -- <cmd>       one-off container command
variables list|set|unset         env vars (secrets masked; --redeploy applies them)
variables shared list|set|unset  project shared vars — referenced as ${{shared.KEY}}
databases list|get|create|update|delete|action|backup|restore|download-backup|import-backup
                               import-backup <id> <file.tar.gz|-> uploads an archive as a restore
                               point; ships offsite too when a backup target is assigned
databases list|get|create|update|delete|action|backup|restore|download-backup
                               create --public exposes the port on all interfaces;
                               update <id> --public/--public=false toggles it (container recreates, data persists)
databases register <name>      register an external database (--type --host --port --database --username --password --ssl; probed before storing)
databases test-connection      probe a connection — by id, or ad-hoc flags
backup-targets list|add|update|remove|test
                               S3-compatible archive destinations; add/update probe the bucket
                               first, empty --access-key/--secret-key on update keep stored keys;
                               update <db-id> --backup-target assigns a target ("" clears it)
cron list|get|create|delete|trigger|executions
templates list|get|create|delete|deploy|plan|import-compose|deploy-graph
                               deploy --name --var K=V; plan dry-resolves expressions;
                               import-compose <compose.yml|-> converts a compose file into
                               a v2 graph config; deploy-graph <project-id> <config.json|->
                               deploys an ad-hoc service graph
import compose <project-id> <compose.yml|->
                               deploys a docker-compose file straight into a project —
                               no template row. DB images become managed databases
                               attached under their service name; --var K=V overrides
                               template variables
nodes list|get|delete|tokens     node agents + onboarding tokens (admin)
scaling status|policies|services|scale
ha status|alerts|health|policies|enable|disable|failover
security scan|vulnerabilities|metrics
gateway services|keys|analytics  (admin)
admin overview|users|settings|audit-logs
notifications list|read
webhooks list|create|update|delete|deliveries|test
                               outbound signed webhooks — create <name> <url>
                               --event 'service.*' (repeatable, '*' = all);
                               deliveries are HMAC-signed (X-Containr-Signature)
operations                       live ops view: active deploys, queue depth,
                               24h failures, cron runs, backups
activity                         enriched activity feed; --severity/--category/
                               --resource/--project/--limit filters
banners list|create|update|delete  announcement banners (admin);
                               active banners print on `auth status`
invites list|create|revoke       team invite links (admin); create
                               prints the one-time acceptance URL
admin impersonate <id>           mint a 15-min token acting as a user
admin github-app status|manifest|convert
                               self-provision the instance GitHub App
up                               deploy the current directory

containr up

Run inside a git checkout: uses remote.origin.url + the current branch as the build source, creates a service named after the directory when none exists, then triggers a deployment.

containr up --project <id>
containr up --project <id> --image ghcr.io/example/app:latest   # image, no build

Set project_id on the active profile to omit --project (stored in ~/.containr.yaml under profiles.<name>.project_id).

Service volumes

services update manages mounts; flags replace the whole mount list:

containr services update <id> --volume data:/data --bind /srv/cfg:/etc/app:ro
containr services update <id> --clear-volumes

type is volume (docker named volume, auto-created) or bind (host path). target must be absolute. Mounts apply on the next deploy or redeploy — containr deploy <id> to apply immediately.

volumes list shows the node's docker volume inventory with in-use flags; volumes delete <name> removes an unused volume (admin scope).

Service domains and access

A service can carry multiple hostnames; the default is mirrored to services.domain:

containr services domains add <id> app.example.com --default
containr services domains check <id>      # DNS preflight per domain
containr services domains remove <id> <domain-id>

DNS check compares each domain to the expected target — set app_settings.public_ip (or PUBLIC_IP env) so wrong-target is meaningful; without it any resolvable domain reports ok.

Access gates apply via Traefik middlewares (no extra proxy):

containr services update <id> --maintenance on
containr services update <id> --basic-auth admin:secret --basic-auth ops:pw2

Maintenance mode redirects all traffic to <BASE_URL>/api/v1/maintenance — set app_settings.base_url or BASE_URL; without it the service is taken offline with empty responses. Basic-auth credentials are stored bcrypt-hashed (htpasswd) and gate every request.

Build strategy and resources

Source-built services pick a builder per service:

containr services update <id> --builder railpack   # auto|railpack|nixpacks|dockerfile|static
containr services update <id> --builder static --static-cmd "npm run build" --static-dir dist

auto detects from repo contents (Dockerfile → railpack → nixpacks). static builds assets in a node stage and serves them from nginx — --static-cmd runs inside the build stage (default npm ci && npm run build), --static-dir is copied into the nginx image (default dist, exposed on port 80).

Resource reservations complement the hard cpu/memory limits:

containr services update <id> --cpu-reserve 0.25 --memory-reserve 128Mi

cpu/memory are hard caps (NanoCPUs/Memory); cpu_reserve/ memory_reserve are soft hints (CPU shares, MemoryReservation). Empty string clears. app_settings.default_cpu/default_memory change the instance-level defaults used at service creation; app_settings.capacity_policy=block fails deployments whose requested memory exceeds node capacity (default warn logs only).

Sleep mode (scale-to-zero)

Services can scale to zero after an idle window and resume on traffic:

containr services update <id> --sleep --sleep-idle 15   # enable, 15 min idle timeout
containr services update <id> --sleep=false            # disable
containr services sleep <id>                           # sleep now
containr services wake <id>                            # resume now
containr services env-check <id>                      # flag empty vars + unresolved ${{refs}}

Idleness is measured as flat container network I/O over the configured window (swept every 30s). Sleeping removes the workload containers and — when the service has domains — installs a busybox placeholder that holds the Traefik route at higher priority, serves a reloading "waking" page, and pings GET /api/v1/internal/wake/:id to reconcile the service back. The API must be reachable from containers for the callback (containerized deploys work via the shared network; bare-metal needs HOST=0.0.0.0).

Clone, move, registries

services clone duplicates config, volumes, domains, access gates, builder settings, and variables — ciphertext secret values are copied verbatim (no decrypt/re-encrypt). The clone starts stopped:

containr services clone <id> --name staging-api --project <other-project-id> --environment preview
containr services move <id> <project-id>    # name conflicts → 409

Note: cloned domains are verbatim copies — the same hostname attached to two services makes Traefik routing ambiguous. Re-domain or undeploy one.

registries stores private-image pull credentials, owner-scoped and encrypted at rest. Matched by image host on every image-sourced deploy:

containr registries add ghcr ghcr.io --username <user> --password <pat>
containr registries add hub docker.io --username <user> --password <pw>   # Docker Hub
containr registries update <id> --username <new-user>   # empty --password keeps stored
containr registries remove <id>

MCP server

containr-mcp (go build ./cmd/mcp) exposes the same API as MCP tools over stdio — every command group above maps to a containr_* tool (~60 tools). Destructive tools require the caller to pass confirm: true; results are pretty JSON with isError on failures.

Client configuration (Claude Code, Cursor, Devin, any stdio MCP host):

{
  "mcpServers": {
    "containr": {
      "command": "containr-mcp",
      "env": {
        "CONTAINR_API_URL": "https://containr.example.com",
        "CONTAINR_TOKEN": "cnp_..."
      }
    }
  }
}

Scope the token deliberately: read is enough for inspection agents, write for deploy automation, admin only for platform operations.

Deployments are serialized per service: a second deploy on a busy service lands in queued state and runs when the active one finishes. deployments cancel <id> drops a queued deployment or aborts a running one (builds/reconciles stop via context cancellation).