# Runbook: Host disk cleanup (unbounded container logs / apt cache / docker artifacts) ## Purpose Reclaim space on a root filesystem that is filling up (≥70% used) on a Docker Compose host, by fixing unbounded container log growth at the source, clearing apt/journal caches, and removing unused Docker images/volumes. Success: root usage drops to a safe band (≤55% used, or per acceptance in the tracking issue) and log growth stays bounded afterwards. ## Scope - 适用环境: production single-root-fs hosts running Docker Compose stacks (first application: `hk2.chans.xyz`; reusable for `mx2.windy.me` / `us2.wsvc.info` which run the same unbounded-`json.log` pattern). - 适用对象: root filesystem usage; container stdout/stderr log files (`/var/lib/docker/containers/*/*-json.log`); `/var/cache/apt`; systemd journal; unused Docker images / anonymous volumes / build cache. - 不适用情形: hosts without systemd-journald or without Docker; LAN/HAOS hosts (use their own runbooks); cases needing disk *growth* (provider resize) rather than cleanup; anything touching service data volumes or `/opt/*` configs (STOP and use the service-specific runbook instead). ## Ownership - Owner: windy (operator) + agent executing per approval - Last reviewed: 2026-09-02 - Related systems: hk2.chans.xyz (PowerDNS auth / AdGuard Home / Traefik / RustDesk compose stacks) ## Preconditions - SSH access to the target host with **passwordless sudo** (`sudo -n true` must succeed). - A recorded `df -h` baseline and `docker system df` baseline. - **Explicit user approval** for every service touch listed in Approval gates (recorded in the tracking issue, e.g. Plane `vps` VPS-81). - No open incident on the target host. - Container log growth root cause identified in Diagnose before mutating. ## Inputs | Input | Source | Required | Validation | |---|---:|---| | Target host | inventory/hosts.md | yes | SSH login + `uname -r` | | df/docker baseline | live read-only probe | yes | recorded before first mutation | | Approved service touches | user confirmation in tracking issue | yes | issue comment states approval | | Image keep-list (rollback pins) | operator decision in issue | yes | review `docker image ls` before rmi | | Backup of any config edited | local copy with timestamp | yes | exists before edit | ## Safety ### Non-negotiable rules - Prefer read-only diagnosis before mutation (never mutate on an unmeasured disk). - Never use `rm` on a live container log — use `truncate -s 0` (keeps the fd valid). - Never run `docker image prune -a` when a keep-list is intended — no keep-list exists; delete explicitly with `docker rmi`. - Never run `docker volume prune -a` — plain `docker volume prune` (no `-a`) removes only unused anonymous volumes; named/in-use volumes stay. - After every mutation, verify the expected state (`df -h`, container status). - Destructive actions require explicit approval (Approval gates). ### Stop conditions - Live state conflicts with this runbook's preconditions or expectations (e.g. root usage differs wildly from baseline, or a container is unhealthy). - Missing approval, missing backup, or missing rollback ability. - A verification step fails with no documented next step. - Any step would touch a volume/container/mount that is not on the approved list. ### Approval gates | Action | Risk | Explicit approval | Approval record | |---|---:|---|---| | `docker restart ` | low (sec-level blip of that service only) | yes | tracking issue (VPS-81 T1) | | `systemctl restart systemd-journald` | low (sec-level, no state loss) | yes | tracking issue (VPS-81 T2) | | `apt-get clean` | low (re-downloadable) | no | — | | `journalctl --vacuum-*` / journald drop-in | low | no (restart above is gated) | — | | `docker rmi` of unused images | medium (rollback pin removed unless kept) | yes (keep-list) | tracking issue (VPS-81 T3) | | `docker volume prune` | medium (data in anonymous volumes lost) | yes | tracking issue (VPS-81 T4) | | `docker builder prune` | low | no | — | ## Procedure ### Step 1 — Diagnose **Action** Read-only: `df -h`, `df -i`, `sudo du -x -h --max-depth=1 /`, `docker system df`, and locate oversized container logs: `sudo ls -la /var/lib/docker/containers/*/*-json.log`. Map a big log to its container (`docker inspect -f '{{.Name}} {{.LogPath}}' `), then inspect what it logs (`sudo tail -c 400000 `; count `[debug]` lines) and find the config flag driving it (e.g. AGH `log.verbose` in its YAML; note `log.file: ""` means the app's own rotation keys are inert and output goes to the container log). **Expected** A full accounting of root usage and identification of: (a) any unbounded container log and its root-cause flag; (b) reclaimable apt cache; (c) journal size and journald limits; (d) unused images (0 dangling expected) and unused anonymous volumes. **Decision** - If root is ≥70% used or any container log is unbounded → Step 2. - If root is healthy and logs are bounded → STOP (no change needed; record evidence). - If state conflicts with expectations (e.g. missing sudo, unexpected mount) → STOP. ### Step 2 — Fix noisy container logging at the source, then truncate **Action** 1. Back up the app config: `sudo cp .bak-YYYYMMDD-`. 2. Disable the debug/verbose flag (e.g. `log.verbose: true → false` in the AGH YAML). 3. Apply config with a container restart: `docker restart ` (config-level change; **no recreate** needed and daemon.json rotation would not apply anyway). 4. Truncate the accumulated logs: `sudo truncate -s 0 ` for the chatty container(s) (and any other oversized ones, e.g. traefik). 5. Record `df -h` before/after. **Expected** `docker logs ` no longer shows the `[debug]` flood; the `*-json.log` stops growing; several GB reclaimed. **Verification** - `sudo tail -c 200000 ` after ≥1 minute → no new debug lines. - `df -h` improvement recorded. - Container still `Up (healthy)`. **Rollback** - Trigger: log volume unchanged, service degraded, or debug output is actually needed. - Action: restore the config backup and `docker restart `. - Verify: original verbose behaviour back; container healthy. ### Step 3 — Clear apt cache and cap journald **Action** 1. `sudo apt-get clean` (clears only `/var/cache/apt/archives`; `/var/lib/apt/lists` is not cleared by it and regenerates on `apt update` — optional/low value, skip). 2. `sudo journalctl --vacuum-size=100M`. 3. Write drop-in `/etc/systemd/journald.conf.d/00-disk-.conf`: `[Journal]` + `SystemMaxUse=200M`. 4. `sudo systemctl restart systemd-journald` (approved service touch). 5. Record `df -h` before/after. **Expected** Archives cleared (~1.4G on hk2), journal ≤100M, future journal capped at 200M. **Verification** - `du -sh /var/cache/apt/archives` → ~0. - `journalctl --disk-usage` → ≤100M. - `systemctl show systemd-journald -p ...` or restart log confirms new limit; `journalctl -b` still readable. **Rollback** - Trigger: journald fails to start or logs lost unexpectedly. - Action: remove the drop-in, `sudo systemctl restart systemd-journald`. - Verify: journald active, prior journal entries still listed. ### Step 4 — Remove unused Docker images (explicit keep-list) **Action** 1. Enumerate unused images: `docker image ls` cross-checked against the images of running containers (`docker ps --format '{{.Image}}'`). Re-enumerate at execution time — the list drifts. 2. Present the exact removal list to the operator; keep the agreed rollback pin(s) (e.g. `powerdns/pdns-auth-50:5.0.5`) and delete the rest explicitly: `docker rmi ...` (per image). 3. Record `df -h` before/after. **Expected** Only in-use images + kept pins remain; ~1–2.5G reclaimed (reclaim is an upper bound — layers shared with kept images are not freed; measure with `df`, do not promise the estimate). **Verification** - `docker image ls` shows only the expected set. - `docker system df` images reclaimable ≈ 0 for the removed set. - All containers still `Up`. **Rollback** - Trigger: an image that was actually needed was removed. - Action: re-pull it from the registry (`docker pull `); if a kept pin must change, update the compose pin and `up -d`. - Verify: image present; affected service healthy. ### Step 5 — Remove unused anonymous volumes and build cache **Action** 1. Enumerate volumes: `docker volume ls`, and confirm which are referenced by containers (`docker inspect` Mounts). Expected targets: anonymous volumes with no container reference. 2. `docker volume prune` (**no `-a`**) — engine ≥ v23 removes only unused anonymous volumes; in-use volumes (e.g. PG data) are protected by container references in every version. 3. `docker builder prune -f`. 4. Record `df -h` before/after. **Expected** Unused anonymous volumes (~1.2G on hk2) and build cache gone; in-use volumes intact. **Verification** - `docker volume ls` shows only in-use volumes. - Services that own volumes (e.g. postgres) report healthy and data present. - `df -h` improvement recorded. **Rollback** - Trigger: data loss suspected in a removed volume. - Action: restore from backup if the volume ever contained data; verify against the pre-prune enumeration (targets must be anonymous + unreferenced before prune). - Note: this is why target enumeration is recorded before pruning. ## Troubleshooting ### Troubleshooting A — Log still grows after disabling verbose - Evidence: `sudo tail -c 200000 ` still shows new lines; app config re-checked. - Allowed actions: check for a second verbose source (container entrypoint flags, other apps in the same log); check `docker inspect --format '{{.HostConfig.LogConfig}}'`. - Next step: back to Step 2 or STOP if a container-level log-opts change (recreate) would be needed — that is a separate approval. ### Troubleshooting B — `docker rmi` fails (image in use) - Evidence: `image is being used by stopped container ...`. - Allowed actions: identify the stopped container (`docker ps -a`); confirm it is not needed; remove it only with explicit approval. - Next step: re-run rmi for the remaining images; never force-delete blindly. ### Troubleshooting C — `docker volume prune` would remove more than expected - Evidence: prune dry-run/listing includes a named or referenced volume. - Allowed actions: abort; do not add `-a`; re-check references. - Next step: STOP and report to the operator with the enumeration. ## Final Verification The flow is successful only when all of the following hold: - `df -h` root usage is in the agreed band (VPS-81: 76% → ≤55% used; measure, do not assume). - `docker system df` shows reclaimable ≈ 0 for images/volumes targeted. - All containers `Up` (health checks pass); public services verified (`dig @ SOA ` for DNS hosts; service URLs reachable). - Tracking issue updated with before/after `df`, actions, and the one-week observation checkpoint for log growth. ## Failure Handling If the flow cannot complete: 1. Stop further mutation. 2. Collect command output, timestamps, and the exact step that failed. 3. Record completed steps, actual results, unmet expectations, and whether a rollback ran. 4. Hand over per the tracking issue with evidence; do not guess further. ## References - Plane `vps` issue VPS-81 "hk2: 释放根盘空间" (+ subtasks VPS-82…88) — plan, review findings, approvals. - [hosts/hk2.chans.xyz.md](../hosts/hk2.chans.xyz.md) — host facts. - [RUNBOOKS.md](../RUNBOOKS.md) — runbook spec; [runbooks/README.md](README.md) — index.