Watch at EaseHistorias. Respuestas. Perspectiva.

Tecnología · GUÍA PRÁCTICA

¿El disco WSL de Docker Desktop es enorme? Mide tres capas antes de limpiar o compactar

Separa los objetos de Docker, el sistema de archivos invitado WSL y el VHDX anfitrión con una auditoría de solo lectura.

Ilustración conceptual original: tres capas de Docker, WSL y el VHDX anfitrión
Ilustración conceptual original: tres capas de Docker, WSL y el VHDX anfitrión Watch at Ease

Que docker_data.vhdx siga siendo grande después de borrar imágenes no prueba que Docker conserve la misma cantidad de objetos.

Esta guía crea un registro JSON de solo lectura; no ejecuta limpieza, borrado, apagado de WSL, conversión dispersa ni compactación.

Nombra las tres capas antes de comparar

Las cifras de objetos Docker, uso del sistema invitado y longitud del VHDX pertenecen a capas distintas. Conserva las tres antes de concluir qué está mal.

Una diferencia puede ser normal: un VHD dinámico puede seguir grande tras borrar archivos y el invitado puede llenarse por datos que el resumen de Docker no clasifica.

Conserva una línea base y la ruta exacta

Copia la ruta exacta de la imagen de disco desde la configuración actual de Docker Desktop. La auditoría exige esa ruta y no busca candidatos por el disco.

Ejecuta primero desde una terminal normal. Un daemon ausente, una distribución no disponible o una ruta invitada cambiada queda como error, no como cero.

Guarda y ejecuta la auditoría de solo lectura

Guarda el programa completo como audit_docker_wsl_disk.mjs en UTF-8 y verifica que no termine en .txt. Solo ejecuta consultas de lectura.

Ejecuta node --check audit_docker_wsl_disk.mjs y después node audit_docker_wsl_disk.mjs "C:\ruta\exacta\docker_data.vhdx" > docker-wsl-disk-before.json.

