Cél és előfeltételek

Az előző leckében ("JWT autentikáció") megépítettük a REST API-nk védelmét: bejelentkezés, token kiadás, auth middleware. Egy ilyen szolgáltatás azonban addig csak papíron "él", amíg a saját gépeden fut cargo run-nal. Ebben a leckében megtanulod, hogyan csomagolod be a Rust szervert egy Docker image-be, és hogyan teszed publikusan elérhetővé a Fly.io felhőplatformon.

Ehhez a leckéhez a következő korábbi ismeretekre lesz szükséged:

  • axum Router, handler-ek, State, Arc megosztott állapot
  • tokio::main, axum::serve
  • sqlx connection pool és migrációk
  • jsonwebtoken és az auth middleware felépítése
  • tracing/tracing-subscriber
  • parancssori (bash) alapok, cargo build --release

Két új dolgot fogsz megtanulni:

  1. Hogyan épül fel egy multi-stage Dockerfile, és miért ez az iparági alapértelmezés Rust szerverekhez.
  2. Hogyan zajlik egy Fly.io deploy az első parancstól a publikus URL-ig.
Megjegyzés

A Docker és a Fly.io telepítése (docker, flyctl) ennek a leckének nem témája - feltételezzük, hogy a Docker Desktop/Engine már fut a gépeden, a flyctl parancsot pedig a 4. lépésben telepítjük.

Miért van szükség konténerizált deployra?

A Rust nagy előnye, hogy a cargo build --release egyetlen, viszonylag kis méretű, natív binárist ad. Elsőre azt gondolhatnád, hogy elég csak ezt a fájlt felmásolni egy szerverre. A gyakorlatban azonban több probléma is felmerül:

  • Eltérő futtatási környezet. A build gépeden lévő glibc-verzió, OpenSSL-könyvtárak vagy CA-tanúsítványok hiányozhatnak a célszerverről.
  • Reprodukálhatóság. Egy Dockerfile pontosan leírja, milyen build-eszközökkel és milyen alap image-ből készül a szerver - ez ugyanazt az eredményt adja a laptopodon és a felhőben is.
  • Platform-elvárás. A modern felhőplatformok (Fly.io, Railway, AWS Fargate, Google Cloud Run) mind konténer image-eket futtatnak, nem nyers binárisokat.

A Rust ökoszisztéma erőssége itt is megmutatkozik: mivel a végeredmény egy statikusan linkelt, kis méretű bináris, a Docker image-ünk is drámaian kisebb lehet, mint egy tipikus Node.js vagy Java alapú szerveré - ha jól építjük fel a Dockerfile-t.

Előkészítés: a projekt és a függőségek

Ha még nincs kéznél a JWT-leckéből származó projekt, hozz létre egy egyszerűsített demót, amivel ezt a leckét gyakorolhatod:

cargo new jwt-api --bin
cd jwt-api

A Cargo.toml függőségei (a korábbi leckékben megismert crate-ek):

[package]
name = "jwt-api"
version = "0.1.0"
edition = "2024"

