Skip to content

Контейнеризация приложений Axum с Docker

Docker позволяет упаковать ваше приложение Axum вместе со всеми его зависимостями в изолированный контейнер, обеспечивая единообразие среды выполнения независимо от хост-системы.

Содержание

Базовый Dockerfile

Начнём с простого Dockerfile для приложения Axum:

dockerfile
FROM rust:1.74-slim as builder

WORKDIR /app

# Копируем файлы проекта
COPY Cargo.toml Cargo.lock ./
COPY src/ ./src/

# Собираем приложение в релизном режиме
RUN cargo build --release

# Вторая стадия: создаем финальный образ
FROM debian:bookworm-slim

WORKDIR /app

# Устанавливаем необходимые зависимости
RUN apt-get update && apt-get install -y \
    libssl-dev \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# Копируем бинарный файл из стадии сборки
COPY --from=builder /app/target/release/my-axum-app /app/my-axum-app

# Открываем порт, который использует приложение
EXPOSE 3000

# Запускаем приложение
CMD ["./my-axum-app"]

Многостадийная сборка

Для более оптимизированного образа используйте многостадийную сборку, которая сокращает размер финального образа и оставляет только необходимые компоненты:

dockerfile
# Стадия 1: Кэширование зависимостей
FROM rust:1.74-slim as cacher
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
# Создаем пустой проект для кэширования зависимостей
RUN mkdir src && \
    echo "fn main() {println!(\"Deps cache\")}" > src/main.rs && \
    cargo build --release && \
    rm -rf src

# Стадия 2: Сборка приложения
FROM rust:1.74-slim as builder
WORKDIR /app
# Копируем кэш зависимостей
COPY --from=cacher /app/Cargo.toml /app/Cargo.lock ./
COPY --from=cacher /app/target/ ./target/
# Копируем исходный код
COPY src/ ./src/
# Собираем приложение
RUN cargo build --release

# Стадия 3: Создание финального образа
FROM debian:bookworm-slim as runtime

WORKDIR /app

# Устанавливаем только необходимые зависимости времени выполнения
RUN apt-get update && apt-get install -y \
    libssl-dev \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# Копируем только исполняемый файл
COPY --from=builder /app/target/release/my-axum-app /app/my-axum-app

# Устанавливаем непривилегированного пользователя
RUN useradd -m axumuser
USER axumuser

# Открываем порт
EXPOSE 3000

# Указываем точку входа
ENTRYPOINT ["/app/my-axum-app"]

Docker Compose для Axum

Для проектов, использующих несколько сервисов (например, Axum + БД), создайте конфигурацию Docker Compose:

yaml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://postgres:password@db:5432/mydb
      - RUST_LOG=info
      - JWT_SECRET=my_secret_key
    depends_on:
      - db
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 15s

  db:
    image: postgres:15-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=password
      - POSTGRES_USER=postgres
      - POSTGRES_DB=mydb
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:

Оптимизация образов

1. Использование малых базовых образов

Для минимизации размера финального образа используйте alpine или дистрибутивы с суффиксом -slim:

dockerfile
# Для релизной стадии
FROM alpine:3.18

# Устанавливаем зависимости
RUN apk add --no-cache libssl1.1 ca-certificates

2. Статическая компиляция

Можно создать полностью статически скомпилированный бинарный файл:

dockerfile
FROM rust:1.74-alpine as builder

# Устанавливаем зависимости для статической компиляции
RUN apk add --no-cache musl-dev

WORKDIR /app

# Копируем файлы проекта
COPY Cargo.toml Cargo.lock ./
COPY src/ ./src/

# Статическая компиляция
RUN rustup target add x86_64-unknown-linux-musl
RUN cargo build --release --target x86_64-unknown-linux-musl

# Финальный образ
FROM scratch

# Копируем только бинарный файл
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/my-axum-app /app/my-axum-app

# Экспозиция порта и запуск
EXPOSE 3000
ENTRYPOINT ["/app/my-axum-app"]

3. Использование .dockerignore

Создайте файл .dockerignore для исключения ненужных файлов:

target/
.git/
.github/
.gitignore
*.md
docker-compose.yml
Dockerfile
tests/
.env*

Конфигурирование через переменные окружения

Для гибкой конфигурации приложения Axum в Docker используйте переменные окружения:

rust
use axum::{
    routing::get,
    Router,
};
use std::env;
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
    // Загрузка переменных окружения
    dotenv::dotenv().ok();
    
    // Конфигурация из переменных окружения
    let port = env::var("APP_PORT").unwrap_or_else(|_| "3000".to_string());
    let host = env::var("APP_HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL должен быть установлен");
    
    // Настройка логгирования
    let log_level = env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string());
    env::set_var("RUST_LOG", &log_level);
    tracing_subscriber::fmt::init();
    
    // Создание адреса сервера
    let addr: SocketAddr = format!("{}:{}", host, port).parse().unwrap();
    
    // Запуск сервера
    let app = Router::new()
        .route("/", get(|| async { "Hello, World!" }));
    
    println!("Сервер запущен на http://{}", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

А в Dockerfile или docker-compose.yml указываем переменные окружения:

yaml
environment:
  - APP_PORT=3000
  - APP_HOST=0.0.0.0
  - DATABASE_URL=postgres://postgres:password@db:5432/mydb
  - RUST_LOG=info

Безопасность в Docker

1. Запуск от непривилегированного пользователя

dockerfile
# Создаем пользователя в образе
RUN useradd -m appuser

# Устанавливаем права на директорию приложения
WORKDIR /app
COPY --from=builder /app/target/release/my-axum-app /app/my-axum-app
RUN chown -R appuser:appuser /app

# Переключаемся на непривилегированного пользователя
USER appuser

ENTRYPOINT ["/app/my-axum-app"]

2. Проверка уязвимостей образа

Используйте инструменты для сканирования:

bash
# Установка Trivy
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:latest image myapp:latest

3. Ограничение ресурсов

В docker-compose.yml или при запуске контейнера:

yaml
services:
  app:
    # ...
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M

Мониторинг контейнеров

Настройка Prometheus метрик

rust
use axum::{
    routing::get,
    Router,
};
use prometheus::{Encoder, TextEncoder, Registry};
use std::sync::Arc;

// Создаем метрики
let registry = Registry::new();
let request_counter = prometheus::IntCounter::new("app_requests_total", "Total request count").unwrap();
registry.register(Box::new(request_counter.clone())).unwrap();

// Обработчик для метрик
async fn metrics_handler(registry: Arc<Registry>) -> String {
    let encoder = TextEncoder::new();
    let mut buffer = Vec::new();
    encoder.encode(&registry.gather(), &mut buffer).unwrap();
    String::from_utf8(buffer).unwrap()
}

// Добавляем эндпоинт метрик в приложение
let app = Router::new()
    .route("/", get(|| async { "Hello, World!" }))
    .route("/metrics", get(move || metrics_handler(Arc::clone(&registry))));

Интеграция с Docker healthcheck

dockerfile
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
    CMD curl -f http://localhost:3000/health || exit 1

В приложении:

rust
async fn health_check() -> &'static str {
    "OK"
}

let app = Router::new()
    .route("/", get(|| async { "Hello, World!" }))
    .route("/health", get(health_check));

Лучшие практики

1. Многоэтапная сборка для минимизации размера

dockerfile
# Стадия сборки
FROM rust:1.74-slim as builder
# ... сборка приложения

# Финальная стадия
FROM debian:bookworm-slim
# ... только необходимые зависимости
COPY --from=builder /app/target/release/my-app /app/my-app

2. Предварительное кэширование зависимостей

dockerfile
# Кэширование зависимостей
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && \
    echo "fn main() {}" > src/main.rs && \
    cargo build --release && \
    rm -rf src

# Теперь копируем реальный код
COPY src/ ./src/

3. Правильная обработка сигналов завершения

rust
use tokio::signal;

async fn shutdown_signal() {
    let ctrl_c = async {
        signal::ctrl_c()
            .await
            .expect("Не удалось установить обработчик Ctrl+C");
    };

    #[cfg(unix)]
    let terminate = async {
        signal::unix::signal(signal::unix::SignalKind::terminate())
            .expect("Не удалось установить обработчик для сигнала terminate")
            .recv()
            .await;
    };

    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();

    tokio::select! {
        _ = ctrl_c => {},
        _ = terminate => {},
    }

    println!("Получен сигнал завершения, закрываем соединения");
}

// Использование в main
let server = axum::Server::bind(&addr)
    .serve(app.into_make_service())
    .with_graceful_shutdown(shutdown_signal());

server.await.unwrap();

4. Разделение конфигурации по окружениям

Создайте директорию docker с подкаталогами для разных окружений:

docker/
  ├── development/
  │   ├── Dockerfile
  │   └── docker-compose.yml
  ├── production/
  │   ├── Dockerfile
  │   └── docker-compose.yml
  └── ci/
      └── Dockerfile

5. Правильное логирование в Docker

rust
use tracing_subscriber::{
    fmt::format::FmtSpan,
    EnvFilter,
};

// Настройка для JSON-логирования (полезно для анализа логов)
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));

if std::env::var("LOG_FORMAT").unwrap_or_default() == "json" {
    tracing_subscriber::fmt()
        .with_env_filter(env_filter)
        .json()
        .init();
} else {
    tracing_subscriber::fmt()
        .with_env_filter(env_filter)
        .with_span_events(FmtSpan::CLOSE)
        .init();
}

Контейнеризация приложений Axum с использованием Docker обеспечивает согласованность среды разработки и продакшена, упрощает развертывание и масштабирование. Следуя лучшим практикам, можно создавать оптимальные и безопасные контейнеры для ваших приложений.