# aptmatic ๐ค๐ฆ
> Because SSHing into 40 servers one by one to run `apt-get upgrade` is a cry for help.
[](https://github.com/growse/aptmatic/actions/workflows/ci.yml)
A snappy terminal UI for wrangling `apt` across a fleet of Debian/Ubuntu hosts โ written in Rust, because I don't know how to code in OCaml.
```
โญโ aptmatic โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Hosts โDetail โ
โ โธ webservers โweb1.example.com โ
โ โ web1 [2] โuser: ubuntu port: 22 sudo: true โ
โ โ web2 โ โ
โ โธ databases โStatus: 2 upgrade(s) available (1 security) โ
โ โ ผ db1 โ โ
โ โ db2 โKernel โ
โ โRunning: 6.1.0-28-amd64 โ
โ โLatest: linux-image-6.1.0-32-amd64 โ reboot โ
โ โ โ
โ โUpgradable โ
โ โ[sec] curl (7.88.1-10 โ 7.88.1-10+deb12u8) โ
โ โ libcurl4 (7.88.1-10 โ 7.88.1-10+deb12u8) โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
r:update+refresh R:refresh all u:upgrade U:upgrade all
f:full-upgrade F:full-upgrade all s:sec-upgrade S:sec-upgrade all
a:autoremove A:autoremove all p:purge-rc c:config files b:reboot
t:task output z:zoom /:search q:quit
```
## Features
- ๐ฅ๏ธ **Multi-host dashboard** โ see every host's status at a glance
- ๐ฅ **Groups** โ organise hosts and trigger actions on a whole group at once
- ๐ **SSH native** โ talks directly to each host over SSH, no agents or daemons required
- ๐ **Live task output** โ watch `apt-get upgrade` scroll by in real time
- ๐ง **Kernel tracking** โ know which hosts are silently waiting for a reboot
- ๐ฆ **Held/kept-back packages** โ spot the stragglers and why they're stuck
- ๐ก๏ธ **Security-update badge** โ upgradable packages from a security suite are called out separately, with a key to upgrade just those
- ๐ **Sidebar search** โ `/` to filter hosts/groups by name in a big fleet
- ๐พ **Cached last-known state** โ the dashboard isn't blank on startup while it reconnects
- ๐ฆ **Bounded connection concurrency** โ "all hosts" actions queue instead of opening a connection per host at once
- ๐งน **RC package purging** โ one key to purge all those half-removed ghosts
- ๐ **Pending config files** โ upgrades never stop to ask about a changed conffile; the new version is counted per host and reviewed later, with a diff, on your schedule
- โฌ๏ธ **Full-upgrade & autoremove** โ `apt-get full-upgrade` and `apt-get autoremove --purge`, on selected hosts or the whole fleet
- ๐ **Confirmed reboot** โ type the hostname to confirm before a host goes down
- ๐ฑ๏ธ **Draggable divider** โ because you deserve to customise your own TUI
- ๐ฆ **Written in Rust** โ guaranteed\* to have no bugs
<sub>\* guarantee void where prohibited by logic</sub>
## Installation
```bash
cargo install aptmatic
```
Or build from source:
```bash
cargo build --release
# binary at ./target/release/aptmatic
```
## Configuration
aptmatic looks for its config at `~/.config/aptmatic.toml` by default. Pass `-c /path/to/config.toml` to override.
```toml
[defaults]
user = "ubuntu"
port = 22
use_sudo = true
[[groups]]
name = "webservers"
[[groups.hosts]]
hostname = "web1.example.com"
[[groups.hosts]]
hostname = "web2.example.com"
user = "admin" # override per-host
[[groups]]
name = "databases"
[[groups.hosts]]
hostname = "db1.example.com"
```
## Keybindings
| `โ` / `k` | Move up |
| `โ` / `j` | Move down |
| `/` | Search/filter the sidebar by hostname or group name |
| `r` | `apt-get update` + refresh on selected |
| `R` | `apt-get update` + refresh on **all** hosts |
| `u` | `apt-get upgrade` on selected |
| `U` | `apt-get upgrade` on **all** hosts |
| `f` | `apt-get full-upgrade` on selected |
| `F` | `apt-get full-upgrade` on **all** hosts |
| `s` | Upgrade **security-only** packages on selected |
| `S` | Upgrade **security-only** packages on **all** hosts |
| `a` | `apt-get autoremove --purge` on selected |
| `A` | `apt-get autoremove --purge` on **all** hosts |
| `p` | Purge RC packages on selected |
| `c` | Review pending config files on the selected host |
| `b` | Reboot the selected host (type the hostname to confirm) |
| `t` / `Enter` | View live task output |
| `z` | Zoom โ hide sidebar for clean copy/paste |
| `q` / `Esc` | Quit |
The sidebar divider is also mouse-draggable if you're feeling fancy.
### Pending config files
Upgrades run with `--force-confdef --force-confold`, so dpkg never stops to ask what to do about a config file you've edited โ it keeps yours and drops the maintainer's version next to it as `.dpkg-dist`. Hosts carrying unresolved files show a `[n cfg]` badge in the sidebar; `c` opens a review pane listing them with a diff against the live file. Mark each file with a decision, then execute the whole batch in one go:
| `โ` / `โ` | Select a file |
| `PgUp` / `PgDn` | Scroll the diff |
| `d` | Mark: discard the new version, keeping your current config |
| `a` | Mark: install the new version, backing your current one up to `.dpkg-old` |
| `u` / `Space` | Unmark |
| `Enter` | Execute all marked decisions (asks for confirmation first) |
| `Esc` | Close without touching anything |
Marking is free โ nothing happens on the host until you confirm with `Enter` then `y`. Unmarked files are left pending for a later review, and a failure on one file doesn't stop the rest of the batch. Applying a config file does not restart anything โ restart the affected service yourself once you're happy with it.
While searching, type to filter, `โ`/`โ` to jump between matches, `Enter`/`Esc` to stop editing (the filter stays applied โ clear it by backspacing to empty).
Actions on a whole group or "all hosts" are queued through a small connection pool (8 at a time) rather than opening an SSH connection per host simultaneously.
## Development
```bash
just build # build
just fmt # format
just lint # fmt check + clippy
```
## Why?
Managing a modest fleet of Linux boxes with `apt` should not require an orchestration platform, a PhD in Ansible, or accepting a cookie banner. aptmatic is a single binary, a TOML file, and a spare SSH key away from a good time.