# dry
[](https://github.com/moncho/dryblob/master/LICENSE)


[](https://goreportcard.com/report/github.com/moncho/dry)
[](https://godoc.org/github.com/moncho/dry)
[]()
[](https://github.com/moncho/dry/releases/latest)
[](https://snapcraft.io/dry)
**Dry** is a terminal application for **Docker** and **Docker Compose**.
It lets you browse containers, images, networks, and volumes, and manage Compose projects — bring them up, take them down, see what has drifted from the compose file — without leaving the terminal. It can be used with both local or remote **Docker** daemons.
Besides showing information, it can be used to manage Docker. Most of the commands that the official **Docker CLI** provides, are available in **dry** with the same behaviour. A list of available commands and their keybindings can be found in **dry**'s help screen or in this README.
It can also be used as a monitoring tool for **Docker** containers, and, when the daemon is running a **Swarm** cluster, to manage Nodes, Services, and Stacks — see [Docker Swarm](#docker-swarm) below.
**Dry** is installed as a single binary and does not require external libraries.
The demo below shows a **dry** session.
[](https://asciinema.org/a/35825?autoplay=1&speed=1.5)
## **dry** keybinds
### Global
Keybinding | Description
---------------------|---------------------------------------
% | filter list
F1 | sort list
F5 | refresh list
F7 | toggle showing Docker daemon information
F8 | show docker disk usage
F9 | show last 10 docker events
F10 | show docker info
1 | show container list
2 | show image list
3 | show network list
4 | show volumes list
5 | show node list (on Swarm mode)
6 | show service list (on Swarm mode)
7 | show stacks list (on Swarm mode)
8 | show compose projects list
ArrowUp or k | move the cursor one line up
ArrowDown or j | move the cursor one line down
g | move the cursor to the top
G | move the cursor to the bottom
: | open command palette
Space | open Quick Peek for the current selection
Ctrl+0 | cycle color theme (dark/light)
q | quit dry
### Workspace mode (`--workspace`)
`dry --workspace` enables the Phase 1 workspace shell. This keeps the current list view visible together with a passive context pane and a bottom activity pane.
- `Tab` and `Shift+Tab` switch focus between `Navigator`, `Context`, and `Activity` in the full workspace layout.
- When `Context` is focused, you can scroll it with the usual navigation keys.
- `p` / `P` pins or unpins the current preview.
- `:` opens the command palette with global and context-aware actions.
- `Space` opens `Quick Peek`, a temporary side panel with recent logs or inspect/details for the current selection.
- `f` toggles follow mode in the activity pane.
- Existing primary actions stay on their original keys. For example, `Enter` still opens the container command menu and still inspects resources in views where `Enter` already meant inspect/drill-down.
- Embedded activity logs start from a recent tail instead of replaying the full log history.
- On narrow or short terminals, workspace mode falls back to a compact single-pane layout and lets `Tab` switch between navigator and activity.
#### Container commands
Keybinding | Description
---------------------|---------------------------------------
Enter | show container command menu (includes Attach for running containers)
F2 | toggle on/off showing stopped containers
i | inspect
l | container logs
e | remove
s | stats
x | exec a command in the selected container (default `/bin/sh`)
Ctrl+e | remove all stopped containers
Ctrl+k | kill
Ctrl+r | start/restart
Ctrl+t | stop
#### Image commands
Keybinding | Description
---------------------|---------------------------------------
i | history
Ctrl+d | remove dangling images
Ctrl+e | remove image
Ctrl+f | remove image (force)
Ctrl+u | remove unused images
Enter | inspect
#### Network commands
Keybinding | Description
---------------------|---------------------------------------
Ctrl+e | remove network
Enter | inspect
#### Volume commands
Keybinding | Description
---------------------|---------------------------------------
Ctrl+a | remove all volumes
Ctrl+e | remove volume
Ctrl+f | remove volume (force)
Ctrl+u | remove unused volumes
Enter | inspect
#### Service commands
Keybinding | Description
---------------------|---------------------------------------
i | inspect service
l | service logs
Ctrl+r | remove service
Ctrl+s | scale service
Ctrl+u | update service
Enter | show service tasks
#### Compose Projects commands
Keybinding | Description
---------------------|---------------------------------------
Enter | show project services
u | bring the selected service up, with no prompt; on a project's own row it asks first
d | take the project down, from its own row and behind a prompt: `compose down` removes the project's networks too, so there is no per-service form
c | show the project's rendered compose configuration
l | logs for the selected service, or the project on its header
Ctrl+t | stop the selected service, or the project on its header
Ctrl+r | restart the selected service, or the project on its header
Ctrl+e | remove the selected service's containers, or the project's
`Ctrl+t`, `Ctrl+r`, `Ctrl+e` and `d` all name their target in a confirmation
prompt, "Stop service webapp/api?" or "Take project webapp down?", so a
cursor that has moved is visible before anything runs. `u` on a service row
is the one action that runs unasked, since it creates rather than destroys;
on a `drift` row it does recreate the containers, which is the point of the
label.
#### Compose Services commands
Keybinding | Description
---------------------|---------------------------------------
Enter | inspect service
Esc | back to projects
u | bring the selected service up
c | show the project's rendered compose configuration
l | service logs
Ctrl+s | start service containers
Ctrl+t | stop service containers
Ctrl+r | restart service containers
Ctrl+e | remove service containers
#### Moving around buffers
Keybinding | Description
---------------------|---------------------------------------
ArrowUp or k | move the cursor one line up
ArrowDown or j | move the cursor one line down
g | move the cursor to the beginning of the buffer
G | move the cursor to the end of the buffer
n | after search, move forwards to the next search hit
N | after search, move backwards to the previous search hit
s | search
pg up | move the cursor "screen size" lines up
pg down | move the cursor "screen size" lines down
## Installation
The easiest way to install the latest binaries for Linux and Mac is to run this in a shell:
```sh
curl -sSf https://moncho.github.io/dry/dryup.sh | sudo sh
sudo chmod 755 /usr/local/bin/dry
```
### Binaries
If you dont like to **curl | sh**, binaries are provided.
* **darwin** [amd64](https://github.com/moncho/dry/releases/latest/download/dry-darwin-amd64) / [arm64](https://github.com/moncho/dry/releases/latest/download/dry-darwin-arm64)
* **freebsd** [386](https://github.com/moncho/dry/releases/latest/download/dry-freebsd-386) / [amd64](https://github.com/moncho/dry/releases/latest/download/dry-freebsd-amd64)
* **linux** [386](https://github.com/moncho/dry/releases/latest/download/dry-linux-386) / [amd64](https://github.com/moncho/dry/releases/latest/download/dry-linux-amd64) / [arm64](https://github.com/moncho/dry/releases/latest/download/dry-linux-arm64) / [armv6](https://github.com/moncho/dry/releases/latest/download/dry-linux-armv6) / [armv7](https://github.com/moncho/dry/releases/latest/download/dry-linux-armv7)
* **windows** [amd64](https://github.com/moncho/dry/releases/latest/download/dry-windows-amd64)
#### Mac OS X / Homebrew
If you're on OS X and want to use homebrew:
```
brew tap moncho/dry
brew install dry
```
#### Docker
```docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock -e DOCKER_HOST=$DOCKER_HOST moncho/dry```
#### Arch Linux
```yay -S dry-bin```
### Usage
Open a console, type ```dry```. It will try to connect to:
* A Docker host given as a parameter (**-H**).
* if none given, a Docker host defined in the **$DOCKER_HOST** environment variable.
* if not defined, to **unix:///var/run/docker.sock**.
**dry** does not read `docker context`, so with Docker Desktop, colima or
Rancher Desktop, whose active context is not that socket, name the host with
**-H** or **$DOCKER_HOST**. The same value is what **dry** hands the
`docker compose` plugin, so the two always agree about which daemon they are
talking to.
If no connection with a Docker host succeeds, **dry** will exit.
#### Connecting over SSH
**dry** can talk to a remote Docker daemon through SSH, the same way the
docker CLI does:
```
DOCKER_HOST=ssh://user@host dry
```
Host, port, and user are resolved like the `ssh` command resolves them:
`Hostname`, `Port`, `User`, and `IdentityFile` directives from
`~/.ssh/config` are honored, the port defaults to 22, the user defaults to
the current OS user, and the remote Docker socket defaults to
`/var/run/docker.sock` (a different socket path can be given in the URL,
e.g. `ssh://user@host:2222/run/user/1000/docker.sock`).
Authentication tries, in order: the identity files configured for the host
in `~/.ssh/config` (or, when none is configured, the `~/.ssh/id_*` keys),
keys held by a running SSH agent (`SSH_AUTH_SOCK`), and a password given
in the URL. Passphrase protected key files are used through the agent.
The remote host key is verified against `~/.ssh/known_hosts` and
`/etc/ssh/ssh_known_hosts`, and the connection fails if the host is
unknown or its key does not match. Connect once with `ssh host` to record
the key. Verification can be disabled with
`DRY_SSH_INSECURE_SKIP_HOST_KEY_CHECK=1`, which leaves the connection open
to interception and should only be used against hosts you fully control.
```dry -T light``` launches dry with the light color theme. Available themes: `dark` (default), `light`.
```dry --workspace``` launches the experimental Phase 1 workspace layout.
```dry -p``` launches dry with [pprof](https://golang.org/pkg/net/http/pprof/) package active.
### Docker Compose
Compose projects show up in their own view (key 8), each project row followed by its services indented under it; pressing Enter on a project opens its services, networks and volumes in the Compose Services view.
Keybinding | Description
-----------------|---------------------------------------
u | bring the selected service up, with no prompt; on a project's own row it asks first
d | take the project down, from its own row and behind a confirmation prompt
c | show the project's rendered compose configuration
`u` and `c` work in both the Compose Projects and Compose Services views; `d` works in Compose Projects only.
Projects are discovered two ways: from container labels, including stopped
containers, and from a compose file in the directory **dry** was started in.
A project known only from a file is listed with no running containers, and
pressing `u` brings it up.
Everything compose-specific needs the `docker compose` CLI plugin: the
`u`/`d`/`c` keys, the SYNC column and that directory scan. Without it the
views still list projects found from container labels, and **dry** probes
once at startup, so installing it needs a restart. An empty Compose view has
three causes, and these tell them apart:
* `docker compose version` fails: no plugin, so no scan either.
* the directory holds none of `compose.yaml`, `compose.yml`,
`docker-compose.yml`, `docker-compose.yaml`. The scan looks in the
directory **dry** started in, never below it.
* `docker compose config` in that directory fails. A file that does not
resolve, a YAML error or a required variable with no value, is dropped
without a message.
The scan passes the one file it picked, so an override file beside it is not
applied and `u` on a scanned project creates something different from
`docker compose up` in the same directory. It matches an existing project by
name, and compose takes that name from the directory's basename unless the
file sets `name:`. A project brought up with `-p` therefore needs
`COMPOSE_PROJECT_NAME= dry` to match, or the scan lists it a second
time under the directory's name.
The SYNC column reports whether a service's running containers match its
compose file:
Label | Meaning
---------|----------------------------------------------------------------
`ok` | the containers were created from the compose file as it is now
`drift` | the compose file changed since the containers were created, the next `u` recreates them
`absent` | the compose file defines the service and dry lists no container for it
A service with `absent` gets a row of its own under its project, in the same
name order as the rest, so pressing `u` on it brings up that one service. It
appears once the drift check has run. The keys that need a container to act
on answer "u brings it up" on that row rather than prompting for an action
with nothing to do: `enter`, `l`, `^t`, `^r` and `^e` in both Compose views,
plus `^s` in Compose Services, which is the only view that has it. `u`, the
palette's Up and its Force Recreate all create the service, so they run as
usual. A service behind a compose profile is not part of the file's resolved
configuration, so it gets no row until the profile is enabled, and a service
whose only container is a one-off (`docker compose run`) reads as `absent`,
since dry lists that container under neither the service nor the project.
SYNC is not HEALTH, the column immediately to its left, which is Docker's
own reading of a container: `none` there means the container declares no
healthcheck, and has nothing to do with the compose file. A project row's
STATUS says `not created` for the same idea one level up, when nothing in
the project has containers.
A blank cell is none of those. SYNC is per service, so it is always empty on
a project row, and on the section, network and volume rows of the Compose
Services view. On a service row it means one of:
* no plugin, or the check has not run yet;
* the project's compose file is not on this machine, see below;
* the first check for this project failed. The message bar names it once,
and a later identical failure keeps the label it already had rather than
banners again;
* the file no longer defines a service whose containers are still running;
* the file defines it behind a profile, which reports no hash. Start with
`COMPOSE_PROFILES=dev dry` to fix both that and what `u` brings up.
F5 re-runs the check: the directory scan and every project's drift
in Compose Projects, the selected project's resources and drift in Compose
Services. A refresh that arrives while a check is already running is deferred
rather than dropped, so it lands once that one finishes.
The command palette (:) adds Force Recreate, on a service row in
the Compose Services view: it recreates that service's containers even when
nothing has drifted, which `u` would skip.
#### Compose files and the machine **dry** runs on
`u`, `c` and Force Recreate have to read the compose file, and the plugin
reads it from the filesystem **dry** runs on. The paths come from the
project's `com.docker.compose.project.config_files` label, written by
whichever machine ran `compose up`, or from the directory scan. Every
recorded path has to be absolute and present there; when one is not, **dry**
says `Project has no compose file on this host` and refuses before
running compose.
`docker compose ls -a` prints each project's recorded files, which shows
which of three cases you are in. (It talks to whatever your shell's
`docker context` or `DOCKER_HOST` points at, which is not necessarily the
daemon **dry** is connected to.)
* **No path recorded**, from an older Compose: start **dry** in the
project's directory and the scan fills in the file it finds.
* **A relative path**, which an older Compose recorded as given: bring the
project down and up again from its directory.
* **An absolute path that is not there**: the project was brought up on
another machine, or the file has since moved. A project brought up from a
different directory *on this machine* is not this case, its recorded path
is absolute and still valid.
Either put the files back where the label says, or run **dry** on the machine
that has them. To do the latter, name the daemon with `DOCKER_HOST` or `-H`:
both reach the plugin. **dry** does not read `docker context` and always
passes the plugin a `DOCKER_HOST`, so with Docker Desktop or colima name the
host explicitly rather than relying on the context.
To put the files back, recreate the project directory too, not just the
compose file: **dry** passes the project's
`com.docker.compose.project.working_dir` label as `--project-directory`, and
relative `build:` contexts and bind-mount sources resolve under it. That
label is printed by:
```sh
docker inspect $(docker ps -aq --filter label=com.docker.compose.project= | head -1) \
--format '{{index .Config.Labels "com.docker.compose.project.working_dir"}}'
```
The paths are re-checked on every invocation, so the keys start working the
moment the files are in place.
`d` (down) needs the plugin but no file, because compose removes a project by
its container labels, so it keeps working when the paths do not. The `Ctrl+`
lifecycle keys need neither: they act on containers through the Docker API,
in [Compose Projects](#compose-projects-commands) and
[Compose Services](#compose-services-commands).
### Docker Swarm
**dry** also works with Docker Swarm. When the connected daemon reports an active swarm, three extra views become available: Nodes (5), Services (6), and Stacks (7). Without an active swarm these views, their keybindings, and their command-palette entries are hidden.
### Contributing
All contributions are welcome.
* Fork the project.
* Make changes on a topic branch.
* Pull request.
## Copyright and license
Code released under the MIT license. See
[LICENSE](https://github.com/moncho/dry/blob/master/LICENSE) for the full license text.
## Credits
Built on top of:
* [Bubbletea](https://github.com/charmbracelet/bubbletea)
* [Bubbles](https://github.com/charmbracelet/bubbles)
* [Lipgloss](https://github.com/charmbracelet/lipgloss)
* [Docker](https://github.com/docker/docker)
* [Docker CLI](https://github.com/docker/cli)
## Alternatives
See [Awesome Docker list](https://github.com/veggiemonk/awesome-docker/blob/master/README.md#terminal) for similar tools to work with Docker.