[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tracing = "0.1"
tracing-subscriber = "0.3"
sqlx = { version = "0.9", features = ["runtime-tokio", "postgres", "tls-rustls"] }
jsonwebtoken = "10"
thiserror = "2"
Tipp

A tls-rustls feature-t azért adtuk hozzá az sqlx-hez, mert a legtöbb felhős Postgres (így a Fly Postgres is) TLS-en keresztül fogadja a kapcsolatokat. Enélkül a sqlx::PgPool::connect hívás elakadhat éles adatbázis ellen.

1. lépés: .dockerignore

Feladat: hozz létre egy .dockerignore fájlt a projekt gyökerében, amely kizárja a build-artefaktumokat és a helyi konfigurációt a Docker build kontextusból. Miért fontos ez? Mert a docker build minden fájlt átküld a Docker daemon-nak a build kontextusból - ha ott van a target/ mappa (ami akár gigabyte-os is lehet), a build lassú és felesleges.

Megoldás
target/
.git/
.env
*.log
Dockerfile
.dockerignore

2. lépés: multi-stage Dockerfile

Feladat: írj egy két szakaszos (multi-stage) Dockerfile-t:

  • az első szakasz (builder) egy teljes Rust build-környezetből (rust:1.96-slim) lefordítja a projektet release módban,
  • a második szakasz (runtime) egy minimális Debian image-ből csak a lefordított binárist és a szükséges futásidejű fájlokat (CA-tanúsítványok) tartalmazza.

A cél, hogy a végső image ne tartalmazza a Rust compiler-t, a forráskódot vagy a build-cache-t - csak azt, amire a szervernek futáskor szüksége van.

Tipp

Ha a Cargo.toml/Cargo.lock-ot előbb másolod be, és egy üres main.rs-sel lefordítod a függőségeket, a Docker cache-elni tudja ezt a réteget. Így ha csak a forráskódot módosítod (a függőségeket nem), a következő build nem fordítja újra az összes crate-et.

Megoldás
# --- 1. szakasz: build ---
FROM rust:1.96-slim AS builder
WORKDIR /app

# Előbb csak a függőség-leírókat másoljuk be, hogy a Docker cache-elhesse
# a build lépést, amíg a Cargo.toml/Cargo.lock nem változik.
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm -rf src

# Most jöhet a valódi forráskód - a fenti réteg a cache-ből jön,
# csak a saját kódunkat kell újra lefordítani.
COPY src ./src
RUN touch src/main.rs && cargo build --release

# --- 2. szakasz: futtatás ---
FROM debian:bookworm-slim AS runtime
WORKDIR /app

# A ca-certificates a HTTPS/TLS kapcsolatokhoz szükséges gyökértanúsítványokat
# tartalmazza - ezekre a Postgres felé irányuló TLS-kapcsolathoz (sqlx
# `tls-rustls` feature) és minden kimenő HTTPS híváshoz szükség van.
RUN apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates \
    && rm -rf /var/lib/apt/lists/*

COPY --from=builder /app/target/release/jwt-api /usr/local/bin/jwt-api

ENV PORT=8080
EXPOSE 8080

CMD ["/usr/local/bin/jwt-api"]
Figyelem

A rust:1.96-slim image több száz megabyte, mert tartalmazza a teljes toolchaint. Ez rendben van, hiszen ez csak a builder szakasz - a végső image-ben ebből semmi nem marad, csak a lefordított bináris kerül át a COPY --from=builder sorral.

3. lépés: lokális build és futtatás tesztelése

Feladat: buildeld le az image-et lokálisan, futtasd le egy konténerben, és ellenőrizd egy curl hívással, hogy válaszol-e a /health endpoint.

Megoldás
docker build -t jwt-api:local .

docker run --rm -p 8080:8080 \
  -e DATABASE_URL="postgres://user:pass@localhost:5432/jwtdb" \
  -e JWT_SECRET="fejleszteshez-ideiglenes-titok" \
  jwt-api:local

Egy másik terminálban:

curl http://localhost:8080/health

Sikeres válasz esetén egy {"status":"ok","version":"0.1.0"} jellegű JSON-t kapsz vissza.

4. lépés: a Fly.io CLI telepítése és az első deploy

A Fly.io egy olyan felhőplatform, amely a feltöltött Docker image-et egy könnyű virtuális gépként ("Fly Machine") indítja el, a hozzá tartozó publikus URL-lel és HTTPS-tanúsítvánnyal.

Feladat: telepítsd a flyctl parancssori eszközt, jelentkezz be, majd futtasd le a fly launch parancsot úgy, hogy a már meglévő Dockerfile-unkat használja (ne generáljon újat).

Megoldás
# Telepítés (macOS/Linux; Windows-on lásd a Fly.io dokumentációját)
curl -L https://fly.io/install.sh | sh

# Bejelentkezés / regisztráció
fly auth login

# Az alkalmazás inicializálása a jelenlegi mappában
fly launch --dockerfile Dockerfile --no-deploy

A fly launch interaktívan rákérdez az alkalmazás nevére és a régióra, majd létrehoz egy fly.toml konfigurációs fájlt. A --no-deploy kapcsolóval most még nem indítjuk el a deploy-t - előbb beállítjuk a titkos változókat.

Egy tipikus fly.toml így néz ki:

app = "jwt-api-demo"
primary_region = "fra"

[build]
  dockerfile = "Dockerfile"

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 0

[[http_service.checks]]
  interval = "15s"
  timeout = "3s"
  grace_period = "5s"
  method = "get"
  path = "/health"

A [[http_service.checks]] blokk pontosan a mi /health endpointunkat hívja rendszeresen - ha a szerver nem válaszol, a Fly.io újraindítja a machine-t.

5. lépés: titkos változók és az adatbázis csatlakoztatása

Feladat: állítsd be a DATABASE_URL és JWT_SECRET értékeket titkos (secret) változóként, majd futtasd le a valódi deploy-t, és nézd meg a naplókat.

Jó tudni

Titkos adatokat (adatbázis-jelszó, JWT-titok) soha ne írj bele a fly.toml-ba vagy a Dockerfile-ba - azok könnyen a repóba kerülhetnek. A fly secrets set a Fly.io saját titokkezelőjébe menti az értéket, futásidőben környezeti változóként kapja meg a konténer.

Megoldás
fly secrets set \
  DATABASE_URL="postgres://user:pass@adatbazis-host:5432/jwtdb" \
  JWT_SECRET="egy-eleg-hosszu-veletlen-string"

fly deploy

fly status
fly logs

Sikeres deploy után a Fly.io kiad egy publikus címet a https://<az-app-neve>.fly.dev mintázat szerint - ezt a fly status parancs is kiírja. A /health endpointot innen kívülről is elérheted:

curl https://<az-app-neve>.fly.dev/health

Ha van saját Postgres adatbázisod a Fly.io-n (fly postgres create), a kapott csatlakozási URL-t másold be a DATABASE_URL secretbe - ekkor a tls-rustls feature miatt az sqlx automatikusan titkosított kapcsolatot épít fel hozzá.

Teljes referencia-megoldás

A demó API (amit konténerbe csomagoltunk) src/main.rs tartalma:

use axum::{extract::State, routing::get, Json, Router};
use serde::Serialize;
use std::sync::Arc;

/// Egyszerűsített alkalmazásállapot - egy valódi projektben itt lakna
/// az sqlx connection pool és a JWT-titkos kulcs is.
struct AppState {
    version: &'static str,
}

#[derive(Serialize)]
struct HealthResponse {
    status: &'static str,
    version: &'static str,
}

async fn health(State(state): State<Arc<AppState>>) -> Json<HealthResponse> {
    Json(HealthResponse {
        status: "ok",
        version: state.version,
    })
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    let state = Arc::new(AppState { version: "0.1.0" });

    let app = Router::new()
        .route("/health", get(health))
        .with_state(state);

    let port = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string());
    let addr = format!("0.0.0.0:{port}");

    tracing::info!("Szerver indul: {addr}");

    let listener = tokio::net::TcpListener::bind(&addr)
        .await
        .expect("nem sikerült portot foglalni");

    axum::serve(listener, app)
        .await
        .expect("a szerver leállt hibával");
}

Ehhez tartozik a fentebb bemutatott Cargo.toml, a .dockerignore, a multi-stage Dockerfile és a fly.toml. Ha mind a négy fájl megvan a projekt gyökerében, a teljes deploy folyamat három parancsra egyszerűsödik:

fly launch --dockerfile Dockerfile --no-deploy
fly secrets set DATABASE_URL="..." JWT_SECRET="..."
fly deploy

Bónusz-kihívások

  • Vezess be egy plusz lépést a Dockerfile builder szakaszába, amely cargo clippy -- -D warnings és cargo test parancsokat futtat build közben - így egy hibás commit sosem jut el a runtime image-ig.
  • Állíts be automatikus deploy-t: hozz létre egy GitHub Actions workflow-t, amely minden main branch-re történő push után lefuttatja a fly deploy parancsot egy FLY_API_TOKEN GitHub secret felhasználásával.

Összefoglalás és mi jön legközelebb

Ezzel a leckével a JWT-védett API-d nem csak a laptopodon fut, hanem egy valódi, publikus URL-en elérhető, HTTPS-en keresztül védett szolgáltatásként üzemel a Fly.io-n. Megtanultad, hogyan válik szét a build és a futtatási réteg egy multi-stage Dockerfile-ban, és hogyan zajlik egy Fly.io deploy a fly launch-tól a fly secrets set-en át a fly deploy-ig.

Ez volt az útvonal utolsó gyakorlati leckéje. A következő, záró leckében ("Záróvizsga") összefoglaljuk az egész utat: a Future trait-től és a Tokio executor-tól kezdve az Axum routing-on és az sqlx-en át egészen a JWT-autentikációig és a mostani deploy-ig - és számot adhatsz arról, hogy mindezt önállóan is össze tudod rakni.