Command Palette
Search for a command to run...

Installation

power-manage deploys as a Compose stack. Six containers: Traefik, Postgres, Valkey (Redis-compatible; runs the Asynq queue and the search indexes), the control server, the gateway, and the indexer. A setup.sh helper generates the cert chain, secrets, and valkey.conf so you don't have to assemble them by hand.

Prerequisites

  • Docker 24+ or Podman 4.5+ and the Compose plugin
  • Two DNS names pointing at the host: one for the control server to point the web UI at (control.example.com), one for the agent gateway (gateway.example.com). If you plan to use the remote terminal, a third name (tty.example.com) too.
  • TCP port 443 reachable from the public internet (Traefik handles Let's Encrypt; the gateway uses SNI-based TCP passthrough)
  • openssl and bash on the host

Setup

git clone https://github.com/manchtools/power-manage-server.git
cd power-manage-server/deploy
./setup.sh

setup.sh prompts you through the required configuration on a fresh install and writes everything to .env. Re-running it is safe. Any non-placeholder .env value is kept silently; to rotate a value, edit .env directly and re-run. You'll be asked before regenerating any certificate under deploy/certs/. The rendered valkey.conf is always rewritten from the current .env, so a password change there picks up on next run.

What goes in .env

If you'd rather edit by hand, here's the shape of the file (deploy/.env.example is the annotated reference). The minimum to bring the stack up is:

# Domains
CONTROL_DOMAIN=control.example.com
GATEWAY_DOMAIN=gateway.example.com
GATEWAY_TTY_DOMAIN=tty.example.com   # only if you'll use the remote terminal
ACME_EMAIL=ops@example.com

# Database
POSTGRES_PASSWORD=<64 hex chars>
INDEXER_POSTGRES_PASSWORD=<64 hex chars>

# Redis + Asynq HMAC
VALKEY_PASSWORD=<64 hex chars>
PM_TASK_SIGNING_KEY=<64 hex chars>

# Auth
JWT_SECRET=<at least 32 chars; longer is better>
CONTROL_ENCRYPTION_KEY=<64 hex chars>   # AES-256 for secrets-at-rest

# First admin (created on first boot only)
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=<strong password>

Optional toggles you may want to know about:

VariableDefaultWhat it does
CONTROL_PASSWORD_AUTH_ENABLEDtrueWhen false, password sign-in is disabled fleet-wide and only SSO works.
CONTROL_SSH_ACCESS_FOR_ALLfalseAuto-create SSH actions so every user can SSH to every device they have access to.
DYNAMIC_GROUP_EVAL_INTERVAL1hHow often the server drains the dynamic-group re-evaluation queue. Clamped to a min of 30m and a max of 8h — values outside that range are rounded into the range at startup; 0 disables the worker.

First boot

docker compose up -d

Traefik usually has certificates in under a minute. Once they're issued, sign in: open the web UI at https://app.power-manage.manchtools.com, point it at your control-server domain (control.example.com), and use the admin credentials from setup. The admin account is created on first boot only; afterwards you should add a real SSO provider and treat the bootstrap account as break-glass.

For details on how the hosted UI talks to your server (and what it does not see), read The web UI.

The stack runs six containers:

  • Traefik terminates TLS and routes traffic. SNI-based TCP passthrough sends agent mTLS straight to the gateway.
  • Postgres holds the event store and projections.
  • Valkey runs the Asynq task queue, the search indexes (valkey-search), and short-lived auth state.
  • Control serves the Connect-RPC API on :8081 and the internal mTLS-protected InternalService on :8082.
  • Gateway terminates agent mTLS and runs the bidirectional Connect-RPC stream on :8080. When the remote terminal is configured, a separate cleartext terminal-WebSocket listener (GATEWAY_WEB_LISTEN_ADDR, :8443 in the reference stack) runs behind Traefik's TLS termination.
  • Indexer consumes events off Valkey and writes the search indexes. Stateless. Run more than one if you want.

Enrolling your first agent

The agent ships as a single install.sh published with every release. It downloads the binary, sets up the power-manage-agent systemd unit, and enrols against the control server in one step. There's no .deb or .rpm package today; the curl pipe is the only supported install path.

On any Linux endpoint, generate an enrolment token from the web UI (DevicesEnrolment tokensCreate token) and then:

curl -fsSL https://github.com/manchtools/power-manage-agent/releases/latest/download/install.sh \
  | sudo bash -s -- \
    -s https://control.example.com \
    -t <enrolment-token-from-web-UI>

Use --pre to install the latest release candidate instead of the stable release.

Useful flags (--help for the full list):

FlagDefaultWhat it does
-s, --server URLControl-server URL the agent enrols against
-t, --token TOKENRegistration token from the web UI
--preoffInstall the latest prerelease instead of stable
-v, --version VERSIONlatestPin to a specific release tag (e.g. v2026.06)
-d, --data-dir DIR/var/lib/power-manageOverride the agent's data directory
-b, --binary PATH/usr/local/bin/power-manage-agentWhere the agent binary lives
--skip-downloadoffUse the binary already on disk at -b instead of fetching
--uninstallRemove the agent and its config

The install script registers the agent through a local enrolment socket at /run/pm-agent/enroll.sock. The control server signs a client certificate (1-year validity, auto-renews at 80% lifetime), and the agent starts heartbeating to the gateway. It shows up in the web UI within a few seconds.

Enrolment tokens

A registration token is 256 bits of random material; the server stores only its SHA-256 hash and shows you the plaintext exactly once, at creation. Tokens come in two shapes: one-time (consumed by the first successful enrolment) and reusable, with an optional max_uses cap (0 means unlimited) and an optional expires_at deadline.

Ownership decides what happens to the enrolled device. A token created with an owner_id auto-assigns every device it enrols to that user. Left empty, the token is ownerless — no auto-assignment, which is what you want for bulk or imaging enrolment. Users holding only the scoped CreateToken:self permission can't choose: the server forces a one-time token with a 7-day expiry, owned by the creator, and ignores any supplied owner_id.

Tokens can be renamed, disabled (and re-enabled), or deleted after creation; disabling is the kill switch for a leaked reusable token without losing its audit trail.

Re-enrolling an existing install

If you ever need to swap to a different control server or refresh credentials after a CA rotation, the binary itself accepts an enroll subcommand:

sudo power-manage-agent enroll \
  -server https://control.example.com \
  -token <fresh-token>

Or the equivalent URI form: power-manage-agent enroll 'power-manage://control.example.com?token=<token>'. Both go through the same enrolment socket the install script uses, just without re-downloading the binary.

Health checks

  • https://control.example.com/health returns ok (public; safe to point a load balancer at)
  • The indexer's :8090/health is internal to the Docker network
  • docker compose ps reports container health

If something looks wrong, docker compose logs control gateway indexer --tail=200 is the first place to look.

Next steps