Почему Swift и Docker — идеальная пара для серверной разработки
Если вы привыкли думать, что Swift — это только мобильная разработка под iOS, пришло время удивиться. Swift уверенно шагает на серверную сторону: Vapor, Kitura, Perfect — фреймворки, которые позволяют писать быстрые и безопасные бэкенды. Но есть одна проблема: как гарантировать, что ваше приложение будет работать одинаково на Mac разработчика и на production-сервере под Ubuntu?
Ответ — контейнеризация Swift приложений с Docker. Docker позволяет упаковать всё: сам бинарник, системные библиотеки, зависимости Swift Package Manager (SPM) — и запускать это где угодно. Никаких «у меня работает» и «а у меня нет».
В этой статье мы разберём, как собрать Swift-приложение в Docker-образ, оптимизировать его размер, настроить многоступенчатую сборку и даже добавить горячую перезагрузку для разработки. Поехали!
Шаг 1. Подготовка Swift-приложения
Для примера возьмём минимальное веб-приложение на Vapor. Если у вас ещё нет проекта, создайте его:
mkdir MySwiftApp && cd MySwiftApp
swift package init --type executable
Добавим Vapor в Package.swift:
// swift-tools-version:5.7
import PackageDescription
let package = Package(
name: "MySwiftApp",
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "4.89.0")
],
targets: [
.executableTarget(
name: "MySwiftApp",
dependencies: [
.product(name: "Vapor", package: "vapor")
]
)
]
)
Самый простой main.swift:
import Vapor
var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)
let app = Application(env)
defer { app.shutdown() }
app.get("hello") { req -> String in
return "Hello from Swift in Docker!"
}
try app.run()
Проверьте, что проект компилируется локально: swift build. Если всё ок — переходим к контейнеризации.
Шаг 2. Пишем Dockerfile для Swift
Главный секрет хорошей контейнеризации Swift — многоступенчатая сборка. На первом этапе мы компилируем бинарник, на втором — копируем только его и рантайм-зависимости. Итоговый образ получается в 5-10 раз меньше.
# ---- Этап 1: Сборка ----
FROM swift:5.9-jammy AS build
WORKDIR /app
# Копируем Package.swift и Package.resolved для кэширования зависимостей
COPY Package.swift ./
COPY Package.resolved ./
# Скачиваем зависимости отдельно — это кэшируется Docker'ом
RUN swift package resolve
# Копируем исходный код
COPY Sources ./Sources
# Сборка в release-режиме
RUN swift build -c release --static-swift-stdlib
# ---- Этап 2: Runtime ----
FROM ubuntu:jammy AS runtime
# Устанавливаем минимальные зависимости для Swift runtime
RUN apt-get update && apt-get install -y \
libicu70 \
libcurl4 \
libxml2 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Копируем только бинарник из этапа сборки
COPY --from=build /app/.build/release/MySwiftApp .
# Порт, который слушает Vapor по умолчанию
EXPOSE 8080
CMD ["./MySwiftApp"]
Ключевые моменты:
--static-swift-stdlib— статическая линковка стандартной библиотеки, чтобы не тащить libswiftCore.so в runtime.- Разделение COPY — сначала
Package.swift, потом код. Так Docker использует кэш слоёв и не пересобирает зависимости при изменении исходников. - Базовый образ
ubuntu:jammyвесит ~80 МБ, итоговый образ — около 100-150 МБ.
Шаг 3. Сборка и запуск контейнера
Собираем образ:
docker build -t my-swift-app .
Запускаем:
docker run -p 8080:8080 my-swift-app
Откройте браузер: http://localhost:8080/hello. Видите приветствие? Поздравляю — контейнеризация Swift приложений с Docker выполнена!
Шаг 4. Docker Compose для разработки с горячей перезагрузкой
На этапе разработки пересобирать образ после каждой правки кода — мучительно. Используем Docker Compose с монтированием томов и командой swift run:
# docker-compose.yml
version: '3.8'
services:
swift-app:
image: swift:5.9-jammy
container_name: swift-dev
working_dir: /app
volumes:
- .:/app
ports:
- "8080:8080"
command: swift run --skip-update
environment:
- SWIFT_DEVELOPMENT=true
Теперь меняйте код на хосте — контейнер автоматически перекомпилирует и перезапустит приложение. Запуск одной командой:
docker compose up
Этот подход идеален для контейнеризации Swift-приложений на этапе активной разработки.
Шаг 5. Оптимизация: уменьшаем образ до минимума
Хотите ещё меньше? Используйте scratch или alpine как базовый образ. Для Swift с статической линковкой это реально:
# Финальный этап на scratch
FROM scratch AS runtime
COPY --from=build /app/.build/release/MySwiftApp /app
EXPOSE 8080
CMD ["/app"]
Размер образа — всего 20-30 МБ! Минус: нет shell, нет apt, только бинарник. Подходит для production, где вы уверены в окружении.
Шаг 6. CI/CD и деплой контейнера
Готовый образ можно пушить в Docker Hub или GitHub Container Registry. Пример GitHub Actions workflow:
# .github/workflows/deploy.yml
name: Build and Push Swift Docker Image
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: docker build -t ghcr.io/myorg/my-swift-app:latest .
- name: Push to GitHub Packages
run: |
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker push ghcr.io/myorg/my-swift-app:latest
После пуша образ автоматически разворачивается на сервере через docker pull. Вся контейнеризация Swift приложений становится частью пайплайна.
Частые проблемы и их решения
- Ошибка "Segmentation fault" — обычно из-за несоответствия версий libicu. Убедитесь, что установили
libicu70(или версию, соответствующую вашей сборке