You develop on macOS but need to ship a Linux binary. You don't want to install Swift on Linux, set up a cross-compilation toolchain locally, or maintain a Linux VM. Docker can do everything: compile your Swift package into fully static Linux binaries for both amd64 and arm64, strip them, and drop them into your project folder. Nothing gets installed on your machine except Docker itself.
What "fully static" means
The Static Linux SDK from Swift.org links your binary against musl instead of glibc. The result is an ELF executable with zero runtime dependencies -- no shared libraries, no Swift runtime to install, not even a C library. Copy it to any Linux machine and run it.
The setup
You need three files in your Swift package root:
Dockerfile-- multi-stage build that compiles and strips both binaries.dockerignore-- keeps the Docker context smallbuild-linux.sh-- one command to build and smoke-test
Dockerfile
# Build static Linux binaries for both amd64 and arm64.
#
# Usage:
# DOCKER_BUILDKIT=1 docker build --output type=local,dest=./output .
FROM swift:6.2.3 AS builder
RUN swift sdk install \
https://download.swift.org/swift-6.2.3-release/static-sdk/swift-6.2.3-RELEASE/swift-6.2.3-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz \
--checksum f30ec724d824ef43b5546e02ca06a8682dafab4b26a99fbb0e858c347e507a2c
WORKDIR /build
# Copy package manifest first for dependency caching
COPY Package.swift Package.resolved ./
RUN swift package resolve
# Copy source code
COPY Sources/ Sources/
# Build for x86_64
RUN swift build -c release --swift-sdk x86_64-swift-linux-musl \
&& mkdir -p /output \
&& cp "$(swift build -c release --swift-sdk x86_64-swift-linux-musl --show-bin-path)/myapp" \
/output/myapp-linux-amd64
# Build for aarch64
RUN swift build -c release --swift-sdk aarch64-swift-linux-musl \
&& cp "$(swift build -c release --swift-sdk aarch64-swift-linux-musl --show-bin-path)/myapp" \
/output/myapp-linux-arm64
# Install cross-architecture strip tools and strip debug symbols
RUN apt-get update \
&& apt-get install -y --no-install-recommends binutils-x86-64-linux-gnu binutils-aarch64-linux-gnu \
&& rm -rf /var/lib/apt/lists/* \
&& x86_64-linux-gnu-strip /output/myapp-linux-amd64 \
&& aarch64-linux-gnu-strip /output/myapp-linux-arm64
# Export stage - only the binaries
FROM scratch
COPY --from=builder /output/ /Replace myapp with your executable target name (the one in your Package.swift).
.dockerignore
.build/
.git/
.DS_Store
DerivedData/
xcuserdata/
.swiftpm/Without this, Docker sends your entire .build/ and .git/ into the build context, which can be gigabytes.
build-linux.sh
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
APP_NAME="myapp"
OUTPUT_DIR="$SCRIPT_DIR/output"
BINARY_AMD64="$OUTPUT_DIR/${APP_NAME}-linux-amd64"
BINARY_ARM64="$OUTPUT_DIR/${APP_NAME}-linux-arm64"
echo "==> Building static Linux binaries..."
DOCKER_BUILDKIT=1 docker build --output "type=local,dest=$OUTPUT_DIR" .
echo ""
echo "==> Verifying binaries exist..."
for bin in "$BINARY_AMD64" "$BINARY_ARM64"; do
if [ ! -f "$bin" ]; then
echo "ERROR: $bin not found"
exit 1
fi
echo " $(basename "$bin"): $(du -h "$bin" | cut -f1)"
done
echo ""
echo "==> Testing arm64 binary in Alpine container..."
docker run --rm --platform linux/arm64 \
-v "$BINARY_ARM64":/app \
alpine /app --help
echo ""
echo "==> Testing amd64 binary in Alpine container..."
docker run --rm --platform linux/amd64 \
-v "$BINARY_AMD64":/app \
alpine /app --help
echo ""
echo "==> All builds and tests passed."
echo " $BINARY_AMD64"
echo " $BINARY_ARM64"chmod +x build-linux.shHow it works
Cross-compilation, not emulation
The Dockerfile does not use docker buildx --platform to emulate each architecture with QEMU. Instead, it runs the Swift compiler natively on whatever architecture your Docker host provides, and cross-compiles to both targets using the Static Linux SDK. The SDK contains musl sysroots for both x86_64 and aarch64, and the Swift compiler (backed by LLVM) can generate code for any supported architecture. This is significantly faster than QEMU emulation, which would run the entire Swift compiler under software translation.
Layer caching
The Dockerfile copies Package.swift and Package.resolved before the source code. This means swift package resolve (which downloads all dependencies) is cached as long as your dependency graph doesn't change. When you only edit source files, Docker skips straight to the build step.
Stripping
Unstripped static Swift binaries are large. In one project, the binaries were ~200MB each before stripping and ~70-80MB after. The catch: GNU strip only handles the host architecture. If your Docker host is arm64 (Apple Silicon), the native strip cannot process an x86_64 binary. The Dockerfile installs binutils-x86-64-linux-gnu and binutils-aarch64-linux-gnu to get architecture-specific strip tools that work regardless of the host.
Binary extraction
The final FROM scratch stage contains nothing but the two binaries. Combined with docker build --output type=local,dest=./output, Docker copies those files directly to your filesystem instead of producing an image. No docker cp, no docker create, no cleanup.
Smoke testing
The build script mounts each binary into a plain Alpine container (no Swift installed) and runs --help. This verifies the binaries are truly self-contained. The --platform flag ensures Docker uses the correct architecture for each test -- arm64 runs natively on Apple Silicon, amd64 runs through Rosetta/QEMU.
Gotchas
Swift version must match the SDK version. The FROM swift:6.2.3 image and the SDK URL both reference 6.2.3. If you upgrade Swift, update both. The SDK URLs follow a predictable pattern:
https://download.swift.org/swift-<VERSION>-release/static-sdk/swift-<VERSION>-RELEASE/swift-<VERSION>-RELEASE_static-linux-0.0.1.artifactbundle.tar.gzFind the current checksum on the Swift install page.
musl is not glibc. If your code or dependencies use import Glibc, they need to be updated to handle musl:
#if canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#endifMost of the Swift server ecosystem (swift-nio, AsyncHTTPClient, swift-argument-parser, swift-openapi-runtime) already handles this. But if you have a dependency that unconditionally imports Glibc, it will fail to compile.
Platform constraints are ignored on Linux. If your Package.swift has platforms: [.macOS(.v14)], that's fine. Swift Package Manager ignores Apple platform constraints when building for Linux.
No AVFoundation, AppKit, UIKit, etc. Apple frameworks aren't available on Linux. Guard any usage with #if canImport(AVFoundation) and provide a fallback.
Running it
./build-linux.shOutput:
==> Building static Linux binaries...
...
==> Verifying binaries exist...
myapp-linux-amd64: 81M
myapp-linux-arm64: 66M
==> Testing arm64 binary in Alpine container...
OVERVIEW: My application.
...
==> Testing amd64 binary in Alpine container...
OVERVIEW: My application.
...
==> All builds and tests passed.The binaries are in ./output/. Copy them to any Linux server and they just work.