Контейнеризация приложений Axum с Docker
Docker позволяет упаковать ваше приложение Axum вместе со всеми его зависимостями в изолированный контейнер, обеспечивая единообразие среды выполнения независимо от хост-системы.
Содержание
- Базовый Dockerfile
- Многостадийная сборка
- Docker Compose для Axum
- Оптимизация образов
- Конфигурирование через переменные окружения
- Безопасность в Docker
- Мониторинг контейнеров
- Лучшие практики
Базовый Dockerfile
Начнём с простого Dockerfile
для приложения Axum:
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"]
Многостадийная сборка
Для более оптимизированного образа используйте многостадийную сборку, которая сокращает размер финального образа и оставляет только необходимые компоненты:
# Стадия 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:
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
:
# Для релизной стадии
FROM alpine:3.18
# Устанавливаем зависимости
RUN apk add --no-cache libssl1.1 ca-certificates
2. Статическая компиляция
Можно создать полностью статически скомпилированный бинарный файл:
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 используйте переменные окружения:
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 указываем переменные окружения:
environment:
- APP_PORT=3000
- APP_HOST=0.0.0.0
- DATABASE_URL=postgres://postgres:password@db:5432/mydb
- RUST_LOG=info
Безопасность в Docker
1. Запуск от непривилегированного пользователя
# Создаем пользователя в образе
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. Проверка уязвимостей образа
Используйте инструменты для сканирования:
# Установка Trivy
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:latest image myapp:latest
3. Ограничение ресурсов
В docker-compose.yml
или при запуске контейнера:
services:
app:
# ...
deploy:
resources:
limits:
cpus: '1'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
Мониторинг контейнеров
Настройка Prometheus метрик
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(®istry.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(®istry))));
Интеграция с Docker healthcheck
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
В приложении:
async fn health_check() -> &'static str {
"OK"
}
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.route("/health", get(health_check));
Лучшие практики
1. Многоэтапная сборка для минимизации размера
# Стадия сборки
FROM rust:1.74-slim as builder
# ... сборка приложения
# Финальная стадия
FROM debian:bookworm-slim
# ... только необходимые зависимости
COPY --from=builder /app/target/release/my-app /app/my-app
2. Предварительное кэширование зависимостей
# Кэширование зависимостей
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && \
echo "fn main() {}" > src/main.rs && \
cargo build --release && \
rm -rf src
# Теперь копируем реальный код
COPY src/ ./src/
3. Правильная обработка сигналов завершения
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
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 обеспечивает согласованность среды разработки и продакшена, упрощает развертывание и масштабирование. Следуя лучшим практикам, можно создавать оптимальные и безопасные контейнеры для ваших приложений.