Skip to content

Repository files navigation

harbr

A small container platform you drive over SSH. ssh harbr@your-box drops you into a TUI where you add projects from git, deploy them, watch logs, edit environment variables, assign domains, and roll back.

There is no web UI and no agent to install — the SSH server is the interface, and your existing SSH key is the login.

╭──────────────────────────────────────────────────────────────────────────────────────────╮
│ [ 1 DASHBOARD ]  [ 2 PROJECTS ]  [ 3 RESOURCES ]  [ 4 SETTINGS ]   [ ana@laptop ]        │
│                                                                                          │
│  R E S O U R C E   O V E R V I E W          [ CONTAINERS: 6 ]  [ CORES: 20 ]             │
│ ╭ HOST ──────────────────────────────────────────────────────────────────────────────╮   │
│ │CPU    ███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░     6%  20 cores                            │   │
│ │MEM    ██████████████░░░░░░░░░░░░░░░░░░    44%  13.6 GB / 31.0 GB                   │   │
│ │DISK   ██████████████████████████░░░░░░    87%  202 GB / 233 GB                     │   │
│ │CORES  ▂▁▄▂▁▁▃▁▂▁▁▅▂▁▁▂▁▃▁▂  20 cores                                               │   │
│ ╰────────────────────────────────────────────────────────────────────────────────────╯   │
│ ╭ NETWORK   ↓ 33.0 KB/s   ↑ 12.5 KB/s ───────────────────────────────────────────────╮   │
│ │       ▄       ▄             █     ▄                                                │   │
│ │ █▁ █▁▁█  █ ▁ ▁█▁ ▁ ▁▁       █ ▁  ▁█▁▁▁▁  ▁█ █▁▁▁█  ▁                               │   │
│ ╰────────────────────────────────────────────────────────────────────────────────────╯   │
╰──────────────────────────────────────────────────────────────────────────────────────────╯
  1-4 section   ↑↓ move   ↵ open   / filter   ? help   q quit

What it does

Four sections, reachable at any time with the number keys shown in the nav.

Dashboard — host CPU, memory, disk and network throughput as live charts, alongside project health and recent deployments.

Projects — every project, filterable with /. Open one for tabs of Services, Logs, Deploys, Env, Domains, Volumes and Access — plus Definition when the project defines its own containers.

Resources — host meters including a per-core strip, a network history chart, and a breakdown of CPU, memory and IO by project or by container.

Settings — who may sign in, who is signed in right now, the login history, and the instance configuration.

Windows — one connection, several pages, driven like tmux. The prefix is a backtick:

key
` c open a window
` n p next, previous
` l the window before this one
` 18 jump straight to one
` x close it
` d detach, leaving everything running
` ` send a literal backtick

Watch a build in one window, read runtime logs in another, keep Settings open in a third. Background windows keep streaming, so a build log is complete when you come back to it rather than starting from wherever you happened to look.

A backtick is a character people type, so ` ` sends one through, and while harbr has a text field open the prefix stands down entirely — nobody expects a domain or an env value to swallow one. Pasted text passes through whole, brackets and all, so pasting a command with a backtick in it into a shell is not quietly corrupted. ctrl-] works as a prefix too, for when a backtick is precious.

Shells are windows too. Several can run at once, and one that is not in front keeps working — output that arrives while you are elsewhere is replayed when you come back to it.

