From 19c47e2768d2bebac015d018f4d32a03b47e1739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Sat, 6 Dec 2025 01:07:45 +0900 Subject: [PATCH 1/4] feat(dgw): add ARM64 Docker image support Adds native ARM64 Docker images for Devolutions Gateway, enabling deployment on ARM-based devices like Raspberry Pi and AWS Graviton instances with full native performance. Multi-arch manifests automatically select the correct image for the user's platform. --- .github/workflows/README.md | 9 +- .github/workflows/package.yml | 30 ++++++- .github/workflows/release.yml | 163 +++++++++++++++++++++++++++------- 3 files changed, 169 insertions(+), 33 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 43d339ee6..d96af3064 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -69,12 +69,19 @@ The "Release" workflow downloads the artifacts from a [Package](#package) workfl The following actions are taken: -- Build containers for Devolutions Gateway and publish to Docker +- Build multi-architecture containers for Devolutions Gateway (AMD64 and ARM64) and publish to Docker - Push the Devolutions Gateway PowerShell module to PSGallery - Generate a GitHub release Re-releasing the same version multiple times is not supported. The "Release" workflow checks for an existing GitHub release with the specified version and will not proceed if found. +##### Multi-Architecture Docker Images + +Devolutions Gateway Docker images support both AMD64 (x86_64) and ARM64 (aarch64) architectures. The workflow: +1. Prepares separate build contexts for each architecture with appropriate binaries and native libraries +2. Uses Docker Buildx and QEMU to build both architectures on x86_64 runners +3. Creates multi-arch manifest lists so users automatically get the correct image for their platform + ##### Parameters - `run` The run-id of the [Package](#package) workflow run containing the artifacts to package diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 4746b8fb9..efb0c0f74 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -135,10 +135,38 @@ jobs: echo "version=$Version" >> $Env:GITHUB_OUTPUT shell: pwsh + # Download Cadeau native libraries for all supported platforms and architectures. + # Cadeau provides the libxmf library for XMF (eXtensible Media Format) support. + # + # Organization strategy: + # We organize native libraries by platform and architecture (native-libs/{platform}/{arch}/) + # because different architectures require different compiled binaries even on the same OS. + # This structure makes it explicit which library is used for each platform+arch combination, + # preventing accidental use of wrong-architecture libraries which would cause runtime failures. + # + # Structure: + # native-libs/windows/x64/xmf.dll - Windows x64 + # native-libs/linux/x64/libxmf.so - Linux x86_64 (amd64) + # native-libs/linux/arm64/libxmf.so - Linux ARM64 (aarch64) - name: Download Cadeau run: | + # Download Windows x64 ./ci/download-cadeau.ps1 -Platform 'win' -Architecture 'x64' + $WinFiles = Get-ChildItem -Path native-libs -File + New-Item -ItemType Directory -Path native-libs/windows/x64 -Force | Out-Null + $WinFiles | Move-Item -Destination native-libs/windows/x64 + + # Download Linux x64 ./ci/download-cadeau.ps1 -Platform 'linux' -Architecture 'x64' + $LinuxX64Files = Get-ChildItem -Path native-libs -File + New-Item -ItemType Directory -Path native-libs/linux/x64 -Force | Out-Null + $LinuxX64Files | Move-Item -Destination native-libs/linux/x64 + + # Download Linux arm64 + ./ci/download-cadeau.ps1 -Platform 'linux' -Architecture 'arm64' + $LinuxArm64Files = Get-ChildItem -Path native-libs -File + New-Item -ItemType Directory -Path native-libs/linux/arm64 -Force | Out-Null + $LinuxArm64Files | Move-Item -Destination native-libs/linux/arm64 shell: pwsh - name: Upload native libs @@ -391,7 +419,7 @@ jobs: $Env:DGATEWAY_PSMODULE_PATH = Join-Path $PackageRoot PowerShell DevolutionsGateway $Env:DGATEWAY_WEBCLIENT_PATH = Join-Path "webapp" "client" | Resolve-Path $Env:DGATEWAY_WEBPLAYER_PATH = Join-Path "webapp" "player" | Resolve-Path - $Env:DGATEWAY_LIB_XMF_PATH = Join-Path "native-libs" "xmf.dll" | Resolve-Path + $Env:DGATEWAY_LIB_XMF_PATH = Join-Path "native-libs" "windows" "x64" "xmf.dll" | Resolve-Path Write-Host "DGATEWAY_EXECUTABLE = ${Env:DGATEWAY_EXECUTABLE}" Write-Host "DGATEWAY_PSMODULE_PATH = ${Env:DGATEWAY_PSMODULE_PATH}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6c6beea50..5a1ccdef8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -110,7 +110,6 @@ jobs: strategy: fail-fast: false matrix: - arch: [x86_64] os: [linux] base-image: [bookworm-slim] @@ -127,17 +126,22 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Prepare artifacts - id: prepare-artifacts + # Multi-arch Docker build strategy: + # We prepare separate build contexts for amd64 and arm64 because each architecture + # needs its own pre-compiled binary and architecture-specific native libraries (libxmf.so). + # This approach is more reliable than trying to use COPY --platform in the Dockerfile, + # which would require the binaries to be organized in a specific directory structure. + - name: Prepare artifacts (amd64) + id: prepare-artifacts-amd64 run: | Set-PSDebug -Trace 1 - $PkgDir = Join-Path docker $Env:RUNNER_OS # RUNNER_OS is camelcase - echo "package-path=$PkgDir" >> $Env:GITHUB_OUTPUT + $PkgDir = Join-Path docker $Env:RUNNER_OS "amd64" + echo "package-path-amd64=$PkgDir" >> $Env:GITHUB_OUTPUT Write-Host "PkgDir = $PkgDir" - Get-ChildItem -Path "$PkgDir" + New-Item -ItemType Directory -Path $PkgDir -Force - $SourceFileName = "DevolutionsGateway_$($Env:RUNNER_OS)_${{ needs.preflight.outputs.version }}_${{ matrix.arch }}" + $SourceFileName = "DevolutionsGateway_$($Env:RUNNER_OS)_${{ needs.preflight.outputs.version }}_x86_64" $TargetFileName = "devolutions-gateway" Write-Host "SourceFileName = $SourceFileName" Write-Host "TargetFileName = $TargetFileName" @@ -147,13 +151,10 @@ jobs: Write-Host "SourcePath = $SourcePath" Write-Host "TargetPath = $TargetPath" Copy-Item -Path $SourcePath -Destination $TargetPath - - if ($Env:RUNNER_OS -eq "Linux") { - Invoke-Expression "chmod +x $TargetPath" - } + chmod +x $TargetPath $XmfFileName = "libxmf.so" - $XmfSourcePath = Get-ChildItem -Recurse -Filter $XmfFileName -File -Path native-libs + $XmfSourcePath = Join-Path "native-libs" "linux" "x64" $XmfFileName | Resolve-Path $XmfTargetPath = Join-Path $PkgDir $XmfFileName Write-Host "XmfSourcePath = $XmfSourcePath" Write-Host "XmfTargetPath = $XmfTargetPath" @@ -170,42 +171,142 @@ jobs: $psModuleArchiveHash = (Get-FileHash -Path "$PowerShellArchive").Hash Write-Host "PS module archive hash: $psModuleArchiveHash" tar -xvf "$PowerShellArchive" -C "$PkgDir" + + # Copy Dockerfile and entrypoint + Copy-Item -Path "docker/Linux/Dockerfile" -Destination $PkgDir + Copy-Item -Path "docker/Linux/entrypoint.ps1" -Destination $PkgDir shell: pwsh - - name: Build container - id: build-container + - name: Prepare artifacts (arm64) + id: prepare-artifacts-arm64 run: | Set-PSDebug -Trace 1 - $Version = "${{ needs.preflight.outputs.version }}" - $ImageName = "devolutions/devolutions-gateway:$Version" - $LatestImageName = "devolutions/devolutions-gateway:latest" + $PkgDir = Join-Path docker $Env:RUNNER_OS "arm64" + echo "package-path-arm64=$PkgDir" >> $Env:GITHUB_OUTPUT + Write-Host "PkgDir = $PkgDir" + New-Item -ItemType Directory -Path $PkgDir -Force + + $SourceFileName = "DevolutionsGateway_$($Env:RUNNER_OS)_${{ needs.preflight.outputs.version }}_arm64" + $TargetFileName = "devolutions-gateway" + Write-Host "SourceFileName = $SourceFileName" + Write-Host "TargetFileName = $TargetFileName" + + $SourcePath = Get-ChildItem -Recurse -Filter $SourceFileName -File -Path devolutions-gateway + $TargetPath = Join-Path $PkgDir $TargetFileName + Write-Host "SourcePath = $SourcePath" + Write-Host "TargetPath = $TargetPath" + Copy-Item -Path $SourcePath -Destination $TargetPath + chmod +x $TargetPath - docker build -t "$ImageName" -t "$LatestImageName" . - echo "image-name=$ImageName" >> $Env:GITHUB_OUTPUT - echo "latest-image-name=$LatestImageName" >> $Env:GITHUB_OUTPUT + $XmfFileName = "libxmf.so" + $XmfSourcePath = Join-Path "native-libs" "linux" "arm64" $XmfFileName | Resolve-Path + $XmfTargetPath = Join-Path $PkgDir $XmfFileName + Write-Host "XmfSourcePath = $XmfSourcePath" + Write-Host "XmfTargetPath = $XmfTargetPath" + Copy-Item -Path $XmfSourcePath -Destination $XmfTargetPath - Get-ChildItem -Recurse + $WebAppArchive = Get-ChildItem -Recurse -Filter "devolutions_gateway_webapp_*.tar.gz" | Select-Object -First 1 + $TargetPath = Join-Path $PkgDir "webapp" "client" + Write-Host "WebAppArchive = $WebAppArchive" + Write-Host "TargetPath = $TargetPath" + New-Item -ItemType Directory -Path $TargetPath + tar -xvzf $WebAppArchive.FullName -C $TargetPath --strip-components=1 + + $PowerShellArchive = Get-ChildItem -Recurse -Filter "DevolutionsGateway-ps-*.tar" | Select-Object -First 1 + $psModuleArchiveHash = (Get-FileHash -Path "$PowerShellArchive").Hash + Write-Host "PS module archive hash: $psModuleArchiveHash" + tar -xvf "$PowerShellArchive" -C "$PkgDir" + + # Copy Dockerfile and entrypoint + Copy-Item -Path "docker/Linux/Dockerfile" -Destination $PkgDir + Copy-Item -Path "docker/Linux/entrypoint.ps1" -Destination $PkgDir shell: pwsh - working-directory: ${{ steps.prepare-artifacts.outputs.package-path }} - - name: Push container + # QEMU is required for cross-platform Docker builds on x86_64 runners. + # It enables the runner to emulate ARM64 architecture during the build process. + # Without QEMU, we would need native ARM64 runners (which are more expensive and less available). + # Note: QEMU is only used during the IMAGE BUILD, not at runtime - the final images are native. + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + # Docker Buildx is required for multi-platform builds and creating manifest lists. + # It provides: + # 1. The ability to build for multiple architectures in a single command + # 2. The 'docker buildx imagetools' command for creating multi-arch manifests + # 3. Better caching and build performance compared to legacy docker build + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + run: | + echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u devolutionsbot --password-stdin + shell: bash + + # Multi-arch build strategy: + # 1. Build separate images for each architecture with arch-specific tags + # 2. Push each image to the registry + # 3. Create a multi-arch manifest that references both images + # + # When users pull the image without specifying architecture, Docker automatically + # selects the correct variant based on their platform. This works because: + # - Each image is tagged with the architecture (e.g., :2025.3.3-amd64) + # - The manifest list (:2025.3.3) contains references to both arch-specific images + # - Docker client inspects the manifest and pulls the matching architecture + # + # Why we use separate contexts instead of --platform linux/amd64,linux/arm64: + # - Each architecture needs different pre-compiled binaries and native libraries + # - Separate contexts allow us to use architecture-specific artifacts directly + # - More explicit and easier to debug than complex Dockerfile conditionals + - name: Build and push multi-arch container + id: build-container run: | Set-PSDebug -Trace 1 - echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u devolutionsbot --password-stdin - $DockerPushCmd = 'docker push ${{ steps.build-container.outputs.image-name }}' - $DockerPushLatestCmd = 'docker push ${{ steps.build-container.outputs.latest-image-name }}' - Write-Host $DockerPushCmd - Write-Host $DockerPushLatestCmd + $Version = "${{ needs.preflight.outputs.version }}" + $ImageName = "devolutions/devolutions-gateway" + + $Amd64Context = "${{ steps.prepare-artifacts-amd64.outputs.package-path-amd64 }}" + $Arm64Context = "${{ steps.prepare-artifacts-arm64.outputs.package-path-arm64 }}" $DryRun = [System.Convert]::ToBoolean('${{ inputs.dry-run }}') + + # Build and push amd64 image + Write-Host "Building amd64 image..." + $BuildPushFlag = if (-Not $DryRun) { "--push" } else { "" } + docker buildx build --platform linux/amd64 ` + --tag "${ImageName}:${Version}-amd64" ` + $BuildPushFlag ` + $Amd64Context + + # Build and push arm64 image (cross-compiled on x86_64 runner using QEMU) + Write-Host "Building arm64 image..." + docker buildx build --platform linux/arm64 ` + --tag "${ImageName}:${Version}-arm64" ` + $BuildPushFlag ` + $Arm64Context + if (-Not $DryRun) { - Invoke-Expression $DockerPushCmd - Invoke-Expression $DockerPushLatestCmd + # Create multi-arch manifests that reference both architecture-specific images. + # This enables Docker to automatically select the correct image based on the user's platform. + Write-Host "Creating multi-arch manifest for version ${Version}..." + docker buildx imagetools create ` + --tag "${ImageName}:${Version}" ` + "${ImageName}:${Version}-amd64" ` + "${ImageName}:${Version}-arm64" + + Write-Host "Creating multi-arch manifest for latest..." + docker buildx imagetools create ` + --tag "${ImageName}:latest" ` + "${ImageName}:${Version}-amd64" ` + "${ImageName}:${Version}-arm64" + } else { + Write-Host "Dry run: skipping manifest creation and push" } + + echo "image-name=${ImageName}:${Version}" >> $Env:GITHUB_OUTPUT + echo "latest-image-name=${ImageName}:latest" >> $Env:GITHUB_OUTPUT shell: pwsh - working-directory: ${{ steps.prepare-artifacts.outputs.package-path }} github-release: name: GitHub release From 9f6c710a0f1f9e7a29fb85c2c94d9a3b828468e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Sat, 6 Dec 2025 01:22:13 +0900 Subject: [PATCH 2/4] . --- .github/workflows/release.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5a1ccdef8..f86d20884 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -239,9 +239,10 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub - run: | - echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u devolutionsbot --password-stdin - shell: bash + uses: docker/login-action@v3 + with: + username: devolutionsbot + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} # Multi-arch build strategy: # 1. Build separate images for each architecture with arch-specific tags From 84f71731a7b605c952a2b802f49f021cc2cad67c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Sat, 6 Dec 2025 01:25:47 +0900 Subject: [PATCH 3/4] . --- .github/workflows/release.yml | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f86d20884..4a6368ade 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -274,18 +274,29 @@ jobs: # Build and push amd64 image Write-Host "Building amd64 image..." - $BuildPushFlag = if (-Not $DryRun) { "--push" } else { "" } - docker buildx build --platform linux/amd64 ` - --tag "${ImageName}:${Version}-amd64" ` - $BuildPushFlag ` - $Amd64Context + if (-Not $DryRun) { + docker buildx build --platform linux/amd64 ` + --tag "${ImageName}:${Version}-amd64" ` + --push ` + $Amd64Context + } else { + docker buildx build --platform linux/amd64 ` + --tag "${ImageName}:${Version}-amd64" ` + $Amd64Context + } # Build and push arm64 image (cross-compiled on x86_64 runner using QEMU) Write-Host "Building arm64 image..." - docker buildx build --platform linux/arm64 ` - --tag "${ImageName}:${Version}-arm64" ` - $BuildPushFlag ` - $Arm64Context + if (-Not $DryRun) { + docker buildx build --platform linux/arm64 ` + --tag "${ImageName}:${Version}-arm64" ` + --push ` + $Arm64Context + } else { + docker buildx build --platform linux/arm64 ` + --tag "${ImageName}:${Version}-arm64" ` + $Arm64Context + } if (-Not $DryRun) { # Create multi-arch manifests that reference both architecture-specific images. From 5c64563deaaaaad8a6c6599aecce04875e1d091d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Sat, 6 Dec 2025 01:39:48 +0900 Subject: [PATCH 4/4] . --- package/Linux/Dockerfile | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/package/Linux/Dockerfile b/package/Linux/Dockerfile index 647b3a4a7..2fe122e6c 100644 --- a/package/Linux/Dockerfile +++ b/package/Linux/Dockerfile @@ -1,14 +1,26 @@ FROM debian:bookworm-slim LABEL maintainer="Devolutions Inc." +# Install PowerShell and dependencies +# Microsoft's APT repository doesn't have PowerShell for ARM64, so we install from GitHub releases RUN apt-get update \ - && apt-get install -y --no-install-recommends wget ca-certificates \ - && wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb -O packages-microsoft-prod.deb \ - && dpkg -i packages-microsoft-prod.deb \ - && rm packages-microsoft-prod.deb \ - && apt-get update \ - && apt-get install -y --no-install-recommends \ - powershell openssl \ + && apt-get install -y --no-install-recommends wget ca-certificates openssl \ + && ARCH=$(dpkg --print-architecture) \ + && if [ "$ARCH" = "arm64" ]; then \ + PWSH_VERSION=7.4.6 \ + && wget -q "https://github.com/PowerShell/PowerShell/releases/download/v${PWSH_VERSION}/powershell-${PWSH_VERSION}-linux-arm64.tar.gz" \ + && mkdir -p /opt/microsoft/powershell/7 \ + && tar -xzf "powershell-${PWSH_VERSION}-linux-arm64.tar.gz" -C /opt/microsoft/powershell/7 \ + && chmod +x /opt/microsoft/powershell/7/pwsh \ + && ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh \ + && rm "powershell-${PWSH_VERSION}-linux-arm64.tar.gz"; \ + else \ + wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb -O packages-microsoft-prod.deb \ + && dpkg -i packages-microsoft-prod.deb \ + && rm packages-microsoft-prod.deb \ + && apt-get update \ + && apt-get install -y --no-install-recommends powershell; \ + fi \ && rm -rf /var/lib/apt/lists/* ENV XDG_CACHE_HOME="/tmp/.cache"