Skip to content

Commit 493af20

Browse files
feat: readable docker ps with one-line installer
Single static Go binary. Fits the table to the terminal, groups containers by compose project, and degrades to a plain greppable table through a pipe. Ships install.sh (POSIX sh, checksum-verified, no Go needed on the target) and a tag-triggered workflow that builds linux and darwin on amd64 and arm64.
0 parents  commit 493af20

24 files changed

Lines changed: 3052 additions & 0 deletions

‎.github/workflows/release.yml‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: release
2+
3+
# A tag push builds every target, publishes the tarballs and their checksums,
4+
# and that is what install.sh downloads. Nothing is published by hand.
5+
on:
6+
push:
7+
tags:
8+
- 'v*'
9+
10+
permissions:
11+
contents: write
12+
13+
jobs:
14+
release:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v7
18+
with:
19+
fetch-depth: 0
20+
21+
- uses: actions/setup-go@v7
22+
with:
23+
go-version: '1.25'
24+
25+
- name: Test
26+
run: |
27+
gofmt -l . | tee /tmp/unformatted
28+
test ! -s /tmp/unformatted
29+
go vet ./...
30+
go test ./...
31+
32+
- name: Build every target
33+
env:
34+
VERSION: ${{ github.ref_name }}
35+
run: |
36+
set -eu
37+
v="${VERSION#v}"
38+
mkdir -p dist
39+
for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64; do
40+
os="${target%/*}"
41+
arch="${target#*/}"
42+
stage="dist/dps_${v}_${os}_${arch}"
43+
mkdir -p "$stage"
44+
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build \
45+
-trimpath \
46+
-ldflags "-s -w -X main.version=${VERSION}" \
47+
-o "$stage/dps" ./cmd/dps
48+
cp LICENSE README.md "$stage/"
49+
tar -czf "${stage}.tar.gz" -C "$stage" .
50+
rm -rf "$stage"
51+
done
52+
cd dist && sha256sum ./*.tar.gz | sed 's| \./| |' > checksums.txt
53+
cat checksums.txt
54+
55+
- name: Publish the release
56+
env:
57+
GH_TOKEN: ${{ github.token }}
58+
run: |
59+
gh release create "${{ github.ref_name }}" \
60+
dist/*.tar.gz dist/checksums.txt \
61+
--title "${{ github.ref_name }}" \
62+
--generate-notes

‎.gitignore‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Build output. The binary is 11 MB and is rebuilt by `go build`.
2+
/bin/
3+
/dist/
4+
5+
# Config writes go to a temporary file first and are renamed; a crash can
6+
# leave one behind.
7+
*.tmp
8+
9+
# Local tooling state, not part of the project.
10+
.memsearch/
11+
12+
.DS_Store
13+
CLAUDE.md
14+
/docs

‎LICENSE‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 developerabdan
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

‎README.md‎

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
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

Comments
 (0)