@@ -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
0 commit comments