# Lazydocker Admin & Architecture Guide

This guide is for operators, platform engineers, and contributors who need to
understand how lazydocker works internally, how to configure it exhaustively,
how to deploy it across teams and remote hosts, and what its security posture
is. For day-to-day usage and keybindings, see the
[User Guide](/jesseduffield/lazydocker/user-guide/).

---

## 1. Executive Summary

Lazydocker is a single-binary terminal UI for Docker and Docker Compose, written
in Go. It is a **client**: it holds no state of its own beyond a YAML config
file, runs no daemon or server, and exposes no network ports. It talks to the
Docker Engine through the official Docker SDK (`github.com/docker/docker`) and
shells out to the `docker compose` CLI for Compose-specific operations.

Architecturally it is the Docker sibling of `lazygit` (same author, same
`gocui` TUI foundation, same config-driven command-template philosophy). The
application is organized into three clean layers:

- **`commands`** — the Docker/OS abstraction: a thin wrapper over the Docker SDK
  plus a shell-command runner.
- **`gui`** — the `gocui`-based terminal interface: panels, keybindings, layout,
  and rendering.
- **`config`** — user configuration with sensible defaults merged over a
  `config.yml`.

| Property | Value |
|----------|-------|
| Language | Go (`go 1.22`, toolchain `1.23.6`) |
| TUI library | `github.com/jesseduffield/gocui` (a fork of `jroimartin/gocui`) |
| Docker access | Docker Engine SDK + `docker compose` CLI |
| State | Stateless; one optional `config.yml` |
| Network listeners | None |
| Privilege model | Inherits the caller's access to the Docker socket |
| Distribution | Single static binary (Homebrew, Scoop, Choco, AUR, Go, Docker, script) |

---

## 2. Architecture

### 2.1 High-level component diagram

```text
                         ┌──────────────────────────────────────┐
   Terminal (stdin/out)  │              lazydocker               │
        ▲     │          │                                       │
        │     ▼          │   ┌───────────────────────────────┐   │
   ┌──────────────┐      │   │  pkg/gui  (gocui TUI)          │   │
   │  Keyboard /  │◀────▶│   │  panels, layout, keybindings, │   │
   │  Mouse / TTY │      │   │  main-panel tabs, rendering   │   │
   └──────────────┘      │   └───────────────┬───────────────┘   │
                         │                   │                   │
                         │   ┌───────────────▼───────────────┐   │
                         │   │  pkg/commands                 │   │
                         │   │  DockerCommand · OSCommand    │   │
                         │   └───────┬───────────────┬───────┘   │
                         │           │               │           │
                         │  Docker SDK         shell exec         │
                         └───────────┼───────────────┼───────────┘
                                     ▼               ▼
                         ┌────────────────┐  ┌──────────────────┐
                         │ Docker Engine  │  │ docker compose   │
                         │ (daemon/API)   │  │ CLI subprocess   │
                         └───────┬────────┘  └──────────────────┘
                                 ▼
                      unix socket / npipe / ss:// / tcp
```

### 2.2 Layer responsibilities

| Package | Responsibility | Key types |
|---------|----------------|-----------|
| `main` (`main.go`) | Parse CLI flags (via `integrii/flaggy`), build `AppConfig`, construct and run the `App`, map known errors to friendly messages. | — |
| `pkg/app` | Wire everything together: logger, translations, `OSCommand`, `DockerCommand`, `Gui`. | `App` |
| `pkg/config` | Define `UserConfig` and all sub-configs; merge user `config.yml` over compiled defaults. | `AppConfig`, `UserConfig` |
| `pkg/commands` | Wrap the Docker SDK (containers, images, volumes, networks, services, stats) and run shell commands. | `DockerCommand`, `OSCommand`, `Container`, `Service`, `Image`, `Volume`, `Network`, `Project` |
| `pkg/gui` | The entire terminal UI: side panels, main-panel tabs, keybindings, layout (`boxlayout`), confirmation/menu panels, filtering. | `Gui`, `Panels`, `Binding` |
| `pkg/tasks` | Manage long-running, cancellable background tasks (log following, stat polling) so switching panels cleanly stops the previous task. | `TaskManager`, `Task` |
| `pkg/i18n` | Translation sets for 9 languages. | `TranslationSet`, `Localizer` |
| `pkg/utils` | Templating, table rendering, color helpers, byte formatting. | — |
| `pkg/log` | Logrus-based logging (file output when debug enabled). | — |
| `pkg/cheatsheet` + `scripts/cheatsheet` | Generate the `docs/keybindings/Keybindings_*.md` files from the keybinding definitions. | — |

