|
| 1 | +# dps |
| 2 | + |
| 3 | +A readable `docker ps`. |
| 4 | + |
| 5 | +`docker ps` prints one very wide line per container, wraps it, and leaves you to |
| 6 | +find the part you care about. `dps` fits the table to the terminal it is running |
| 7 | +in, groups containers by their compose project, and scrolls when the list is |
| 8 | +longer than the window. |
| 9 | + |
| 10 | +``` |
| 11 | +NAME STATE IMAGE PORTS |
| 12 | +plane-app 12/12 up |
| 13 | + proxy ● up 3h plane-proxy:v1.3.1 8090→80 |
| 14 | + admin ● up 3h plane-admin:v1.3.1 — |
| 15 | + space ● up 3h plane-space:v1.3.1 — |
| 16 | + live ● up 3h plane-live:v1.3.1 — |
| 17 | + web ● up 3h plane-frontend:v1.3.1 — |
| 18 | + worker ● up 3h plane-backend:v1.3.1 — |
| 19 | + api ● up 3h plane-backend:v1.3.1 — |
| 20 | + plane-minio ● up 3h minio:latest — |
| 21 | + plane-db ● up 3h postgres:15.7-alpine — |
| 22 | + plane-mq ● up 3h …itmq:3.13.6-management-alpine — |
| 23 | + plane-redis ● up 3h valkey:7.2.11-alpine — |
| 24 | +12 containers · project plane-app · 12 up |
| 25 | +``` |
| 26 | + |
| 27 | +On a terminal you get the interactive view: colour, live refresh, and a cursor |
| 28 | +you can move. Through a pipe you get a plain table with no colour and no |
| 29 | +headings, so `dps | grep exited` and `dps | awk '{print $1}'` behave the way |
| 30 | +every other unix tool does. Nothing about that is a flag — it follows stdout. |
| 31 | + |
| 32 | +## Requirements |
| 33 | + |
| 34 | +**To run:** |
| 35 | + |
| 36 | +| Need | Detail | |
| 37 | +|---|---| |
| 38 | +| Docker Engine | Any daemon that answers `GET /version`. `dps` asks for API 1.44 and then clamps to whatever the daemon reports it supports, so old and new engines both work. | |
| 39 | +| A unix socket | `dps` speaks to unix sockets only. `DOCKER_HOST=tcp://…` and `ssh://…` are refused with an error, not a silent fallback. | |
| 40 | +| Read access to that socket | Your user must be in the `docker` group, or run as root. `dps` only ever reads — `GET /version` and `GET /containers/json`. It never starts, stops or removes anything. | |
| 41 | +| A UTF-8 terminal | The table uses `●`, `→` and `…`. A latin-1 terminal shows mojibake instead. | |
| 42 | + |
| 43 | +No daemon, no agent, no config file is needed. A machine that has never seen |
| 44 | +`dps` before runs it with defaults. |
| 45 | + |
| 46 | +**To build:** Go 1.25 or newer. That is the only build dependency; the one |
| 47 | +runtime dependency is Bubble Tea, for the interactive view. |
| 48 | + |
| 49 | +## Install |
| 50 | + |
| 51 | +```sh |
| 52 | +curl -fsSL https://raw.githubusercontent.com/developerabdan/dps/main/install.sh | sh |
| 53 | +``` |
| 54 | + |
| 55 | +That line is the whole install, on a laptop or on a server that has no Go. The |
| 56 | +script reads `uname`, downloads the release built for that platform, checks it |
| 57 | +against the published `checksums.txt`, and installs one file in |
| 58 | +`/usr/local/bin` — or in `~/.local/bin` when `/usr/local/bin` needs a root it |
| 59 | +cannot get. It asks for `sudo` only for that one `install` command. |
| 60 | + |
| 61 | +Read it before you run it if you prefer — it is one file of POSIX `sh`: |
| 62 | + |
| 63 | +```sh |
| 64 | +curl -fsSL https://raw.githubusercontent.com/developerabdan/dps/main/install.sh | less |
| 65 | +``` |
| 66 | + |
| 67 | +| Variable | Effect | |
| 68 | +|---|---| |
| 69 | +| `DPS_VERSION` | Install this tag instead of the latest, e.g. `v0.2.0`. | |
| 70 | +| `DPS_INSTALL_DIR` | Install into this directory instead of the default one. | |
| 71 | + |
| 72 | +```sh |
| 73 | +curl -fsSL https://raw.githubusercontent.com/developerabdan/dps/main/install.sh | |
| 74 | + DPS_INSTALL_DIR="$HOME/bin" DPS_VERSION=v0.2.0 sh |
| 75 | +``` |
| 76 | + |
| 77 | +Supported targets: `linux/amd64`, `linux/arm64`, `darwin/amd64`, |
| 78 | +`darwin/arm64`. Every release carries all four as `.tar.gz`, plus a |
| 79 | +`checksums.txt`, on the [releases page](https://github.com/developerabdan/dps/releases) |
| 80 | +— download one by hand if you would rather not pipe a script into a shell. |
| 81 | + |
| 82 | +### Other ways |
| 83 | + |
| 84 | +```sh |
| 85 | +# with Go — the binary lands in $(go env GOPATH)/bin |
| 86 | +go install github.com/developerabdan/dps/cmd/dps@latest |
| 87 | + |
| 88 | +# from source |
| 89 | +git clone https://github.com/developerabdan/dps.git |
| 90 | +cd dps |
| 91 | +go build -ldflags "-X main.version=$(git describe --tags --always)" -o bin/dps ./cmd/dps |
| 92 | +sudo install -m 0755 bin/dps /usr/local/bin/dps |
| 93 | +``` |
| 94 | + |
| 95 | +### Check the install |
| 96 | + |
| 97 | +```sh |
| 98 | +dps --version |
| 99 | +dps |
| 100 | +``` |
| 101 | + |
| 102 | +If the daemon cannot be reached, the error names the socket path it tried, so |
| 103 | +you can see which one it resolved to. |
| 104 | + |
| 105 | +### Upgrade and uninstall |
| 106 | + |
| 107 | +Run the same `curl` line again to upgrade; it overwrites the binary in place. |
| 108 | +To remove it, delete the one file: `sudo rm /usr/local/bin/dps`, and |
| 109 | +`rm -rf ~/.config/dps` if you saved a column set. |
| 110 | + |
| 111 | +## Usage |
| 112 | + |
| 113 | +``` |
| 114 | +dps [flags] |
| 115 | +``` |
| 116 | + |
| 117 | +| Flag | What it does | |
| 118 | +|---|---| |
| 119 | +| `-a` | Include stopped containers. | |
| 120 | +| `-g`, `-group` | Group rows by compose project. On by default; `-g=false` turns it off. | |
| 121 | +| `-f key=value` | Filter, passed to Docker. Repeatable: `-f status=running -f label=env=prod`. | |
| 122 | +| `-cols LIST` | Columns for this run. `-cols name,ports` replaces the set; `-cols +health,-image` adjusts the saved one. | |
| 123 | +| `-preset NAME` | Use a saved preset as the column set. | |
| 124 | +| `-set-cols LIST` | Save these columns as your default, then exit. | |
| 125 | +| `-save-preset NAME` | Save the resulting columns under a name, then exit. | |
| 126 | +| `-w N` | Redraw every N seconds (plain table, no cursor). | |
| 127 | +| `-wide` | No truncation and no dropped columns. Lines may run past the window. | |
| 128 | +| `-plain` | Force the plain table even on a terminal. | |
| 129 | +| `-json` | One JSON record per line. | |
| 130 | +| `-config` | Print the config path and contents, then exit. | |
| 131 | +| `-list-cols` | Print the column catalog, then exit. | |
| 132 | +| `-version` | Print the version, then exit. | |
| 133 | + |
| 134 | +### Examples |
| 135 | + |
| 136 | +```sh |
| 137 | +dps # grouped, interactive, live |
| 138 | +dps -a # include stopped containers |
| 139 | +dps -g=false # flat table |
| 140 | +dps -f status=exited # only what died |
| 141 | +dps -cols +health,+created # your usual columns, plus two |
| 142 | +dps -preset debug # a saved column set |
| 143 | +dps -w 5 # redraw every five seconds |
| 144 | +dps --json | jq -r 'select(.state != "running") | .name' |
| 145 | +dps | grep exited # plain, greppable, no colour |
| 146 | +``` |
| 147 | + |
| 148 | +## Interactive keys |
| 149 | + |
| 150 | +The interactive view opens when stdout is a terminal and you passed none of |
| 151 | +`-plain`, `-json` or `-w`. |
| 152 | + |
| 153 | +| Key | Action | |
| 154 | +|---|---| |
| 155 | +| `↑` `↓`, `k` `j` | Move the cursor | |
| 156 | +| `PgUp` `PgDn`, `ctrl+b` `ctrl+f`, `space` | Move a page | |
| 157 | +| `ctrl+u` `ctrl+d` | Move half a page | |
| 158 | +| `g` / `home`, `G` / `end` | First row, last row | |
| 159 | +| wheel | Scroll the window | |
| 160 | +| `a` | Toggle stopped containers | |
| 161 | +| `r` | Refresh now | |
| 162 | +| `q`, `esc`, `ctrl+c` | Quit | |
| 163 | + |
| 164 | +The list scrolls inside the window — it never asks for a taller terminal. While |
| 165 | +the list is longer than the window the status line says which row you are on, |
| 166 | +for example `row 12/37`. A cursor on the first row of a project keeps that |
| 167 | +project's heading on screen. |
| 168 | + |
| 169 | +The view refreshes every two seconds. |
| 170 | + |
| 171 | +## Columns |
| 172 | + |
| 173 | +`dps --list-cols` prints this catalog: |
| 174 | + |
| 175 | +| Key | Header | Shows | |
| 176 | +|---|---|---| |
| 177 | +| `name` | NAME | Container name, with the project prefix removed | |
| 178 | +| `state` | STATE | Running state and uptime, e.g. `● up 3h` | |
| 179 | +| `image` | IMAGE | Image, with the registry stripped | |
| 180 | +| `ports` | PORTS | Published ports, compacted, e.g. `8090→80` | |
| 181 | +| `health` | HEALTH | Healthcheck result | |
| 182 | +| `created` | CREATED | Age | |
| 183 | +| `project` | PROJECT | Compose project | |
| 184 | +| `service` | SERVICE | Compose service | |
| 185 | +| `ip` | IP | Container IP | |
| 186 | +| `size` | SIZE | Writable layer size (costs an extra query) | |
| 187 | +| `id` | ID | Short container ID | |
| 188 | +| `cmd` | COMMAND | Entrypoint command | |
| 189 | + |
| 190 | +The default set is `name,state,image,ports`. |
| 191 | + |
| 192 | +When the terminal is too narrow for every column, `dps` drops the lowest |
| 193 | +priority ones — but it says so on stderr rather than hiding data quietly: |
| 194 | + |
| 195 | +``` |
| 196 | +dps: 2 columns hidden (created, cmd) — widen the window or use --wide |
| 197 | +``` |
| 198 | + |
| 199 | +Because that notice goes to stderr, it stays out of `dps | awk` while still |
| 200 | +being said out loud. |
| 201 | + |
| 202 | +## Configuration |
| 203 | + |
| 204 | +Optional. The file is plain JSON, and `--set-cols` and `--save-preset` only |
| 205 | +write what you could have typed yourself. |
| 206 | + |
| 207 | +Location: `~/.config/dps/config.json`, or `$XDG_CONFIG_HOME/dps/config.json` |
| 208 | +when that variable is set. A missing file is not an error. |
| 209 | + |
| 210 | +```json |
| 211 | +{ |
| 212 | + "cols": ["name", "state", "image", "ports"], |
| 213 | + "group_by_project": true, |
| 214 | + "presets": { |
| 215 | + "ports": ["name", "ports", "ip", "state"], |
| 216 | + "debug": ["name", "state", "health", "created"], |
| 217 | + "disk": ["name", "image", "size"] |
| 218 | + } |
| 219 | +} |
| 220 | +``` |
| 221 | + |
| 222 | +| Key | Effect | |
| 223 | +|---|---| |
| 224 | +| `cols` | Your default column set. | |
| 225 | +| `group_by_project` | Default for `-g`. A `-g` typed on the command line wins, in both directions. | |
| 226 | +| `presets` | Named column sets for `--preset`. | |
| 227 | + |
| 228 | +`--set-cols` and `--save-preset` validate against the catalog before writing, so |
| 229 | +a typo cannot save a file that later refuses to load. Writes go to a temporary |
| 230 | +file and are renamed, so an interrupted save cannot leave a half-written config |
| 231 | +behind. |
| 232 | + |
| 233 | +A `sort` key is written into new config files but nothing reads it yet. |
| 234 | + |
| 235 | +## How dps finds Docker |
| 236 | + |
| 237 | +The same order the `docker` CLI uses: |
| 238 | + |
| 239 | +1. `DOCKER_HOST`, when it is a `unix://` path. |
| 240 | +2. The endpoint of the active Docker context, from `~/.docker/config.json` and |
| 241 | + `~/.docker/contexts/`. |
| 242 | +3. `/var/run/docker.sock`. |
| 243 | + |
| 244 | +Step 2 is why `dps` works on Docker Desktop for macOS, where the socket is at |
| 245 | +`~/.docker/run/docker.sock` and `/var/run/docker.sock` does not exist. |
| 246 | + |
| 247 | +To read a socket at a non-standard path: |
| 248 | + |
| 249 | +```sh |
| 250 | +DOCKER_HOST=unix:///run/user/1000/docker.sock dps |
| 251 | +``` |
| 252 | + |
| 253 | +## Output contract |
| 254 | + |
| 255 | +`--json` emits newline-delimited JSON, one container per line. The field names |
| 256 | +are snake_case and stable — scripts depend on them: |
| 257 | + |
| 258 | +```json |
| 259 | +{"id":"0dec549e57b9…","name":"plane-app-proxy-1","image":"makeplane/plane-proxy:v1.3.1","state":"running","status":"Up 3 hours","ports":[{"ip":"127.0.0.1","public":8090,"private":80,"type":"tcp"}],"created":1782108483,"project":"plane-app","service":"proxy","ip":"172.24.0.7","command":"caddy run --config /etc/caddy/Caddyfile","restarts":0} |
| 260 | +``` |
| 261 | + |
| 262 | +JSON carries the raw values, not the shortened ones the table prints: the full |
| 263 | +ID, the full container name, and the image with its registry. Shortening is a |
| 264 | +display decision, and a script should make its own. |
| 265 | + |
| 266 | +Through a pipe, group headings are dropped. A script cannot tell a heading from |
| 267 | +a container row, so `dps -g | awk` gets the flat table instead of something |
| 268 | +quietly wrong. |
| 269 | + |
| 270 | +## Development |
| 271 | + |
| 272 | +```sh |
| 273 | +go build ./... |
| 274 | +go test ./... |
| 275 | +gofmt -l . |
| 276 | +``` |
| 277 | + |
| 278 | +## Limitations |
| 279 | + |
| 280 | +- Unix sockets only. No `tcp://`, no `ssh://`. To read a remote host, ssh in and |
| 281 | + run `dps` there. |
| 282 | +- Read-only. It shows containers; it does not stop, start or remove them. |
| 283 | +- The `sort`, `strip_registry` and `strip_project_prefix` config keys are not |
| 284 | + implemented yet. |
| 285 | + |
| 286 | +## License |
| 287 | + |
| 288 | +MIT. See [LICENSE](LICENSE). |
0 commit comments