Skip to content

Commit b2f5172

Browse files
authored
build(dgw): add ARM64 Docker image support (#1607)
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. Issue: DGW-325
1 parent 4a11893 commit b2f5172

File tree

4 files changed

+200
-40
lines changed

4 files changed

+200
-40
lines changed

.github/workflows/README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,19 @@ The "Release" workflow downloads the artifacts from a [Package](#package) workfl
6969

7070
The following actions are taken:
7171

72-
- Build containers for Devolutions Gateway and publish to Docker
72+
- Build multi-architecture containers for Devolutions Gateway (AMD64 and ARM64) and publish to Docker
7373
- Push the Devolutions Gateway PowerShell module to PSGallery
7474
- Generate a GitHub release
7575

7676
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.
7777

78+
##### Multi-Architecture Docker Images
79+
80+
Devolutions Gateway Docker images support both AMD64 (x86_64) and ARM64 (aarch64) architectures. The workflow:
81+
1. Prepares separate build contexts for each architecture with appropriate binaries and native libraries
82+
2. Uses Docker Buildx and QEMU to build both architectures on x86_64 runners
83+
3. Creates multi-arch manifest lists so users automatically get the correct image for their platform
84+
7885
##### Parameters
7986

8087
- `run` The run-id of the [Package](#package) workflow run containing the artifacts to package

.github/workflows/package.yml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,38 @@ jobs:
135135
echo "version=$Version" >> $Env:GITHUB_OUTPUT
136136
shell: pwsh
137137

138+
# Download Cadeau native libraries for all supported platforms and architectures.
139+
# Cadeau provides the libxmf library for XMF (eXtensible Media Format) support.
140+
#
141+
# Organization strategy:
142+
# We organize native libraries by platform and architecture (native-libs/{platform}/{arch}/)
143+
# because different architectures require different compiled binaries even on the same OS.
144+
# This structure makes it explicit which library is used for each platform+arch combination,
145+
# preventing accidental use of wrong-architecture libraries which would cause runtime failures.
146+
#
147+
# Structure:
148+
# native-libs/windows/x64/xmf.dll - Windows x64
149+
# native-libs/linux/x64/libxmf.so - Linux x86_64 (amd64)
150+
# native-libs/linux/arm64/libxmf.so - Linux ARM64 (aarch64)
138151
- name: Download Cadeau
139152
run: |
153+
# Download Windows x64
140154
./ci/download-cadeau.ps1 -Platform 'win' -Architecture 'x64'
155+
$WinFiles = Get-ChildItem -Path native-libs -File
156+
New-Item -ItemType Directory -Path native-libs/windows/x64 -Force | Out-Null
157+
$WinFiles | Move-Item -Destination native-libs/windows/x64
158+
159+
# Download Linux x64
141160
./ci/download-cadeau.ps1 -Platform 'linux' -Architecture 'x64'
161+
$LinuxX64Files = Get-ChildItem -Path native-libs -File
162+
New-Item -ItemType Directory -Path native-libs/linux/x64 -Force | Out-Null
163+
$LinuxX64Files | Move-Item -Destination native-libs/linux/x64
164+
165+
# Download Linux arm64
166+
./ci/download-cadeau.ps1 -Platform 'linux' -Architecture 'arm64'
167+
$LinuxArm64Files = Get-ChildItem -Path native-libs -File
168+
New-Item -ItemType Directory -Path native-libs/linux/arm64 -Force | Out-Null
169+
$LinuxArm64Files | Move-Item -Destination native-libs/linux/arm64
142170
shell: pwsh
143171

144172
- name: Upload native libs
@@ -391,7 +419,7 @@ jobs:
391419
$Env:DGATEWAY_PSMODULE_PATH = Join-Path $PackageRoot PowerShell DevolutionsGateway
392420
$Env:DGATEWAY_WEBCLIENT_PATH = Join-Path "webapp" "client" | Resolve-Path
393421
$Env:DGATEWAY_WEBPLAYER_PATH = Join-Path "webapp" "player" | Resolve-Path
394-
$Env:DGATEWAY_LIB_XMF_PATH = Join-Path "native-libs" "xmf.dll" | Resolve-Path
422+
$Env:DGATEWAY_LIB_XMF_PATH = Join-Path "native-libs" "windows" "x64" "xmf.dll" | Resolve-Path
395423
396424
Write-Host "DGATEWAY_EXECUTABLE = ${Env:DGATEWAY_EXECUTABLE}"
397425
Write-Host "DGATEWAY_PSMODULE_PATH = ${Env:DGATEWAY_PSMODULE_PATH}"

.github/workflows/release.yml

Lines changed: 144 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ jobs:
110110
strategy:
111111
fail-fast: false
112112
matrix:
113-
arch: [x86_64]
114113
os: [linux]
115114
base-image: [bookworm-slim]
116115

@@ -127,17 +126,22 @@ jobs:
127126
env:
128127
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
129128

130-
- name: Prepare artifacts
131-
id: prepare-artifacts
129+
# Multi-arch Docker build strategy:
130+
# We prepare separate build contexts for amd64 and arm64 because each architecture
131+
# needs its own pre-compiled binary and architecture-specific native libraries (libxmf.so).
132+
# This approach is more reliable than trying to use COPY --platform in the Dockerfile,
133+
# which would require the binaries to be organized in a specific directory structure.
134+
- name: Prepare artifacts (amd64)
135+
id: prepare-artifacts-amd64
132136
run: |
133137
Set-PSDebug -Trace 1
134138
135-
$PkgDir = Join-Path docker $Env:RUNNER_OS # RUNNER_OS is camelcase
136-
echo "package-path=$PkgDir" >> $Env:GITHUB_OUTPUT
139+
$PkgDir = Join-Path docker $Env:RUNNER_OS "amd64"
140+
echo "package-path-amd64=$PkgDir" >> $Env:GITHUB_OUTPUT
137141
Write-Host "PkgDir = $PkgDir"
138-
Get-ChildItem -Path "$PkgDir"
142+
New-Item -ItemType Directory -Path $PkgDir -Force
139143
140-
$SourceFileName = "DevolutionsGateway_$($Env:RUNNER_OS)_${{ needs.preflight.outputs.version }}_${{ matrix.arch }}"
144+
$SourceFileName = "DevolutionsGateway_$($Env:RUNNER_OS)_${{ needs.preflight.outputs.version }}_x86_64"
141145
$TargetFileName = "devolutions-gateway"
142146
Write-Host "SourceFileName = $SourceFileName"
143147
Write-Host "TargetFileName = $TargetFileName"
@@ -147,13 +151,10 @@ jobs:
147151
Write-Host "SourcePath = $SourcePath"
148152
Write-Host "TargetPath = $TargetPath"
149153
Copy-Item -Path $SourcePath -Destination $TargetPath
150-
151-
if ($Env:RUNNER_OS -eq "Linux") {
152-
Invoke-Expression "chmod +x $TargetPath"
153-
}
154+
chmod +x $TargetPath
154155
155156
$XmfFileName = "libxmf.so"
156-
$XmfSourcePath = Get-ChildItem -Recurse -Filter $XmfFileName -File -Path native-libs
157+
$XmfSourcePath = Join-Path "native-libs" "linux" "x64" $XmfFileName | Resolve-Path
157158
$XmfTargetPath = Join-Path $PkgDir $XmfFileName
158159
Write-Host "XmfSourcePath = $XmfSourcePath"
159160
Write-Host "XmfTargetPath = $XmfTargetPath"
@@ -170,42 +171,154 @@ jobs:
170171
$psModuleArchiveHash = (Get-FileHash -Path "$PowerShellArchive").Hash
171172
Write-Host "PS module archive hash: $psModuleArchiveHash"
172173
tar -xvf "$PowerShellArchive" -C "$PkgDir"
174+
175+
# Copy Dockerfile and entrypoint
176+
Copy-Item -Path "docker/Linux/Dockerfile" -Destination $PkgDir
177+
Copy-Item -Path "docker/Linux/entrypoint.ps1" -Destination $PkgDir
173178
shell: pwsh
174179

175-
- name: Build container
176-
id: build-container
180+
- name: Prepare artifacts (arm64)
181+
id: prepare-artifacts-arm64
177182
run: |
178183
Set-PSDebug -Trace 1
179184
180-
$Version = "${{ needs.preflight.outputs.version }}"
181-
$ImageName = "devolutions/devolutions-gateway:$Version"
182-
$LatestImageName = "devolutions/devolutions-gateway:latest"
185+
$PkgDir = Join-Path docker $Env:RUNNER_OS "arm64"
186+
echo "package-path-arm64=$PkgDir" >> $Env:GITHUB_OUTPUT
187+
Write-Host "PkgDir = $PkgDir"
188+
New-Item -ItemType Directory -Path $PkgDir -Force
189+
190+
$SourceFileName = "DevolutionsGateway_$($Env:RUNNER_OS)_${{ needs.preflight.outputs.version }}_arm64"
191+
$TargetFileName = "devolutions-gateway"
192+
Write-Host "SourceFileName = $SourceFileName"
193+
Write-Host "TargetFileName = $TargetFileName"
194+
195+
$SourcePath = Get-ChildItem -Recurse -Filter $SourceFileName -File -Path devolutions-gateway
196+
$TargetPath = Join-Path $PkgDir $TargetFileName
197+
Write-Host "SourcePath = $SourcePath"
198+
Write-Host "TargetPath = $TargetPath"
199+
Copy-Item -Path $SourcePath -Destination $TargetPath
200+
chmod +x $TargetPath
201+
202+
$XmfFileName = "libxmf.so"
203+
$XmfSourcePath = Join-Path "native-libs" "linux" "arm64" $XmfFileName | Resolve-Path
204+
$XmfTargetPath = Join-Path $PkgDir $XmfFileName
205+
Write-Host "XmfSourcePath = $XmfSourcePath"
206+
Write-Host "XmfTargetPath = $XmfTargetPath"
207+
Copy-Item -Path $XmfSourcePath -Destination $XmfTargetPath
208+
209+
$WebAppArchive = Get-ChildItem -Recurse -Filter "devolutions_gateway_webapp_*.tar.gz" | Select-Object -First 1
210+
$TargetPath = Join-Path $PkgDir "webapp" "client"
211+
Write-Host "WebAppArchive = $WebAppArchive"
212+
Write-Host "TargetPath = $TargetPath"
213+
New-Item -ItemType Directory -Path $TargetPath
214+
tar -xvzf $WebAppArchive.FullName -C $TargetPath --strip-components=1
183215
184-
docker build -t "$ImageName" -t "$LatestImageName" .
185-
echo "image-name=$ImageName" >> $Env:GITHUB_OUTPUT
186-
echo "latest-image-name=$LatestImageName" >> $Env:GITHUB_OUTPUT
216+
$PowerShellArchive = Get-ChildItem -Recurse -Filter "DevolutionsGateway-ps-*.tar" | Select-Object -First 1
217+
$psModuleArchiveHash = (Get-FileHash -Path "$PowerShellArchive").Hash
218+
Write-Host "PS module archive hash: $psModuleArchiveHash"
219+
tar -xvf "$PowerShellArchive" -C "$PkgDir"
187220
188-
Get-ChildItem -Recurse
221+
# Copy Dockerfile and entrypoint
222+
Copy-Item -Path "docker/Linux/Dockerfile" -Destination $PkgDir
223+
Copy-Item -Path "docker/Linux/entrypoint.ps1" -Destination $PkgDir
189224
shell: pwsh
190-
working-directory: ${{ steps.prepare-artifacts.outputs.package-path }}
191225

192-
- name: Push container
226+
# QEMU is required for cross-platform Docker builds on x86_64 runners.
227+
# It enables the runner to emulate ARM64 architecture during the build process.
228+
# Without QEMU, we would need native ARM64 runners (which are more expensive and less available).
229+
# Note: QEMU is only used during the IMAGE BUILD, not at runtime - the final images are native.
230+
- name: Set up QEMU
231+
uses: docker/setup-qemu-action@v3
232+
233+
# Docker Buildx is required for multi-platform builds and creating manifest lists.
234+
# It provides:
235+
# 1. The ability to build for multiple architectures in a single command
236+
# 2. The 'docker buildx imagetools' command for creating multi-arch manifests
237+
# 3. Better caching and build performance compared to legacy docker build
238+
- name: Set up Docker Buildx
239+
uses: docker/setup-buildx-action@v3
240+
241+
- name: Login to Docker Hub
242+
uses: docker/login-action@v3
243+
with:
244+
username: devolutionsbot
245+
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
246+
247+
# Multi-arch build strategy:
248+
# 1. Build separate images for each architecture with arch-specific tags
249+
# 2. Push each image to the registry
250+
# 3. Create a multi-arch manifest that references both images
251+
#
252+
# When users pull the image without specifying architecture, Docker automatically
253+
# selects the correct variant based on their platform. This works because:
254+
# - Each image is tagged with the architecture (e.g., :2025.3.3-amd64)
255+
# - The manifest list (:2025.3.3) contains references to both arch-specific images
256+
# - Docker client inspects the manifest and pulls the matching architecture
257+
#
258+
# Why we use separate contexts instead of --platform linux/amd64,linux/arm64:
259+
# - Each architecture needs different pre-compiled binaries and native libraries
260+
# - Separate contexts allow us to use architecture-specific artifacts directly
261+
# - More explicit and easier to debug than complex Dockerfile conditionals
262+
- name: Build and push multi-arch container
263+
id: build-container
193264
run: |
194265
Set-PSDebug -Trace 1
195266
196-
echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u devolutionsbot --password-stdin
197-
$DockerPushCmd = 'docker push ${{ steps.build-container.outputs.image-name }}'
198-
$DockerPushLatestCmd = 'docker push ${{ steps.build-container.outputs.latest-image-name }}'
199-
Write-Host $DockerPushCmd
200-
Write-Host $DockerPushLatestCmd
267+
$Version = "${{ needs.preflight.outputs.version }}"
268+
$ImageName = "devolutions/devolutions-gateway"
269+
270+
$Amd64Context = "${{ steps.prepare-artifacts-amd64.outputs.package-path-amd64 }}"
271+
$Arm64Context = "${{ steps.prepare-artifacts-arm64.outputs.package-path-arm64 }}"
201272
202273
$DryRun = [System.Convert]::ToBoolean('${{ inputs.dry-run }}')
274+
275+
# Build and push amd64 image
276+
Write-Host "Building amd64 image..."
277+
if (-Not $DryRun) {
278+
docker buildx build --platform linux/amd64 `
279+
--tag "${ImageName}:${Version}-amd64" `
280+
--push `
281+
$Amd64Context
282+
} else {
283+
docker buildx build --platform linux/amd64 `
284+
--tag "${ImageName}:${Version}-amd64" `
285+
$Amd64Context
286+
}
287+
288+
# Build and push arm64 image (cross-compiled on x86_64 runner using QEMU)
289+
Write-Host "Building arm64 image..."
203290
if (-Not $DryRun) {
204-
Invoke-Expression $DockerPushCmd
205-
Invoke-Expression $DockerPushLatestCmd
291+
docker buildx build --platform linux/arm64 `
292+
--tag "${ImageName}:${Version}-arm64" `
293+
--push `
294+
$Arm64Context
295+
} else {
296+
docker buildx build --platform linux/arm64 `
297+
--tag "${ImageName}:${Version}-arm64" `
298+
$Arm64Context
299+
}
300+
301+
if (-Not $DryRun) {
302+
# Create multi-arch manifests that reference both architecture-specific images.
303+
# This enables Docker to automatically select the correct image based on the user's platform.
304+
Write-Host "Creating multi-arch manifest for version ${Version}..."
305+
docker buildx imagetools create `
306+
--tag "${ImageName}:${Version}" `
307+
"${ImageName}:${Version}-amd64" `
308+
"${ImageName}:${Version}-arm64"
309+
310+
Write-Host "Creating multi-arch manifest for latest..."
311+
docker buildx imagetools create `
312+
--tag "${ImageName}:latest" `
313+
"${ImageName}:${Version}-amd64" `
314+
"${ImageName}:${Version}-arm64"
315+
} else {
316+
Write-Host "Dry run: skipping manifest creation and push"
206317
}
318+
319+
echo "image-name=${ImageName}:${Version}" >> $Env:GITHUB_OUTPUT
320+
echo "latest-image-name=${ImageName}:latest" >> $Env:GITHUB_OUTPUT
207321
shell: pwsh
208-
working-directory: ${{ steps.prepare-artifacts.outputs.package-path }}
209322

210323
github-release:
211324
name: GitHub release

package/Linux/Dockerfile

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
FROM debian:bookworm-slim
22
LABEL maintainer="Devolutions Inc."
33

4+
# Install PowerShell and dependencies
5+
# Microsoft's APT repository doesn't have PowerShell for ARM64, so we install from GitHub releases
46
RUN apt-get update \
5-
&& apt-get install -y --no-install-recommends wget ca-certificates \
6-
&& wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb -O packages-microsoft-prod.deb \
7-
&& dpkg -i packages-microsoft-prod.deb \
8-
&& rm packages-microsoft-prod.deb \
9-
&& apt-get update \
10-
&& apt-get install -y --no-install-recommends \
11-
powershell openssl \
7+
&& apt-get install -y --no-install-recommends wget ca-certificates openssl \
8+
&& ARCH=$(dpkg --print-architecture) \
9+
&& if [ "$ARCH" = "arm64" ]; then \
10+
PWSH_VERSION=7.4.6 \
11+
&& wget -q "https://github.com/PowerShell/PowerShell/releases/download/v${PWSH_VERSION}/powershell-${PWSH_VERSION}-linux-arm64.tar.gz" \
12+
&& mkdir -p /opt/microsoft/powershell/7 \
13+
&& tar -xzf "powershell-${PWSH_VERSION}-linux-arm64.tar.gz" -C /opt/microsoft/powershell/7 \
14+
&& chmod +x /opt/microsoft/powershell/7/pwsh \
15+
&& ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh \
16+
&& rm "powershell-${PWSH_VERSION}-linux-arm64.tar.gz"; \
17+
else \
18+
wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb -O packages-microsoft-prod.deb \
19+
&& dpkg -i packages-microsoft-prod.deb \
20+
&& rm packages-microsoft-prod.deb \
21+
&& apt-get update \
22+
&& apt-get install -y --no-install-recommends powershell; \
23+
fi \
1224
&& rm -rf /var/lib/apt/lists/*
1325

1426
ENV XDG_CACHE_HOME="/tmp/.cache"

0 commit comments

Comments
 (0)