Why Swift and Docker Are the Perfect Pair for Server-Side Development
\n\nIf you're used to thinking that Swift is only for iOS mobile development, it's time to be surprised. Swift is confidently moving to the server side: Vapor, Kitura, Perfect — frameworks that allow you to write fast and secure backends. But there's one problem: how to guarantee that your application will work identically on a developer's Mac and on a production server running Ubuntu?
\n\nThe answer is containerizing Swift applications with Docker. Docker allows you to package everything: the binary itself, system libraries, Swift Package Manager (SPM) dependencies — and run it anywhere. No more "it works on my machine" issues.
\n\nIn this article, we'll cover how to build a Swift application into a Docker image, optimize its size, set up multi-stage builds, and even add hot reload for development. Let's go!
\n\nStep 1. Preparing the Swift Application
\n\nFor the example, we'll take a minimal web application using Vapor. If you don't have a project yet, create one:
\n\nmkdir MySwiftApp && cd MySwiftApp\nswift package init --type executable\n\nAdd Vapor to Package.swift:
// swift-tools-version:5.7\nimport PackageDescription\n\nlet package = Package(\n name: "MySwiftApp",\n dependencies: [\n .package(url: "https://github.com/vapor/vapor.git", from: "4.89.0")\n ],\n targets: [\n .executableTarget(\n name: "MySwiftApp",\n dependencies: [\n .product(name: "Vapor", package: "vapor")\n ]\n )\n ]\n)\n\nThe simplest main.swift:
\n\nimport Vapor\n\nvar env = try Environment.detect()\ntry LoggingSystem.bootstrap(from: &env)\nlet app = Application(env)\ndefer { app.shutdown() }\n\napp.get("hello") { req -> String in\n return "Hello from Swift in Docker!"\n}\n\ntry app.run()\n\nVerify that the project compiles locally: swift build. If everything is fine, let's move on to containerization.
Step 2. Writing a Dockerfile for Swift
\n\nThe main secret of good Swift containerization is multi-stage builds. In the first stage, we compile the binary; in the second, we copy only the binary and runtime dependencies. The final image ends up being 5-10 times smaller.
\n\n# ---- Stage 1: Build ----\nFROM swift:5.9-jammy AS build\n\nWORKDIR /app\n\n# Copy Package.swift and Package.resolved for caching dependencies\nCOPY Package.swift ./\nCOPY Package.resolved ./\n\n# Download dependencies separately — this is cached by Docker\nRUN swift package resolve\n\n# Copy source code\nCOPY Sources ./Sources\n\n# Build in release mode\nRUN swift build -c release --static-swift-stdlib\n\n# ---- Stage 2: Runtime ----\nFROM ubuntu:jammy AS runtime\n\n# Install minimal dependencies for Swift runtime\nRUN apt-get update && apt-get install -y \\\n libicu70 \\\n libcurl4 \\\n libxml2 \\\n && rm -rf /var/lib/apt/lists/*\n\nWORKDIR /app\n\n# Copy only the binary from the build stage\nCOPY --from=build /app/.build/release/MySwiftApp .\n\n# Port that Vapor listens on by default\nEXPOSE 8080\n\nCMD ["./MySwiftApp"]\n\nKey points:
\n- \n
--static-swift-stdlib— static linking of the standard library to avoid pulling libswiftCore.so into runtime. \n - Separate COPY — first
Package.swift, then the code. This way Docker uses layer caching and doesn't rebuild dependencies when source files change. \n - The base image
ubuntu:jammyweighs ~80 MB, the final image is about 100-150 MB. \n
Step 3. Building and Running the Container
\n\nBuild the image:
\n\ndocker build -t my-swift-app .\n\nRun it:
\n\ndocker run -p 8080:8080 my-swift-app\n\nO