From 8b1b9cedc227710680c9c8f82332be9cabad2692 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Wed, 17 Jun 2026 11:20:36 +0200 Subject: [PATCH] added caddy --- .env.example | 8 ++++- Caddyfile | 11 +++++++ backend/Dockerfile | 22 +++++++++++++ backend/src/config.rs | 8 +++++ backend/src/main.rs | 2 +- docker-compose.prod.yml | 68 +++++++++++++++++++++++++++++++++++++++++ frontend/Dockerfile | 20 ++++++++++++ 7 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 Caddyfile create mode 100644 backend/Dockerfile create mode 100644 docker-compose.prod.yml create mode 100644 frontend/Dockerfile diff --git a/.env.example b/.env.example index e161410..df5d2aa 100644 --- a/.env.example +++ b/.env.example @@ -11,9 +11,14 @@ BACKEND_PORT=8080 # 64+ hex chars. Generate: openssl rand -hex 32 SESSION_SECRET=please_generate_a_long_random_secret_at_least_64_chars_xxxxxxxxxx # Public URL of the frontend, used to build verification/reset links. +# Prod: https://consume.narl.io PUBLIC_APP_URL=http://localhost:5173 # Comma-separated allowed CORS origins. +# Prod (same-origin behind Caddy): https://consume.narl.io CORS_ORIGINS=http://localhost:5173 +# Mark the session cookie Secure (HTTPS-only). Auto-on when PUBLIC_APP_URL is +# https; override here. Must be true in production, false for plain-http dev. +# COOKIE_SECURE=true # ── SMTP (mail notifications + verification) ──────────────── SMTP_HOST=smtp.example.com @@ -25,5 +30,6 @@ SMTP_FROM="Shopping List " SMTP_SECURITY=starttls # ── Frontend ──────────────────────────────────────────────── -# Where the SvelteKit app reaches the backend (server-side). +# Base origin the browser uses to reach the backend (no trailing /api). +# Prod (same-origin behind Caddy): https://consume.narl.io PUBLIC_API_BASE=http://localhost:8080 diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..7644cb1 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,11 @@ +consume.narl.io { + # Backend API (routes are served under /api by the Rust app). + handle /api/* { + reverse_proxy backend:8080 + } + + # Everything else → SvelteKit (adapter-node) server. + handle { + reverse_proxy frontend:3000 + } +} diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..903a5aa --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,22 @@ +# ── Build ─────────────────────────────────────────────────── +FROM rust:1-bookworm AS build +WORKDIR /app + +# Cache deps: copy manifests, build a stub, then the real source. +COPY Cargo.toml Cargo.lock ./ +RUN mkdir src && echo "fn main() {}" > src/main.rs \ + && cargo build --release \ + && rm -rf src + +COPY . . +# Touch so cargo rebuilds with the real main.rs. +RUN touch src/main.rs && cargo build --release + +# ── Runtime ───────────────────────────────────────────────── +FROM debian:bookworm-slim AS runtime +WORKDIR /app +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* +COPY --from=build /app/target/release/shoplist-backend /usr/local/bin/shoplist-backend +EXPOSE 8080 +CMD ["shoplist-backend"] diff --git a/backend/src/config.rs b/backend/src/config.rs index c23c7b5..32ac7c0 100644 --- a/backend/src/config.rs +++ b/backend/src/config.rs @@ -16,6 +16,8 @@ pub struct Config { pub refetch_min_age_secs: i64, /// Default ISO 4217 currency when an adapter can't determine one. pub default_currency: String, + /// Mark the session cookie Secure (HTTPS-only). Enable in production. + pub cookie_secure: bool, } #[derive(Clone, Debug)] @@ -67,6 +69,12 @@ impl Config { refetch_interval_secs: opt("REFETCH_INTERVAL_SECS", "300").parse()?, refetch_min_age_secs: opt("REFETCH_MIN_AGE_SECS", "21600").parse()?, default_currency: opt("DEFAULT_CURRENCY", "EUR").to_uppercase(), + // Default Secure when the public URL is HTTPS; override with COOKIE_SECURE. + cookie_secure: env::var("COOKIE_SECURE") + .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) + .unwrap_or_else(|_| { + opt("PUBLIC_APP_URL", "http://localhost:5173").starts_with("https://") + }), smtp: SmtpConfig { host: opt("SMTP_HOST", "localhost"), port: opt("SMTP_PORT", "587").parse()?, diff --git a/backend/src/main.rs b/backend/src/main.rs index 46bdc30..6b6f0e2 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -43,7 +43,7 @@ async fn main() -> anyhow::Result<()> { session_store.migrate().await?; let session_layer = SessionManagerLayer::new(session_store) - .with_secure(false) // set true behind HTTPS in production + .with_secure(config.cookie_secure) // true behind HTTPS in production .with_same_site(tower_sessions::cookie::SameSite::Lax) .with_expiry(Expiry::OnInactivity(Duration::days(30))); diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..5253c9b --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,68 @@ +services: + db: + image: postgres:17-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - pgdata:/var/lib/postgresql/data + # No published port: db is reachable only on the compose network. + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 10 + + backend: + build: ./backend + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} + BACKEND_HOST: 0.0.0.0 + BACKEND_PORT: 8080 + SESSION_SECRET: ${SESSION_SECRET} + COOKIE_SECURE: "true" + PUBLIC_APP_URL: ${PUBLIC_APP_URL} + CORS_ORIGINS: ${CORS_ORIGINS} + SMTP_HOST: ${SMTP_HOST} + SMTP_PORT: ${SMTP_PORT} + SMTP_USERNAME: ${SMTP_USERNAME} + SMTP_PASSWORD: ${SMTP_PASSWORD} + SMTP_FROM: ${SMTP_FROM} + SMTP_SECURITY: ${SMTP_SECURITY} + # No published port: reached via caddy only. + + frontend: + build: ./frontend + restart: unless-stopped + depends_on: + - backend + environment: + PUBLIC_API_BASE: ${PUBLIC_API_BASE} + # adapter-node needs the public origin for CSRF/redirects behind a proxy. + ORIGIN: ${PUBLIC_APP_URL} + # No published port: reached via caddy only. + + caddy: + image: caddy:2-alpine + restart: unless-stopped + depends_on: + - frontend + - backend + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + +volumes: + pgdata: + caddy_data: + caddy_config: diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..8868863 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,20 @@ +# ── Build ─────────────────────────────────────────────────── +FROM node:22-bookworm-slim AS build +WORKDIR /app +RUN corepack enable + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ +RUN pnpm install --frozen-lockfile + +COPY . . +RUN pnpm build && pnpm prune --prod + +# ── Runtime ───────────────────────────────────────────────── +FROM node:22-bookworm-slim AS runtime +WORKDIR /app +ENV NODE_ENV=production +COPY --from=build /app/build ./build +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/package.json ./ +EXPOSE 3000 +CMD ["node", "build"]