Problem
Developers in my team are new to Docker. I wanted to provide a way to accomplish general Docker housekeeping tasks (like pruning unused images and compacting the Hyper-V volume) largely automatically.
Solution
The PowerShell script below is designed to be run either interactively or as a service. When run non-interactively, progress is logged to the Windows Event Log, otherwise progress is logged to the console.
Additionally, we provide a number of pre-built base images in our internal registry HQ-DOCKER-
. These provide server software whose details the developers don't need to know. So, point releases and upstream base OS patches are built into images of the same tag name. These also need to be pulled at semi-regular intervals (for those who ignore automated build announcements we already have in place).
Review Goals
- Is this process adequately fault-tolerant?
- Is it overly complicated? (Would it be better broken out into two scripts: one that's always non-interactive and one that can be run interactively?)
- Is it adequately portable (specifically regarding the .NET libraries I use, or something I may have overlooked)?
- Are there any glaring non-idiomatic-PowerShell issues?
As always, further comments beyond the scope of the 4 above points are more than welcome. Similarly, commentary about this post are welcome.
Code
#Requires -RunAsAdministrator
#Requires -Modules Hyper-V
param(
[Switch]$FullSystem,
[Switch]$Pull,
[String]$VHDX
)
if ([System.Diagnostics.EventLog]::SourceExists("DockerMaintenance") -eq $False) {
New-EventLog -LogName Application -Source DockerMaintenance
}
function Write-ErrorLog($Message) {
if (!$MyInvocation.scriptname) {
Write-EventLog -LogName Application `
-Source DockerMaintenance `
-EventID 3001 `
-Category 1 `
-EntryType Error `
-Message $Message
} else {
Write-Host -ForegroundColor Red $Message
}
}
function Write-InfoLog($Message) {
if (!$MyInvocation.scriptname) {
Write-EventLog -LogName Application `
-Source DockerMaintenance `
-EventID 3001 `
-Category 1 `
-EntryType Information `
-Message $Message
} else {
Write-Host $Message
}
}
if ($null -eq $VHDX) {
$err = "The -VHDX parameter is missing! Please add it."
Write-ErrorLog $err
Exit -1
} elseif ((![System.IO.File]::Exists("${VHDX}")) -or ($VHDX -notlike "*\DockerDesktop.vhdx")){
$err = "The path provided for -VHDX ""${VHDX}"" does not point to a 'DockerDesktop.vhdx'!"
Write-ErrorLog $err
Exit -2
}
function Invoke-Toast($Message, $Cancel) {
$AppId = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe'
$XmlString = @"
<toast>
<visual>
<binding template="ToastGeneric">
<text hint-maxLines="1">Docker Maintenance</text>
<text>$Message</text>
</binding>
</visual>
<audio src="ms-winsoundevent:Notification.Default" />
</toast>
"@
$ToastXml = [Windows.Data.Xml.Dom.XmlDocument]::new()
$ToastXml.LoadXml($XmlString)
$Toast = [Windows.UI.Notifications.ToastNotification]::new($ToastXml)
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($AppId).Show($Toast)
}
function Invoke-ImagePrune() {
Write-InfoLog "Pruning unused and dangling images"
docker image prune -f
}
function Invoke-SystemPrune() {
Write-InfoLog "Fully pruning docker system"
docker system prune -f
}
function Invoke-DockerPull() {
Write-InfoLog "Pulling HQ docker images"
docker images --format "{{.Repository}}:{{.Tag}}" | Where-Object { $_ -like "hq-docker-*" } | ForEach-Object { docker pull $_ }
}
function Invoke-CompactDocker() {
Write-InfoLog "Compacting ${VHDX}"
Mount-VHD $VHDX -ReadOnly
Optimize-VHD $VHDX -Mode Full
Dismount-VHD $VHDX
}
function Stop-Docker() {
$docker = Get-VM | Where-Object { $_.Name -eq 'DockerDesktopVM' }
if ($docker.State -ne "Off") {
Write-InfoLog "Docker is still running. Stopping..."
Get-VM | Where-Object { $_.Name -eq 'DockerDesktopVM' } | Stop-VM
Get-Process -Name "*Docker*" | Stop-Process -Force
}
}
function Start-Docker() {
Write-InfoLog "Restarting docker"
Net Start "com.docker.service"
& "C:\Program Files\Docker\Docker\Docker Desktop.exe"
if ($MyInvocation.scriptname) {
Write-Host "Docker for Windows is now starting up. Watch for the usual Toast notification."
}
}
if ($MyInvocation.scriptname) {
Invoke-Toast "Your Docker for Windows will be temporarily shut down for maintenance in 5 MINUTES. `
This process should take about 3 minutes."
Start-Sleep -Seconds (60*5)
} else {
Write-Host -ForegroundColor Green "Beginning maintenance in 5 seconds. Abort with [CTRL-C]."
Start-Sleep -Seconds 5
}
if ($True -eq $Pull) {
Invoke-DockerPull
}
if ($True -eq $FullSystem) {
Invoke-SystemPrune
} else {
Invoke-ImagePrune
}
Stop-Docker
Invoke-CompactDocker
Write-InfoLog "Maintenance Complete, restarting Docker..."
Start-Docker
Doc Comment
At the very top of the file, I extracted it to make the code more concise but include it here for completeness.
<#
.Synopsis
Runs periodic maintenance on Docker for Windows.
It must be run as an Administrator!
.Description
Intended to be run interactively or as a periodoc service, this will execute configurable docker prune
within the VM and then stop and compact the virtual machine hard disk before restarting Docker.
When run as a service, logs are sent to the Windows Event Log. Otherwise, progress is logged to the console.
.Parameter VHDX
Required. The complete path to your DockerDesktop virtual hard disk file, i.e., 'F:\Docker\DockerDesktop.vhdx'
.Parameter FullSystem
Optional. By default docker will prune unused images with 'docker image prune'.
Add this flag to execute 'docker system prune -f'.
.Parameter Pull
Optional (but recommended). Pull rebuilt base images from the internal HQ-DOCKER registry.
.Example
periodic-maintenance.ps1 -VHDX "F:\Docker\DockerDesktop.vhdx" -FullSystem -Pull
# Interactively run a full system prune, pull the latest internal base images, compact the VHDX.
.Example
periodic-maintenance.ps1 -VHDX "F:\Docker\DockerDesktop.vhdx"
# Run a minimal image prune and compact the VHDX.
#>