### 2.3 Startup sequence

Tracing `main.go` → `pkg/app/app.go`:

1. **`updateBuildInfo()`** — if built from source, derive version/commit/date
   from Go's embedded VCS build info.
2. **Flag parsing** (`flaggy`) — read `-c/-d/-f/-p` and `--version`. If `-c` is
   set, print the default config as YAML and exit.
3. **`config.NewAppConfig(...)`** — locate/create the config directory, load
   `config.yml`, and merge it over `GetDefaultConfig()`. Compose files passed
   via `-f` are appended to the `dockerCompose` template as `-f` flags.
4. **`app.NewApp(config)`**:
   - Create the logger.
   - Load the translation set for `gui.language`.
   - Construct `OSCommand` (shell runner).
   - Construct `DockerCommand`: determine the Docker host, set up any SSH
     tunnel, create the SDK client with API-version negotiation, detect whether
     the current directory is a Compose project.
   - Construct the `Gui`.
5. **`app.Run()`** → `Gui.Run()` — enter the gocui event loop.
6. On exit, **`app.Close()`** closes the Docker client and any SSH tunnel.

### 2.4 Error handling at the top level

`main.go` distinguishes *known* errors from unexpected ones via
`App.KnownError()`. For example, the Docker "permission denied … socket" error
is rewritten into a friendly, translated message and exits cleanly (`os.Exit(0)`)
rather than dumping a stack trace. Connection failures
(`client.IsErrConnectionFailed`) print the translated "connection failed"
message. Everything else is wrapped with a stack trace and logged via
`log.Fatalf`.

---

## 3. How Lazydocker Connects to Docker

Understanding host resolution is essential for remote and CI deployments.

### 3.1 Docker host resolution order

`commands.determineDockerHost()` resolves the daemon endpoint with this
precedence:

