feat(maintenance): add Docker build-cache prune action

Add an admin-only POST /api/docker/prune-build-cache endpoint plus a Settings > Maintenance danger-zone button to reclaim disk used by the Docker build cache (image + static-site builds), which previously grew unbounded with no UI lever. Prunes unused-only (all=false) so a warm cache is preserved for apps redeploying soon. Mirrors the existing prune-images vertical slice; full en/ru i18n parity.
This commit is contained in:
2026-06-02 13:34:05 +03:00
parent 15e5b186cd
commit 97f338fba3
7 changed files with 117 additions and 1 deletions
+29
View File
@@ -348,3 +348,32 @@ func (s *Server) pruneImages(w http.ResponseWriter, r *http.Request) {
"space_reclaimed_mb": reclaimedBytes / (1024 * 1024),
})
}
// pruneBuildCache handles POST /api/docker/prune-build-cache. It removes
// unused Docker build-cache records daemon-wide (all=false), so an app's next
// rebuild still hits its warm cache. The build cache is regenerable by
// definition — pruning only forces slower rebuilds, never data loss — and the
// dockerfile/static deploy paths never reclaim it on teardown, so it grows
// monotonically until pruned here.
func (s *Server) pruneBuildCache(w http.ResponseWriter, r *http.Request) {
if s.docker == nil {
respondError(w, http.StatusServiceUnavailable, "Docker is not available")
return
}
result, err := s.docker.PruneBuildCache(r.Context(), false)
if err != nil {
slog.Error("prune: build cache", "error", err)
respondError(w, http.StatusInternalServerError, "internal server error")
return
}
slog.Info("prune: build cache",
"caches_deleted", result.CachesDeleted,
"space_reclaimed_mb", result.SpaceReclaimed/(1024*1024))
respondJSON(w, http.StatusOK, map[string]any{
"caches_deleted": result.CachesDeleted,
"space_reclaimed_mb": result.SpaceReclaimed / (1024 * 1024),
})
}
+1
View File
@@ -501,6 +501,7 @@ func (s *Server) Router() chi.Router {
// Docker management.
r.Post("/docker/prune-images", s.pruneImages)
r.Post("/docker/prune-build-cache", s.pruneBuildCache)
// NPM connection test.
r.Post("/settings/npm/test", s.testNpmConnection)
+26
View File
@@ -108,3 +108,29 @@ func (c *Client) GetSystemStats(ctx context.Context) (SystemStats, error) {
return stats, nil
}
// BuildCachePruneResult reports the outcome of a build-cache prune.
type BuildCachePruneResult struct {
CachesDeleted int `json:"caches_deleted"` // number of cache records removed
SpaceReclaimed int64 `json:"space_reclaimed"` // bytes reclaimed
}
// PruneBuildCache deletes unused Docker build-cache records and returns the
// number of records removed and bytes reclaimed. Docker's build-cache API is
// prune-by-filter only — there is no surgical per-record eviction — so this
// is the daemon-wide "prune unused" operation.
//
// When all is false (the default), only build cache not currently in use is
// removed, so an app's next rebuild still hits its warm cache. When all is
// true, every build-cache record is removed regardless of use, forcing a cold
// rebuild for every app.
func (c *Client) PruneBuildCache(ctx context.Context, all bool) (BuildCachePruneResult, error) {
res, err := c.api.BuildCachePrune(ctx, client.BuildCachePruneOptions{All: all})
if err != nil {
return BuildCachePruneResult{}, fmt.Errorf("prune build cache: %w", err)
}
return BuildCachePruneResult{
CachesDeleted: len(res.Report.CachesDeleted),
SpaceReclaimed: int64(res.Report.SpaceReclaimed),
}, nil
}