Introduction: Why Containerizing Swift is a Challenge and an Advantage
\nContainerizing Swift applications for iOS and macOS was long considered the domain of enthusiasts. Unlike Go or Node.js, Swift requires compilation on the target platform, and official Docker images remained exotic for a long time. However, with the release of Swift 5.x and the emergence of official images like swift:5.10 (and newer), the situation has changed dramatically. Today, Docker allows not only running server backends on Vapor but also automating the build of CLI utilities, testing code in an isolated environment, and even preparing binary artifacts for deployment on macOS.
In this article, we will cover advanced techniques: multi-stage builds, aggressive dependency caching, image size optimization, and integration with Swift Package Manager. The examples are geared towards real projects — from a simple HTTP server to a complex microservice.
\n\n1. Multi-Stage Build: Separating Compilation and Runtime
\nA classic mistake beginners make is using a single image for both building and running. This results in an image weighing >2 GB due to the SDK, tools, and source code. The correct approach is two stages: builder and runner.
\nExample Dockerfile for a Vapor Application
\n# Stage 1: Build\nFROM swift:5.10 AS builder\nWORKDIR /app\nCOPY Package.swift Package.resolved ./\nRUN swift package resolve --cache-path .build/artifacts\nCOPY Sources Sources\nCOPY Tests Tests\nRUN swift build -c release --static-swift-stdlib\n\n# Stage 2: Minimal Runtime\nFROM ubuntu:22.04\nRUN apt-get update && apt-get install -y libicu70 libxml2 ca-certificates && rm -rf /var/lib/apt/lists/*\nWORKDIR /app\nCOPY --from=builder /app/.build/release/Run .\nEXPOSE 8080\nCMD ["./Run"]\nKey points:
\n- \n
- Dependency caching — copy
Package.swiftandPackage.resolvedbefore copying the source code. Docker caches the layer, and when only code changes, only the binaries are rebuilt. \n - The
--static-swift-stdlibflag — statically links the Swift standard library. This reduces runtime requirements but increases the binary size by ~30%. \n - Minimal image — use Ubuntu with only the necessary system libraries (
libicu,libxml2). The final image is less than 150 MB compared to 2 GB with a single-stage build. \n
2. Aggressive Caching with BuildKit and SSH Agents
\nWhen working in CI/CD (GitHub Actions, GitLab CI), each run downloads dependencies from scratch. Use Docker BuildKit to mount the Swift PM cache.
\nDockerfile with BuildKit Caching
\n# syntax=docker/dockerfile:1\nFROM swift:5.10 AS builder\nWORKDIR /app\nRUN --mount=type=cache,target=/app/.build/artifacts \\\n --mount=type=bind,source=Package.swift,target=Package.swift \\\n --mount=type=bind,source=Package.resolved,target=Package.resolved \\\n swift package resolve\nCOPY Sources Sources\nRUN --mount=type=cache,target=/app/.build/artifacts \\\n swift build -c release --static-swift-stdlib\nAdvantages:
\n- \n
- Persistent cache — dependency artifacts are preserved between builds on the same host. \n
- SSH agents — if your
Package.swiftreferences private repositories, use--mount=type=sshfor authentication without storing keys in the image. \n
For macOS hosts (e.g., MacStadium), be sure to add --platform linux/amd64, as Swift on ARM (M1/M2) requires emulation.
3. Advanced Work with Swift Package Manager
\nSwift PM is not just a dependency manager, but also a powerful tool for testing and linting inside a container.
\nRunning Tests in a Container with Code Coverage Support
\nFROM swift:5.10 AS te