Losing the connection is not losing the session. A dropped link parks its windows: the shells keep running, the log streams keep filling, and the next connection with the same key lands back where it left off, skipping the gate. ` d does the same thing deliberately. q is the decision to leave, and that closes them.

Underneath that:

  • Projects from anywhere — public git over https, private git with a per-project deploy key, or a directory that already exists on the host.
  • Compose without the compose CLI — harbr parses docker-compose.yml itself and drives the Docker Engine API directly. Builds go through BuildKit, so --platform=$BUILDPLATFORM, RUN --mount=… and heredocs all work.
  • Health-aware orderingdepends_on: { condition: service_healthy } is honoured, and a deployment fails loudly if a service never becomes healthy.
  • Logs — streaming container logs per service, plus the full build log of every deployment, kept in the database.
  • Environment variables — edited in the TUI, merged over the compose file's environment:, with secrets masked in the list.
  • Domains — assign a hostname to a service and harbr writes the Traefik labels, joins the container to the edge network, and brings up Traefik itself on first use. With HARBR_ACME_EMAIL set you get Let's Encrypt certificates.
  • Two kinds of project — point one at a compose file in the repository, or define the containers in harbr itself: an image or a Dockerfile per service, with ports, command, memory and CPU limits.
  • Volume mapping — mount a host path or a managed volume into any service from the Volumes tab. These sit on top of whatever the compose file declares, and win when they target the same path.
  • Resource limits — compose deploy.resources.limits (cpus, memory) and per-service limits set in harbr both reach the daemon.
  • Rollback — every deployment records the exact image id it ran, so rolling back re-runs the old images without rebuilding. The last three images per service are kept for exactly this; older ones are pruned after each successful deploy, and an image still backing a container is never removed.

Who gets in

Connecting lands you on a login gate that names the key you authenticated with.

  • Known, approved key → press enter and you are in.
  • Unknown key → the gate offers to file an access request. An admin sees it under Settings › Access, presses a, gives the person a name and a level, and they are in without reconnecting.
  • The very first key to connect claims the instance as admin. Set HARBR_TOFU=0 to disable that and pre-authorise keys with harbr key add.

Permission levels

Levels are additive — each contains the one before it.

read write modify admin
view projects, logs, metrics
deploy, roll back, stop
edit env, domains, volumes, definition
create and delete projects
approve keys, grant access, instance config
open a shell in a container

The split between write and modify is the point: an on-call operator can restart a service without also being able to rewrite its secrets.

Shell access is a separate flag

A shell is arbitrary code inside the container. It can read every secret, rewrite every file and reach every service on the project network — which makes it more dangerous than modify, and yet an operator with only write is exactly who needs it at 3am. So it is not a rung on the ladder: it is an independent flag, set per key and grantable per project, the way Kubernetes treats pods/exec as its own verb.

  • No level implies it. Only admin has it by default.
  • A grant can add it for one project without raising anything else.
  • It still requires read on the project — a key that cannot see a project cannot shell into it.
  • Granting it to a read key gives that key write access to the container's data. That is the point of the flag; be deliberate.

Press S on a project's Services tab. The shell opens in a window of its own and takes the screen while it is in front: harbr hands the SSH channel to the container raw, so everything in there behaves as it would over plain ssh. ` l leaves it running and goes back to the page you came from; ` x closes it. Every session is recorded — it is the one action where harbr can see that it happened but not what was done.

Withdrawing shell access closes the shells it granted, rather than only refusing the next one. The same applies to a project a key can no longer read: any window holding it is closed on the next refresh, parked or not.

Per-project grants

A key has a baseline level, and a grant on one project can raise it — never lower it. A baseline of none means a key sees only the projects it was granted, which is how you hand a contractor one project and nothing else.

ana      baseline read      → sees everything, changes nothing
ana @ billing = modify      → full control of billing only

sam      baseline none      → sees nothing by default
sam @ billing = write       → can deploy billing, cannot see marketing

Revoking or un-approving a key beats every grant it holds.

Changing permissions

From the TUI — everything lives in Settings › Access, admin-only. Keys are listed with their project grants nested beneath them, so one person's whole access reads as a single block:

   NAME / PROJECT      LEVEL    SHELL  STATUS   FINGERPRINT           ONLINE
 ● admin-bootstrap     admin    yes    active   SHA256:lnpuW0s5RP…    ● now
 ● sam                 read     —      active   SHA256:RWPNnMcVjR…
     └ billing           modify   yes    on this project
Key On a key row On a grant row
a change level and shell access change that grant
g grant this key a project
x revoke the key remove just that grant

The forms open on the current values, so renaming someone cannot silently reset their access. A project's own Access tab shows the same grants in context, along with your own level on it.

From the CLI — no public key file needed; keys resolve by name, full fingerprint, or fingerprint prefix:

harbr key list                                  # baselines, shell flags, and grants
harbr key set sam --level write --shell         # change a baseline
harbr key set sam --level read --no-shell       # withdraw
harbr key grant sam billing --level modify      # raise on one project
harbr key revoke sam billing                    # drop that grant
harbr key remove SHA256:…                       # delete the key

harbr key list shows grants beneath each key:

ana                  admin   shell  active   SHA256:mj6Q4K5bpaP5jl9…
sam                  write   shell  active   SHA256:dOcry8Hucs3xXP8…
  └ billing            modify  shell

You cannot lock yourself out. Demoting, revoking or deleting the last active admin is refused, by both the TUI and the CLI — nothing except an admin can grant admin, so reaching zero would be unrecoverable:

$ harbr key set ana --level read
Error: ana is the only admin — promote someone else first

Settings › Activity shows who is connected now, every login attempt, and an audit trail of what was changed and by whom.

Running it

docker compose up -d --build

Then connect:

ssh -p 2222 admin@your-box

See Who gets in for what happens on that first connection.

The data directory has to be mounted at the same path on both sides

harbr resolves compose bind mounts to absolute paths and hands them to the Docker daemon, which interprets them on the host. If harbr's data directory were mounted at a different path inside its own container, every relative bind mount in a user's compose file would point at a directory that does not exist. The bundled docker-compose.yml therefore mounts /var/lib/harbr at /var/lib/harbr. Keep it that way, or change both sides together.

Configuration

Variable Default Meaning
HARBR_DATA_DIR ~/.harbr Database, host key, deploy keys, checkouts
HARBR_SSH_ADDR 0.0.0.0 Listen address
HARBR_SSH_PORT 2222 Listen port
HARBR_EDGE_NETWORK harbr-edge Network shared with the proxy
HARBR_BASE_DOMAIN Wildcard base used to suggest hostnames
HARBR_ACME_EMAIL Enables Let's Encrypt; without it, HTTP only
HARBR_HTTP_PORT 80 Host port for the proxy's HTTP entrypoint
HARBR_HTTPS_PORT 443 Host port for the proxy's HTTPS entrypoint
HARBR_TOFU 1 Enrol the first key that connects as admin
HARBR_DETACH_TTL_HOURS 12 How long a dropped session's windows are kept. 0 disables reattaching
HARBR_TRAEFIK_IMAGE traefik:v3.6 Proxy image
HARBR_LOG harbr=info tracing filter

CLI

harbr serve                        # run the SSH server (default)
harbr doctor                       # check docker, git, proxy, key count
harbr key add laptop ~/.ssh/id_ed25519.pub --level admin
harbr key add oncall ~/.ssh/oncall.pub --level write --shell
harbr key list
harbr key remove SHA256:…

Keys in the TUI

Where Key Action
Anywhere 1 2 3 4 Dashboard, Projects, Resources, Settings
Anywhere r ? q refresh, help, disconnect
Projects / filter by name, slug or repo
Projects n d s x R new, deploy, stop, delete, roll back
Project tab [ ] Services, Definition, Logs, Deploys, Env, Domains, Volumes, Access
Project d b esc deploy, change tracked branch, back
Services S open a shell in the selected container
Deploys R roll back to the selected deployment
Env / Domains a e x v add, edit, remove, reveal secrets
Logs n p f g G c switch service, follow, jump, clear
Resources tab by project / by container
Volumes a x mount, unmount
Access a x grant a level on this project, revoke
Settings tab a x section, approve or change a key, revoke

How a deployment runs

  1. Fetch the source — clone or fetch, then check out the ref. A rollback pins the target deployment's commit.
  2. Build the service set: parse the compose file, interpolating ${VAR} from the repo's .env and the project's environment variables — or, for a containers project, compile the services defined in harbr into the same in-memory model.
  3. Compile a plan: topologically sorted services, resolved images, port bindings, binds (compose volumes plus harbr mounts), resource limits, healthchecks, and harbr/Traefik labels.
  4. Build or pull each image. A rollback skips this and reuses recorded image ids.
  5. Ensure the network, volumes, and — if any domain is assigned — the proxy.
  6. Replace containers in dependency order, waiting for health at each step, and remove containers left over from services that no longer exist.
  7. Prune built images beyond the last three per service.

Every line of that is written to the deployment log and streamed to any connected session.

How it fits together

        ssh client
            │  public key auth, one pty per session
    ┌───────▼────────┐
    │   russh server │  src/ssh
    └───────┬────────┘
            │  key bytes ─► decoder ─► ratatui frames ─► channel
    ┌───────▼────────┐        ┌──────────────┐
    │      TUI       │◄───────│  event bus   │  deploy logs, status, sessions
    │   src/tui      │        └──────▲───────┘
    └───────┬────────┘               │
            │ actions                │
    ┌───────▼────────────────────────┴───────┐
    │                App                     │  src/app
    │  job spawning · sessions · samplers    │
    └──┬──────────┬──────────┬───────────────┘
       │          │          │
  ┌────▼───┐ ┌────▼────┐ ┌───▼─────┐
  │ SQLite │ │ metrics │ │ engine  │  src/engine
  │ src/db │ │/proc +  │ │ bollard │
  └────────┘ │ stats   │ └───┬─────┘
             └─────────┘     │
                     ┌───────▼────────┐
                     │ Docker daemon  │
                     └────────────────┘

A deployment is compiled before anything is created: src/compose parses the YAML, interpolates variables, sorts services by depends_on, and produces a DeployPlan of fully-formed Docker API requests. That plan is a pure function of its inputs, which is why most of the test suite needs no daemon at all.

Security

What is enforced:

  • Public keys only. The server advertises no other authentication method, so a client with no key is refused rather than prompted for a password.
  • Unknown keys reach the gate and nothing else. They can file one request and read their own fingerprint. The queue is capped so it cannot be used to fill the database.
  • Hostnames are validated before they become Traefik router rules. The rule is built as Host(`{host}`), so a domain containing a backtick could otherwise rewrite the matcher and steal another project's traffic.
  • Compose paths are validated at creation and at deploy time; .. and absolute paths are rejected so a project cannot read outside its checkout.
  • Git URLs may not begin with -, which would otherwise be parsed by git as an option rather than a repository.
  • Environment keys are restricted to the shape Docker expects.
  • Instances are isolated. Containers, networks and volumes are labelled with an instance id, so two harbr installs on one daemon cannot list, restart or delete each other's work.
  • Deleting a project keeps its volumes unless you explicitly opt in, and requires typing the project slug.
  • Permission levels are enforced on every action, not just hidden in the UI: a read key is refused a deploy, a write key is refused a config change, and a project a key has no level on is not listed at all.
  • Shell access is its own capability, off by default at every level except admin, and every session is written to the audit trail.
  • Editing a key preselects its current level, so renaming someone cannot silently reset their access.

What is not:

  • Deploy keys and environment values are stored unencrypted (mode 0600) under the data directory. Anyone with root on the host, or with the data volume, has them.
  • Levels gate actions, not data. A read key still sees every environment value it has read access to, including ones marked secret once revealed.
  • harbr needs the Docker socket, which is equivalent to root on the host. Treat an admin key as a root credential.

Testing

cargo test --lib --test plan --test access   # no daemon needed
cargo test --test docker                     # talks to a real Docker daemon
./scripts/e2e.sh                             # drives the TUI over real SSH

The three suites cover different things and none of them mock Docker:

Suite What it proves
--lib validators, the compose parser, duration and port grammar, the key decoder
plan dependency ordering, env precedence, volume and port translation, health checks, Traefik labels, instance labels
access key lifecycle, permission resolution, per-project grants, shell capability, login history, restart reconciliation, log retention
docker real deploys, health gating, redeploys, rollback by recorded image and by commit, image retention, containers-mode projects, volume mounts, interactive shells, pruning removed services, instance isolation
scripts/e2e.sh authentication, the login gate, navigation, project creation, live logs, input validation, the request-and-approve flow, permission enforcement at each level, and a real shell driven over the SSH channel

scripts/e2e.sh needs tmux; it starts a real server, connects a real ssh client to it, and asserts on what is actually rendered in the terminal.

Limits worth knowing

Deliberate omissions, not oversights:

  • Redeploying replaces containers rather than draining them, so a service is briefly down during a deploy. There is no blue/green or rolling update.
  • Compose support is a broad subset, not the full spec: no profiles, extends, configs, secrets, multi-file overrides, or swarm keys. Port ranges are rejected with an explanation rather than silently mishandled.
  • No GitHub integration beyond cloning. There is no GitHub App, no OAuth and no webhook receiver, so no deploy-on-push — harbr has no HTTP listener at all.
  • Runtime logs are not aggregated or searchable; you get Docker's buffer.
  • Resource history lives in memory, so charts reset when harbr restarts and cover roughly the last ten minutes.
  • Host metrics come from /proc, which means Linux only.
  • The proxy is assumed to be harbr's — it creates and owns one Traefik container per host.
  • One instance owns one host. There is no clustering and no scheduler.
  • No backup or restore. The data directory (database, host key, deploy keys) is the whole of harbr's state — copy it yourself.
  • Deploy keys and environment values are stored unencrypted (mode 0600) under the data directory.
  • TLS has never been exercised. The ACME code paths are written but have never issued a certificate, because that needs a real domain and port 443. Treat Let's Encrypt support as unverified until you have run it.
  • Concurrent sessions are capped at 64, but connections are not otherwise rate limited beyond russh's authentication delay.
  • Windows do not survive restarting harbr. Parked windows live in the process, so an upgrade or a reboot closes them; only a dropped connection is survivable. Eight windows per session, and a background shell keeps its last 64KB of output, not its whole scrollback.
  • A background shell is not rendered beside anything else. Each one is full screen when it is in front, because putting a live shell next to harbr's own views means hosting a terminal emulator — wide characters, alt-screen, mouse, scrollback — which is a project of its own rather than a flag.
  • Reattaching is one workspace per key: if the same key is connected twice, each connection gets its own windows and the last one to drop is the one kept.

About

ssh app to manage your server and deployments

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages