1
\$\begingroup\$

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

  1. Is this process adequately fault-tolerant?
  2. 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?)
  3. Is it adequately portable (specifically regarding the .NET libraries I use, or something I may have overlooked)?
  4. 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.
#>
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.