1. **`DOCKER_HOST`** environment variable, if set.
2. The host bound to the **current Docker context** (selected via
   `DOCKER_CONTEXT`, or the context store's `currentContext`).
3. The **platform default socket**:
   - Unix/macOS: `unix:///var/run/docker.sock`
   - Windows: `npipe:////./pipe/docker_engine`

### 3.2 SSH hosts and tunneling

If the resolved host begins with `ssh://`, lazydocker:

1. Exports it back to `DOCKER_HOST`.
2. Invokes the SSH handler (`pkg/commands/ssh`), which establishes a local Unix
   socket tunneled over SSH to the remote host.

This lets you drive a remote daemon transparently:

```bash
DOCKER_HOST=ssh://user@remote-host lazydocker
```

### 3.3 API version negotiation

The Docker client is built explicitly with `WithAPIVersionNegotiation()` and
**without** `client.FromEnv`. This is deliberate: `FromEnv` would honor
`DOCKER_API_VERSION` and lock the client version, breaking compatibility with
older daemons (the cause of the "client version 1.25 is too old" error). By
negotiating, lazydocker supports a wide range of daemon versions regardless of a
stray `DOCKER_API_VERSION` in the environment.

> [!IMPORTANT]
> Because lazydocker ignores `DOCKER_API_VERSION` by design, do not rely on that
> variable to force a version. If you must, the behavior is intentionally not
> honored for the SDK client.

---

## 4. Compose Project Detection and Scoping

Lazydocker behaves differently depending on whether it is operating inside a
Compose project. The predicate is `DockerCommand.IsProjectScoped()`:

```text
IsProjectScoped() == InDockerComposeProject  OR  (-p / ProjectName was set)
```

| Situation | Project scoped? | Effect |
|-----------|-----------------|--------|
| Inside a compose dir, no `-p` | Yes | Services + Project panels shown; containers filtered to project. |
| Inside a compose dir, with `-p` | Yes | Same, scoped to the named project. |
| Outside a compose dir, no `-p` | No | Services/Project panels hidden; **all** containers shown flat. |
| Outside a compose dir, with `-p` | Yes | Panels reappear; containers filtered to the named project. |

- **`InDockerComposeProject`** is determined at startup by running the
  `checkDockerComposeConfig` template (`docker compose config --quiet`). If it
  errors, the directory is not a compose context.
- **Services** are derived two ways and merged: from `docker compose config
  --services` (catches *stopped* services) and from container labels
  (`com.docker.compose.service` / `.project`, catches services across *all*
  projects). Compose-derived services win for the local project.
- The **local project name** is resolved from container labels first, falling
  back to the launch directory's base name.

> [!NOTE]
> The "outside compose dir + `-p`" path is a deliberately supported case. It was
> silently disabled by an earlier refactor and restored; the regression test
> `TestIsProjectScoped` guards it.

---

## 5. Configuration Mastery

Lazydocker requires no configuration but exposes a deep config surface. This
section documents **every** key, its type, and its default. All keys are
optional and live in `config.yml` (see the User Guide for per-OS paths).

> [!CAUTION]
> Config merging is shallow per top-level mapping. If you specify a parent key
> like `commandTemplates:` but provide only some children, lazydocker keeps the
> defaults for the others — **but** providing an empty mapping under a key can
> scrap the defaults for that branch and may crash the app. Always provide
> complete values for any nested block you override.

### 5.1 `gui` — interface and appearance

| Key (YAML) | Type | Default | Meaning |
|------------|------|---------|---------|
| `scrollHeight` | int | `2` | Lines scrolled per keypress in the main panel. |
| `language` | string | `"auto"` | UI language: `auto`, `en`, `pl`, `nl`, `de`, `tr`, `fr`, `es`, `pt`, `zh`. |
| `scrollPastBottom` | bool | `false` | Allow scrolling past the end of the main view. |
| `mouseEvents` | bool | `false`¹ | When `true`, ignore mouse events. (Field `IgnoreMouseEvents`.) |
| `theme.activeBorderColor` | []string | `["green","bold"]` | Border color of the focused panel. |
| `theme.inactiveBorderColor` | []string | `["default"]` | Border color of unfocused panels. |
| `theme.selectedLineBgColor` | []string | `["blue"]` | Background of the selected line. |
| `theme.optionsTextColor` | []string | `["blue"]` | Color of the options/footer text. |
| `showAllContainers` | bool | `false` | Show every container (`docker ps -a`), not just those not tied to a service. |
| `returnImmediately` | bool | `false` | Skip the "press enter to return" prompt after a subprocess exits. |
| `wrapMainPanel` | bool | `true` | Word-wrap the main panel (higher CPU cost). |
| `legacySortContainers` | bool | `false` | Sort containers by name (legacy) instead of by status. |
| `sidePanelWidth` | float | `0.3333` | Side-panel width as a fraction of screen width. |
| `showBottomLine` | bool | `true` | Show the bottom keybinding/status line. |
| `expandFocusedSidePanel` | bool | `false` | Accordion effect: the focused side panel grows. |
| `screenMode` | string | `"normal"` | Startup screen mode: `normal`, `half`, `fullscreen`. |
| `containerStatusHealthStyle` | string | `"long"` | Status/health display: `long` (words), `short` (1–2 chars), `icon` (emoji). |
| `border` | string | `"rounded"` | Window border style: `rounded`, `single`, `double`, `hidden`. |

¹ The default struct value is `false` (mouse events **enabled**). Set
`mouseEvents: true` to *disable* mouse handling.

### 5.2 `logs` — log rendering

| Key | Type | Default | Meaning |
|-----|------|---------|---------|
| `timestamps` | bool | `false` | Prefix log lines with timestamps. |
| `since` | string | `"60m"` | How far back to fetch logs. `""` means all logs. |
| `tail` | string | `""` | Limit to the last N lines (e.g. `"200"`). |

### 5.3 `commandTemplates` — the commands lazydocker runs

This is the most powerful customization point. Every Compose action is a Go
template; `{{ .DockerCompose }}` expands to the `dockerCompose` value, and
`{{ .Service.Name }}` / `{{ .Project.Name }}` are filled from context.

| Key | Default |
|-----|---------|
| `dockerCompose` | `docker compose` |
| `restartService` | `{{ .DockerCompose }} restart {{ .Service.Name }}` |
| `startService` | `{{ .DockerCompose }} start {{ .Service.Name }}` |
| `stopService` | `{{ .DockerCompose }} stop {{ .Service.Name }}` |
| `upService` | `{{ .DockerCompose }} up -d {{ .Service.Name }}` |
| `up` | `{{ .DockerCompose }} up -d` |
| `down` | `{{ .DockerCompose }} down` |
| `downWithVolumes` | `{{ .DockerCompose }} down --volumes` |
| `rebuildService` | `{{ .DockerCompose }} up -d --build {{ .Service.Name }}` |
| `recreateService` | `{{ .DockerCompose }} up -d --force-recreate {{ .Service.Name }}` |
| `serviceLogs` | `{{ .DockerCompose }} logs --since=60m --follow {{ .Service.Name }}` |
| `viewServiceLogs` | `{{ .DockerCompose }} logs --follow {{ .Service.Name }}` |
| `allLogs` | `{{ .DockerCompose }} logs --tail=300 --follow` |
| `viewAlLogs` | `{{ .DockerCompose }} logs` |
| `dockerComposeConfig` | `{{ .DockerCompose }} config` |
| `checkDockerComposeConfig` | `{{ .DockerCompose }} config --quiet` |
| `serviceTop` | `{{ .DockerCompose }} top {{ .Service.Name }}` |

> [!NOTE]
> The `viewAlLogs` key is spelled with a single `l` in the source — this is the
> actual YAML key you must use. It is preserved here verbatim so your config
> matches what the application reads.

> [!TIP]
> `checkDockerComposeConfig` is what lazydocker uses to decide whether the
> current directory is a Compose project. If detection misbehaves, this is the
> command to inspect.

If `dockerCompose` is left at its default `docker compose`, lazydocker probes
`docker compose version` at startup and silently falls back to `docker-compose`
if the v2 plugin is unavailable.

### 5.4 `oS` — opening files and links

| Key | Type | Default (Linux) | Default (macOS) | Default (Windows) |
|-----|------|-----------------|-----------------|-------------------|
| `openCommand` | string | `sh -c "xdg-open {{filename}} >/dev/null"` | `open {{filename}}` | `cmd /c "start "" {{filename}}"` |
| `openLinkCommand` | string | `sh -c "xdg-open {{link}} >/dev/null"` | `open {{link}}` | `cmd /c "start "" {{link}}"` |

These are used by the "open in browser" action (`w`) and any open-file flows.
`{{filename}}` and `{{link}}` are placeholder substitutions.

### 5.5 `stats` — graphs and collection window

| Key | Type | Default | Meaning |
|-----|------|---------|---------|
| `maxDuration` | duration | `3m` | How long stat history is retained per container. |
| `graphs[].caption` | string | `"CPU (%)"`, `"Memory (%)"` | Graph label. |
| `graphs[].statPath` | string | `DerivedStats.CPUPercentage`, `DerivedStats.MemoryPercentage` | Path into the recorded-stats struct. |
| `graphs[].color` | string | `cyan`, `green` | Graph line color. |
| `graphs[].min` / `max` | float | — | Fixed axis bounds (set with `minType`/`maxType: "static"`). |
| `graphs[].minType` / `maxType` | string | `""` | `""` = data-driven; `"static"` = use `min`/`max`. |
| `graphs[].height` | int | — | Graph height in characters. |

Valid `statPath` values come from the `RecordedStats` struct
(`ClientStats.*` for raw Docker stats, `DerivedStats.*` for computed values).
The Stats tab renders this struct as JSON so you can discover paths and
PascalCase them.

### 5.6 `customCommands` and `bulkCommands`

Both share the `CustomCommand` shape, grouped by object type (`containers`,
`services`, `images`, `volumes`, `networks`):

| Field (YAML) | Type | Meaning |
|--------------|------|---------|
| `name` | string | Menu label. |
| `command` | string | Go-templated command. |
| `attach` | bool | Switch to the command as an interactive subprocess. |
| `shell` | bool | Run via a shell (`/bin/bash -c "…"`). |
| `serviceNames` | []string | Restrict to specific services. |

`bulkCommands` apply to all items in a panel. Lazydocker ships default bulk
commands for services (up / up attached / stop / pull / build / down /
down-with-volumes / down-with-images / down-with-volumes-and-images). Containers
also have built-in bulk actions (stop all, remove all, prune).

Template variables available in commands: `{{ .DockerCompose }}`,
`{{ .Container }}`, `{{ .Service }}`, `{{ .Image }}`, `{{ .Volume }}`,
`{{ .Network }}`, `{{ .Project }}`.

### 5.7 `replacements`

| Key | Type | Meaning |
|-----|------|---------|
| `imageNamePrefixes` | map[string]string | Replace registry prefixes in displayed image names. |

### 5.8 Config directory resolution

`config.NewAppConfig` resolves the config directory via:

1. **`CONFIG_DIR`** environment variable, if set.
2. A **legacy** XDG path under vendor `jesseduffield`, if it already exists.
3. The modern XDG config home.

The directory is created if missing, and an empty `config.yml` is created on
first run. Writes back to the config (via `WriteToUserConfig`) use `omitempty`,
so zero-valued settings are intentionally not persisted.

---

## 6. Security Analysis

Lazydocker's threat surface is small but its privilege level is high. Treat it
as a privileged administrative tool.

### 6.1 Privilege model — the Docker socket is the crown jewel

Lazydocker has exactly the access its caller has to the Docker daemon. Access to
the Docker socket is **equivalent to root on the host**: a user who can talk to
the daemon can mount the host filesystem into a container and escalate. This is
inherent to Docker, not specific to lazydocker, but it means:

> [!CAUTION]
> Anyone who can run lazydocker against your daemon can start, stop, remove, and
> exec into any container, and can launch new privileged containers. Restrict
> Docker socket access to trusted administrators.

### 6.2 Arbitrary command execution via config

`config.yml` is effectively executable. Two surfaces run arbitrary commands:

- **`commandTemplates`** — every value is a shell command lazydocker runs.
- **`customCommands` / `bulkCommands`** — user-defined commands, optionally via
  a shell (`shell: true`).
- **Editor invocation** — pressing `e` runs `$VISUAL`/`$EDITOR` (falling back to
  `vi`).

> [!WARNING]
> A malicious or compromised `config.yml` can run arbitrary code with the user's
> privileges. Protect the config file's permissions and never load a config from
> an untrusted source. In shared or CI environments, pin `CONFIG_DIR` to a
> controlled location.

### 6.3 Remote hosts and credentials

- `DOCKER_HOST=ssh://…` opens an SSH tunnel; SSH key handling is delegated to
  the local SSH agent/config, not managed by lazydocker.
- TLS material for `tcp://` hosts is read from the environment
  (`WithTLSClientConfigFromEnv`), following standard Docker conventions
  (`DOCKER_TLS_VERIFY`, `DOCKER_CERT_PATH`).

### 6.4 Secret handling in the UI

- The **Env** tab displays a container's environment variables verbatim,
  including any secrets baked into the container's environment. Be mindful when
  screen-sharing.
- The confirmation-panel code is explicitly written to **never** echo the
  original prompt in error messages, because prompts may contain sensitive input
  such as passwords.

### 6.5 No network exposure

Lazydocker opens no listening sockets and serves no API. Its only outbound
connections are to the Docker daemon (local socket, SSH tunnel, or TCP per your
`DOCKER_HOST`) and to the browser/editor it shells out to.

---

## 7. Operations

### 7.1 Deployment patterns

| Pattern | How | Notes |
|---------|-----|-------|
| **Local workstation** | Install the binary; run `lazydocker`. | Simplest; uses the local socket. |
| **Team-standardized config** | Distribute a `config.yml` to the per-OS path or pin `CONFIG_DIR`. | Standardize themes, command templates, and custom commands. |
| **Remote daemon over SSH** | `DOCKER_HOST=ssh://user@host lazydocker`. | Tunnel is created automatically. |
| **Inside a container** | Mount `/var/run/docker.sock`; mount a config volume. | Logs/stats may not render; high privilege — trusted images only. |
| **CI / scripted** | Generally not applicable — lazydocker is interactive. | Use the `docker`/`docker compose` CLIs in CI instead. |

### 7.2 Logging and debugging

- Launch with `--debug` (or set `DEBUG=TRUE`) to enable verbose logging via
  logrus.
- `LOG_LEVEL` is honored by the logging package for finer control.
- Debug logs are the first place to look when startup fails or a shelled-out
  command errors.

### 7.3 Upgrades

Lazydocker is a single binary; upgrading is replacing it. The provided
`scripts/install_update_linux.sh` re-downloads the latest release into
`$HOME/.local/bin` (or `$DIR`). Package-manager installs (Homebrew, Scoop,
Choco, AUR) upgrade through their normal channels. Config files are forward
compatible — new keys simply pick up defaults.

### 7.4 Backup and state

There is no application state to back up beyond `config.yml`. Version-control
your team's `config.yml` if you standardize one.

### 7.5 Environment variables reference

| Variable | Read by | Effect |
|----------|---------|--------|
| `DOCKER_HOST` | `determineDockerHost` | Explicit daemon endpoint (highest precedence). |
| `DOCKER_CONTEXT` | `determineDockerHost` | Selects a Docker context's host. |
| `DOCKER_API_VERSION` | (intentionally **not** honored for the SDK client) | Ignored to preserve version negotiation. |
| `CONFIG_DIR` | `configDirForVendor` | Override the config directory. |
| `DEBUG` | `NewAppConfig` | `TRUE` enables debug mode. |
| `LOG_LEVEL` | `pkg/log` | Sets log verbosity. |
| `VISUAL`, `EDITOR` | `OSCommand.EditFile` | Editor for the `e` (edit config) action; falls back to `vi`. |
| `DIR` | install script | Target directory for the install/update script. |
| `DOCKER_TLS_VERIFY`, `DOCKER_CERT_PATH` | Docker SDK (`WithTLSClientConfigFromEnv`) | TLS for `tcp://` hosts. |

---

## 8. Internals for Contributors

### 8.1 Repository layout

```text
lazydocker/
├── main.go                      Entry point: flags, build info, bootstrap
├── pkg/
│   ├── app/        app.go        App wiring + known-error mapping
│   ├── config/                   UserConfig, defaults, platform-specific OS config
│   ├── commands/                 Docker SDK + OS command wrappers
│   │   ├── docker.go             DockerCommand, host resolution, refresh logic
│   │   ├── container.go          Container ops (start/stop/restart/pause/remove/top)
│   │   ├── container_stats.go    Stats structs + CPU/memory derivation
│   │   ├── image.go              Image ops + layer history
│   │   ├── service.go            Compose service ops
│   │   ├── volume.go / network.go Volume/network ops
│   │   ├── os.go                 Shell runner, file/editor/link helpers
│   │   └── ssh/                  SSH tunnel handler for ssh:// hosts
│   ├── gui/                      gocui TUI: panels, keybindings, layout, tabs
│   │   ├── gui.go                Gui struct, state, panel registry
│   │   ├── keybindings.go        All keybindings (source of truth)
│   │   ├── arrangement.go        boxlayout-based panel arrangement
│   │   ├── *_panel.go            One file per panel + presentation/
│   │   └── tasks_adapter.go      Bridges tasks to the GUI
│   ├── tasks/      tasks.go      Cancellable background task manager
│   ├── i18n/                     9 translation sets
│   ├── log/        log.go        Logrus setup
│   └── utils/      utils.go      Templating, tables, colors, byte formatting
├── docs/
│   ├── Config.md                 User config reference
│   └── keybindings/              Auto-generated cheatsheets (9 languages)
└── scripts/cheatsheet/main.go    Cheatsheet generator (generate | check)
```

### 8.2 The task system (concurrency model)

The GUI is event-driven, but logs and stats need continuous updates. The
`tasks` package provides cancellable tasks so that switching panels stops the
previous stream cleanly:

- **`NewTickerTask`** runs a function on an interval (logs poll every 200ms;
  stats and top every 1s) until its context is cancelled.
- **`NewSimpleRenderStringTask`** renders a one-shot string (env, config tabs).
- An **object cache key** (e.g. `containers-<id>-<state>`) determines when the
  main panel must re-run its render — for instance, restarting a container
  changes its state and forces logs to be re-read.
- Stats are streamed from `Client.ContainerStats` in a background goroutine and
  appended to a per-container ring trimmed to `stats.maxDuration`.

### 8.3 Panel rendering pipeline

Each side panel is a generic `SideListPanel[T]` parameterized by the Docker
object type. A panel defines:

- **`GetTableCells`** — how to render each row (via the `presentation/` package).
- **`Filter`** — which items to show (e.g. project scoping, hide-exited).
- **`Sort`** — ordering (containers default to status order: running → exited →
  created; `legacySortContainers` switches to name order).
- **`GetMainTabs`** — the main-panel tabs and their render functions.

The screen layout is computed by `arrangement.go` using the `boxlayout` package,
which arranges windows by weight/size and adapts to terminal size (including a
portrait mode for narrow terminals and a "limit" box for very small ones).

### 8.4 Keybindings as the source of truth

`pkg/gui/keybindings.go` (`GetInitialKeybindings`) is the canonical list of
bindings. A `Binding` ties a view name (empty = global), a key/rune, a modifier,
a handler, and a description. The cheatsheet files under `docs/keybindings/` are
**generated** from these definitions plus the i18n descriptions by
`scripts/cheatsheet/main.go generate`; `… check` verifies they are up to date in
CI. When adding a binding, update the handler and run the generator.

### 8.5 Internationalization

`pkg/i18n` holds a `TranslationSet` per language (English is the base/reference).
`gui.language: auto` detects the system locale via `jibber_jabber`. User-facing
strings flow through `gui.Tr.*` so the cheatsheet generator and the UI stay in
sync.

### 8.6 Key dependencies

| Dependency | Role |
|------------|------|
| `github.com/jesseduffield/gocui` | Terminal UI primitives (views, events, layout). |
| `github.com/docker/docker` (SDK) | Container/image/volume/network/stats operations. |
| `github.com/docker/cli` | Docker context/config loading for host resolution. |
| `github.com/jesseduffield/lazycore` | Shared layout (`boxlayout`) and utilities. |
| `github.com/integrii/flaggy` | CLI flag parsing. |
| `github.com/samber/lo` | Functional helpers (map/filter/find). |
| `github.com/sirupsen/logrus` | Logging. |
| `github.com/sasha-s/go-deadlock` | Deadlock-detecting mutexes (used on GUI/command state). |
| `github.com/jesseduffield/asciigraph` | ASCII stat graphs. |
| `github.com/imdario/mergo` | Merge command objects / config. |

---

## 9. Testing

The project uses Go's standard testing with `stretchr/testify`. Notable
coverage:

- `config/app_config_test.go` — compose-file flag composition and round-tripping
  writes to `config.yml`.
- `commands/docker_test.go` — API-version negotiation regression (`#715`) and
  the `IsProjectScoped` decision matrix.
- `commands/container_stats_test.go` — CPU-percentage calculation.
- `gui/sort_container_test.go`, `gui/panels/filtered_list_test.go` — sorting and
  filtering logic.

Run the suite with:

```bash
go test ./...
```

```text
ok  	github.com/jesseduffield/lazydocker/pkg/commands	0.123s
ok  	github.com/jesseduffield/lazydocker/pkg/config	0.045s
ok  	github.com/jesseduffield/lazydocker/pkg/gui	0.210s
```

---

## 10. Potential Improvements

Observations from the source, useful for maintainers and contributors:

- **Remappable keybindings.** Bindings are compiled in (`keybindings.go`). A
  config-driven keymap (as in lazygit) is a frequently desired feature.
- **Stats container-in-container.** Logs/stats not rendering when lazydocker
  runs inside a container is a known limitation worth documenting in-app or
  detecting and warning about.
- **Service logs vs container logs.** `serviceLogs` is defined but the app
  currently shows the corresponding container's logs; explicit multi-container
  service log aggregation is noted as a TODO in the source.
- **`O(N*M)` container/service filtering.** The project-scoped container filter
  iterates services × containers; fine for typical sizes but flagged in comments
  as something to watch for large deployments.
- **Compose profile support** (`#423`) and **column sorting in panels**
  (`#384`) are open community requests.

---

## 11. Quick Reference

| Need | Where |
|------|-------|
| Default config dump | `lazydocker --config` |
| Config file (Linux) | `~/.config/lazydocker/config.yml` |
| Config file (macOS) | `~/Library/Application Support/jesseduffield/lazydocker/config.yml` |
| Config file (Windows) | `%APPDATA%\lazydocker\config.yml` |
| Override config dir | `CONFIG_DIR` env var |
| Remote daemon | `DOCKER_HOST=ssh://user@host lazydocker` |
| Debug logging | `lazydocker --debug` or `DEBUG=TRUE` |
| Scope to a project | `lazydocker -p <name>` |
| Combine compose files | `lazydocker -f a.yml -f b.yml` |
| Keybinding source | `pkg/gui/keybindings.go` |
| Regenerate cheatsheets | `go run scripts/cheatsheet/main.go generate` |
| Requirements | Docker ≥ 29.0.0 (API ≥ 1.24); Compose ≥ 1.23.2 (optional) |

---

_Generated June 2026 from jesseduffield/lazydocker at 7e7aadc. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
