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
+4
View File
@@ -438,6 +438,10 @@ export function pruneImages(): Promise<{ images_removed: number; space_reclaimed
return post<{ images_removed: number; space_reclaimed_mb: number }>('/api/docker/prune-images');
}
export function pruneBuildCache(): Promise<{ caches_deleted: number; space_reclaimed_mb: number }> {
return post<{ caches_deleted: number; space_reclaimed_mb: number }>('/api/docker/prune-build-cache');
}
export function testNpmConnection(data: { npm_url?: string; npm_email?: string; npm_password?: string }): Promise<{ status: string }> {
return post<{ status: string }>('/api/settings/npm/test', data);
}
+6
View File
@@ -123,6 +123,12 @@
"pruneResult": "Removed {count} images, reclaimed {mb} MB",
"pruneConfirmMessage": "This will remove unused Docker images belonging to your projects. Images used by active instances will not be affected.",
"pruneFailed": "Failed to prune images",
"buildCacheCleanupHelp": "Reclaim disk used by Docker build cache from image and static-site builds. Only unused cache is removed — apps redeploying still hit any cache that is still in use.",
"pruneBuildCache": "Prune Build Cache",
"pruningCache": "Pruning...",
"pruneCacheResult": "Removed {count} build-cache records, reclaimed {mb} MB",
"pruneBuildCacheConfirmMessage": "This removes unused Docker build-cache layers daemon-wide. Layers still in use are kept, so apps that redeploy soon keep their warm cache; others rebuild from scratch on their next deploy.",
"pruneCacheFailed": "Failed to prune build cache",
"proxyProvider": "Proxy Provider",
"proxyProviderHelp": "Select how reverse proxy routes are managed for deployed containers.",
"proxyNone": "None",
+6
View File
@@ -123,6 +123,12 @@
"pruneResult": "Удалено {count} образов, освобождено {mb} МБ",
"pruneConfirmMessage": "Будут удалены неиспользуемые Docker-образы ваших проектов. Образы активных экземпляров не затрагиваются.",
"pruneFailed": "Не удалось очистить образы",
"buildCacheCleanupHelp": "Освобождение места на диске, занятого кэшем сборки Docker (сборки образов и статических сайтов). Удаляется только неиспользуемый кэш — при следующем развёртывании приложения используют кэш, который ещё задействован.",
"pruneBuildCache": "Очистить кэш сборки",
"pruningCache": "Очистка...",
"pruneCacheResult": "Удалено записей кэша сборки: {count}, освобождено {mb} МБ",
"pruneBuildCacheConfirmMessage": "Будут удалены неиспользуемые слои кэша сборки Docker во всём демоне. Используемые слои сохраняются, поэтому приложения, которые скоро переразвернутся, сохранят тёплый кэш; остальные пересоберутся с нуля при следующем развёртывании.",
"pruneCacheFailed": "Не удалось очистить кэш сборки",
"proxyProvider": "Провайдер прокси",
"proxyProviderHelp": "Выберите способ управления обратным прокси для развёрнутых контейнеров.",
"proxyNone": "Нет",
@@ -6,7 +6,7 @@
never within casual miss-click distance of general form fields.
-->
<script lang="ts">
import { getSettings, updateSettings, pruneImages } from '$lib/api';
import { getSettings, updateSettings, pruneImages, pruneBuildCache } from '$lib/api';
import FormField from '$lib/components/FormField.svelte';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import Skeleton from '$lib/components/Skeleton.svelte';
@@ -18,6 +18,8 @@
let saving = $state(false);
let pruning = $state(false);
let showPruneConfirm = $state(false);
let pruningCache = $state(false);
let showPruneCacheConfirm = $state(false);
let staleThresholdDays = $state('7');
let imagePruneThresholdMb = $state('1024');
@@ -77,6 +79,18 @@
}
}
async function handlePruneBuildCache() {
pruningCache = true;
try {
const result = await pruneBuildCache();
toasts.success($t('settings.pruneCacheResult', { count: String(result.caches_deleted), mb: String(result.space_reclaimed_mb) }));
} catch (err) {
toasts.error(err instanceof Error ? err.message : $t('settings.pruneCacheFailed'));
} finally {
pruningCache = false;
}
}
$effect(() => { load(); });
</script>
@@ -165,6 +179,26 @@
{/if}
</button>
</div>
<hr class="my-5 border-[var(--color-danger)]/20" />
<p class="text-sm text-[var(--text-secondary)]">{$t('settings.buildCacheCleanupHelp')}</p>
<div class="mt-4">
<button
type="button"
onclick={() => { showPruneCacheConfirm = true; }}
disabled={pruningCache}
class="inline-flex items-center gap-2 rounded-lg border border-[var(--color-danger)] bg-[var(--surface-card)] px-4 py-2 text-sm font-medium text-[var(--color-danger)] hover:bg-[var(--color-danger)] hover:text-white disabled:opacity-50 transition-colors"
>
{#if pruningCache}
<IconLoader size={16} />
{$t('settings.pruningCache')}
{:else}
{$t('settings.pruneBuildCache')}
{/if}
</button>
</div>
</div>
</div>
</div>
@@ -180,3 +214,13 @@
onconfirm={() => { showPruneConfirm = false; handlePruneImages(); }}
oncancel={() => { showPruneConfirm = false; }}
/>
<ConfirmDialog
open={showPruneCacheConfirm}
title={$t('settings.pruneBuildCache')}
message={$t('settings.pruneBuildCacheConfirmMessage')}
confirmLabel={$t('settings.pruneBuildCache')}
confirmVariant="danger"
onconfirm={() => { showPruneCacheConfirm = false; handlePruneBuildCache(); }}
oncancel={() => { showPruneCacheConfirm = false; }}
/>