277 lines
12 KiB
Markdown
277 lines
12 KiB
Markdown
# 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 <chatty container>` | 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}}' <id>`), then inspect what
|
||
it logs (`sudo tail -c 400000 <logpath>`; 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 <config> <config>.bak-YYYYMMDD-<issue>`.
|
||
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 <container>` (config-level
|
||
change; **no recreate** needed and daemon.json rotation would not apply anyway).
|
||
4. Truncate the accumulated logs: `sudo truncate -s 0 <json.log>` for the chatty
|
||
container(s) (and any other oversized ones, e.g. traefik).
|
||
5. Record `df -h` before/after.
|
||
|
||
**Expected**
|
||
|
||
`docker logs <container>` no longer shows the `[debug]` flood; the `*-json.log`
|
||
stops growing; several GB reclaimed.
|
||
|
||
**Verification**
|
||
|
||
- `sudo tail -c 200000 <json.log>` 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 <container>`.
|
||
- 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-<issue>.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 <repo:tag> ...` (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 <repo:tag>`); 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 <json.log>` 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 <c> --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 @<host-ip> SOA <zone>` 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.
|