docker-py's containers.run(remove=True) only removes a container after it
ran. A helper whose start failed - an NFS destination refusing the mount -
stayed behind in "Created", holding the destination's volume. Every Test
click and scheduled backup added one (a user found 18), the volume could
not be deleted even with everything stopped, and changed mount options
never took effect because the volume could not be recreated with them.
- docker_client.run_container: create, start, wait, collect output and
always remove, also when the start fails; pulls a missing image. Used
for NFS destination I/O, bind-path inspection, the volume wipe before a
restore and the Trivy scanner.
- Every helper container is labelled stackpilot.helper; on startup the
stopped ones are removed, together with anything left on an NFS
destination volume by older versions.
- When an NFS destination's options change, stopped containers holding
its volume are removed before it is recreated; a running one is now a
clear error instead of silently keeping the old options.
- A refused mount explains itself: NFS enabled and reachable from the
Docker host, NFSv3 needing rpcbind and mountd, and nfsvers=4 needing
only port 2049.
- The scanner waits for Trivy's own 10-minute timeout instead of giving
up after the default client's 60 seconds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A prune that deletes a lot of data (image prune -a in particular) easily
runs past docker-py's 60s read timeout. requests' ReadTimeout is not a
DockerException, so it slipped past safe_call and the UI only showed
"Request failed with status code 500".
Prunes now use a client with a 540s timeout (below nginx's 600s
proxy_read_timeout), and safe_call turns transport timeouts and
connection errors into DockerError, so they surface as a 502 with a
message.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>