import { spawnSync } from 'node:child_process'; import { stat, statfs } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const clip = (value, limit = 200000) => String(value ?? '').slice(0, limit); export function runCommand(file, args, spawn = spawnSync) { const result = spawn(file, args, { encoding: 'utf8', timeout: 30000, windowsHide: true, shell: false, maxBuffer: 1024 * 1024, }); return { command: [file, ...args], mutating: false, status: Number.isInteger(result.status) ? result.status : null, signal: result.signal ?? null, stdout: clip(result.stdout), stderr: clip(result.stderr), error: result.error ? String(result.error.code || result.error.message) : null, }; } export async function buildAudit({ vhdxPath, distro = process.env.DOCKER_WSL_DISTRO || 'docker-desktop', guestPath = process.env.DOCKER_WSL_MOUNT || '/tmp/docker-desktop-root/mnt/docker-desktop-disk', platform = process.platform, fsStat = stat, fsStatfs = statfs, spawn = spawnSync, } = {}) { if (!vhdxPath || typeof vhdxPath !== 'string') throw new Error('VHDX_PATH_REQUIRED'); if (!/^[A-Za-z0-9._-]+$/.test(distro)) throw new Error('INVALID_DISTRO_NAME'); if (!guestPath.startsWith('/') || guestPath.includes('\0')) throw new Error('INVALID_GUEST_PATH'); const exactPath = resolve(vhdxPath); const [file, hostFs] = await Promise.all([fsStat(exactPath), fsStatfs(dirname(exactPath))]); if (!file.isFile()) throw new Error('VHDX_NOT_A_FILE'); const command = (fileName, args) => runCommand(fileName, args, spawn); const commands = { dockerVersion: command('docker', ['version', '--format', '{{json .}}']), dockerObjects: command('docker', ['system', 'df', '-v']), wslStatus: command('wsl.exe', ['--status']), wslDistributions: command('wsl.exe', ['--list', '--verbose']), guestFilesystem: command('wsl.exe', ['-d', distro, '--', 'df', '-B1', guestPath]), }; return { schemaVersion: 1, capturedAt: new Date().toISOString(), scope: 'read-only Docker Desktop / WSL disk evidence', safety: { mutatingCommandsRun: false, notRun: ['docker system prune', 'docker * prune', 'wsl --shutdown', 'diskpart compact vdisk', 'Optimize-VHD', 'wsl --manage --set-sparse', 'file deletion'], }, host: { platform, vhdxPath: exactPath, fileLengthBytes: Number(file.size), containingFilesystem: { blockSize: Number(hostFs.bsize), blocks: Number(hostFs.blocks), blocksFree: Number(hostFs.bfree), blocksAvailable: Number(hostFs.bavail), }, limitation: 'fileLengthBytes is the file logical length. This audit does not claim to measure NTFS allocated clusters or reclaimable physical bytes.', }, docker: { version: commands.dockerVersion, objects: commands.dockerObjects, limitation: 'docker system df reports Docker-managed objects; it is not the Windows VHDX file size and can omit other guest-side use.', }, wsl: { distro, guestPath, status: commands.wslStatus, distributions: commands.wslDistributions, guestFilesystem: commands.guestFilesystem, limitation: 'A failed guest-path check is an unresolved measurement, not proof that the filesystem is empty.', }, interpretationOrder: [ 'Preserve this file before changing anything.', 'Use Docker object output to identify Docker-managed and potentially reclaimable categories.', 'Use guest df output to decide whether the WSL filesystem itself is near full.', 'Compare those readings with the host VHDX logical length and host filesystem free space.', 'Choose a change only after the mismatched layer is identified; rerun the same audit afterward under a new filename.', ], }; } async function main() { if (process.argv.length !== 3) throw new Error('USAGE: node audit_docker_wsl_disk.mjs <exact-vhdx-path>'); const report = await buildAudit({vhdxPath: process.argv[2]}); process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); } if (import.meta.url === pathToFileURL(process.argv[1] || '').href) { main().catch(error => { process.stderr.write(`Audit failed without changing Docker or WSL: ${error.code || error.message}\n`); process.exitCode = 1; }); }

SHA-256 del programa: d43b227f591fff111404afb1505ab5552965d8095eb907d60aa9c680c12785bb. Pasó cinco pruebas sintéticas con comandos simulados; no se probó un Windows o Docker Desktop real.

Lee primero la salida de objetos Docker

docker system df -v separa imágenes, contenedores, volúmenes y caché. No supongas que cada byte del VHDX es recuperable.

Si aparece una categoría prescindible, revisa el alcance, filtros y advertencia del comando específico en un plan aparte.

Comprueba si el invitado WSL está realmente lleno

La fila df muestra total, usado y disponible del montaje configurado. La capacidad interna no equivale al espacio libre del host.

Si el invitado usa mucho y Docker informa poco, investiga consumidores internos, incluidos registros, antes de compactar.

Interpreta con cuidado la longitud del VHDX

El informe registra la longitud lógica del VHDX y el espacio libre del sistema anfitrión. No mide clústeres NTFS asignados ni bytes físicamente recuperables.

Microsoft documenta que un VHD dinámico crece pero no se reduce solo; compactarlo exige que esté separado o en solo lectura.

Elige la rama mínima justificada

Objetos prescindibles, invitado lleno y archivo anfitrión grande son ramas distintas. Elige solo la apoyada por las mediciones.

Si falla una capa o las rutas no coinciden, detente. No desregistres WSL, purgues datos ni compactes solo para hacer coincidir cifras.

Repite la misma auditoría tras un solo cambio aprobado

Tras un único cambio aprobado, guarda un segundo informe y compara las cuatro medidas. Conserva ambos archivos y la acción intermedia.

Una cifra menor no demuestra que las cargas estén intactas. Comprueba contenedores, volúmenes y datos de aplicación por separado.

Ilustración conceptual original: elige la rama mínima según la capa medida y detente si falta evidencia
Ilustración conceptual original: elige la rama mínima según la capa medida y detente si falta evidencia Watch at Ease