From 326062b3a091b31d24ce2a05d4e8e1c17548e5e0 Mon Sep 17 00:00:00 2001 From: Priec Date: Sat, 16 May 2026 23:03:29 +0200 Subject: [PATCH] prod --- ht_booking/.dockerignore | 22 ++++ ht_booking/.env.production.example | 22 ++++ ht_booking/.gitignore | 5 +- ht_booking/Caddyfile | 20 +++ ht_booking/DEPLOY.md | 203 +++++++++++++++++++++++++++++ ht_booking/Dockerfile | 42 ++++++ ht_booking/Makefile | 27 ++++ ht_booking/README.md | 10 ++ ht_booking/config/production.yaml | 60 +++++++++ ht_booking/docker-compose.prod.yml | 41 ++++++ 10 files changed, 450 insertions(+), 2 deletions(-) create mode 100644 ht_booking/.dockerignore create mode 100644 ht_booking/.env.production.example create mode 100644 ht_booking/Caddyfile create mode 100644 ht_booking/DEPLOY.md create mode 100644 ht_booking/Dockerfile create mode 100644 ht_booking/Makefile create mode 100644 ht_booking/config/production.yaml create mode 100644 ht_booking/docker-compose.prod.yml diff --git a/ht_booking/.dockerignore b/ht_booking/.dockerignore new file mode 100644 index 0000000..4bcb750 --- /dev/null +++ b/ht_booking/.dockerignore @@ -0,0 +1,22 @@ +# Build artifacts & dependencies — regenerated inside their build stages. +target +node_modules + +# VCS / Docker / deploy metadata — not needed inside the image. +.git +.gitignore +.dockerignore +Dockerfile +docker-compose.prod.yml +Makefile +Caddyfile +DEPLOY.md + +# Secrets & local data — must never be baked into the image. +.env +.env.* +*.sqlite +*.sqlite-* + +# Misc +*report.html diff --git a/ht_booking/.env.production.example b/ht_booking/.env.production.example new file mode 100644 index 0000000..099ce91 --- /dev/null +++ b/ht_booking/.env.production.example @@ -0,0 +1,22 @@ +# Production environment for ht_booking. +# +# Copy this file to `.env.production` on the server and fill in real values. +# docker-compose.prod.yml loads it via `env_file`. The real .env.production is +# gitignored — never commit it. + +# --- Admin account ----------------------------------------------------------- +# Seeded into the database on first boot. Login is gated to ADMIN_EMAIL, so +# only this account can reach the admin pages. +ADMIN_NAME=Admin +ADMIN_EMAIL=you@example.com +ADMIN_PASSWORD=change-me-to-a-long-random-password + +# --- JWT signing secret (REQUIRED) ------------------------------------------- +# Signs the admin session cookie. The app will not start if this is empty. +# Generate once with: openssl rand -hex 32 +JWT_SECRET= + +# --- Database (optional) ----------------------------------------------------- +# Defaults to a SQLite file on the Docker volume (data/production.sqlite). +# Leave commented unless you want a different location. +# DATABASE_URL=sqlite://data/production.sqlite?mode=rwc diff --git a/ht_booking/.gitignore b/ht_booking/.gitignore index b6bc973..0001472 100644 --- a/ht_booking/.gitignore +++ b/ht_booking/.gitignore @@ -1,6 +1,5 @@ **/config/local.yaml **/config/*.local.yaml -**/config/production.yaml # Generated by Cargo # will have compiled files and executables @@ -19,8 +18,10 @@ target/ *.sqlite *.sqlite-* -# Local secrets (hardcoded admin credentials) +# Local / production secrets — never commit. The committed templates are +# config/development.yaml and .env.production.example. .env +.env.production todo.md *report.html diff --git a/ht_booking/Caddyfile b/ht_booking/Caddyfile new file mode 100644 index 0000000..678b0ad --- /dev/null +++ b/ht_booking/Caddyfile @@ -0,0 +1,20 @@ +# Reverse-proxy config for Tenis Rajec. +# +# This file is imported by the central Caddyfile on the server. Caddy +# provisions and renews the HTTPS certificate automatically. See DEPLOY.md. + +tenisrajec.sk { + encode gzip + + # Long-cache the build-time static assets (CSS, images). They are + # rebuilt with the image, so a stale cache only lasts until the next deploy. + @static path /static/* + header @static Cache-Control "public, max-age=2592000" + + reverse_proxy ht-booking:5150 +} + +# Send the www host to the bare domain (one canonical URL — also good for SEO). +www.tenisrajec.sk { + redir https://tenisrajec.sk{uri} permanent +} diff --git a/ht_booking/DEPLOY.md b/ht_booking/DEPLOY.md new file mode 100644 index 0000000..2329ba7 --- /dev/null +++ b/ht_booking/DEPLOY.md @@ -0,0 +1,203 @@ +# Deploying ht_booking (Tenis Rajec) + +This app ships as a single Docker container that runs behind your existing +shared **Caddy** reverse proxy. Caddy terminates HTTPS; the app container has +no host ports and is reachable only through Caddy. The SQLite database lives +on a Docker volume so it survives rebuilds and restarts. + +``` +Internet ──▶ Caddy (:80/:443, HTTPS) + │ reverse_proxy ht-booking:5150 (over tenisrajec-net) + ▼ + ht-booking container ──▶ /usr/app/data/production.sqlite + (Docker volume: ht_booking_data) +``` + +Files in this repo that drive the deployment: + +| File | Role | +|---|---| +| `Dockerfile` | 3-stage build: CSS → Rust binary → slim runtime image | +| `docker-compose.prod.yml` | the app service, volume, network | +| `Caddyfile` | the site's reverse-proxy block (imported by central Caddy) | +| `config/production.yaml` | Loco production config (no secrets) | +| `.env.production.example` | template for secrets — copy to `.env.production` | +| `Makefile` | `make up` / `down` / `logs` / `restart` | + +--- + +## Prerequisites + +- The server already runs the shared Caddy stack (`docker-compose.caddy.yml`). +- Docker + `docker-compose` are installed (they already are — Caddy uses them). +- You can edit DNS for `tenisrajec.sk`. + +--- + +## One-time setup + +### 1. Point DNS at the server + +Create DNS **A records** so both names resolve to the server's public IP: + +``` +tenisrajec.sk A +www.tenisrajec.sk A +``` + +Do this first — Caddy needs the domain to resolve to obtain the TLS +certificate. Propagation can take a while. + +### 2. Clone the repo onto the server + +```sh +cd ~ +git clone ht_booking +cd ht_booking +``` + +(The rest of this guide assumes the repo is at `~/ht_booking`.) + +### 3. Create the shared network + +The app and Caddy talk over a dedicated Docker network — same pattern as your +other projects (`biomed-net`, `farmeris-net`, …): + +```sh +docker network create tenisrajec-net \ + --driver bridge --opt com.docker.network.driver.mtu=1450 +``` + +### 4. Create the secrets file + +```sh +cp .env.production.example .env.production +openssl rand -hex 32 # copy the output into JWT_SECRET +nano .env.production +``` + +Fill in: +- `JWT_SECRET` — paste the `openssl` output (**required** — the app won't start without it). +- `ADMIN_EMAIL` / `ADMIN_PASSWORD` — the single admin login, seeded on first boot. + +`.env.production` is gitignored — it stays only on the server. + +### 5. Hook the site into the central Caddy + +Caddy must (a) import this site's `Caddyfile` and (b) join `tenisrajec-net`. + +**a.** Add one line to the central `~/Caddyfile`: + +``` +import /etc/caddy/Caddyfile_tenisrajec +``` + +**b.** In `~/docker-compose.caddy.yml`, under the `caddy` service add the +Caddyfile mount to `volumes:` … + +```yaml + - ./ht_booking/Caddyfile:/etc/caddy/Caddyfile_tenisrajec +``` + +… add the network to the `caddy` service's `networks:` list … + +```yaml + networks: + - vonavucke-net + - biomed-net + - gitea-net + - mqtt-net + - farmeris-net + - tenisrajec-net # <-- add +``` + +… and declare it in the top-level `networks:` block: + +```yaml + tenisrajec-net: + external: true + driver: bridge + driver_opts: + com.docker.network.driver.mtu: 1450 +``` + +**c.** Recreate Caddy so it picks up the new mount and network: + +```sh +cd ~ +docker-compose -f docker-compose.caddy.yml up -d +``` + +### 6. Build and start the app + +```sh +cd ~/ht_booking +make up +``` + +The first build takes a few minutes (it compiles the Rust release binary). +On first boot the app creates the SQLite database, runs all migrations, and +seeds the admin account and a default court — automatically. + +### 7. Verify + +```sh +make logs # look for "listening on http://0.0.0.0:5150" +make ps # STATUS should become "healthy" +``` + +Then open — Caddy will have issued the certificate. + +--- + +## Updating after code changes + +```sh +cd ~/ht_booking +git pull +make restart +``` + +`make restart` rebuilds the image and recreates the container. The database +volume is untouched, so all bookings are preserved. Migrations for any new +schema run automatically on boot. + +> If you changed templates/CSS, the image rebuilds `app.css` itself — you do +> not need to run `npm run build:css` on the server. + +--- + +## Backups + +The whole database is one SQLite file inside the `ht_booking_data` volume. +Copy it out at any time: + +```sh +docker cp ht-booking:/usr/app/data/production.sqlite ./backup-$(date +%F).sqlite +``` + +Restore by stopping the app, copying a file back, and starting it: + +```sh +make down +docker cp ./backup-2026-05-16.sqlite ht-booking:/usr/app/data/production.sqlite +make up +``` + +A nightly `cron` job running that `docker cp` into a backed-up directory is +enough for this site. + +--- + +## Troubleshooting + +| Symptom | Cause / fix | +|---|---| +| App exits immediately, logs mention `JWT_SECRET` / config | `JWT_SECRET` is empty in `.env.production`. Set it, `make restart`. | +| `502 Bad Gateway` from Caddy | App not up yet, or Caddy didn't join `tenisrajec-net`. Check `make ps` and step 5b. | +| Caddy can't get a certificate | DNS not pointing at the server yet, or ports 80/443 blocked. | +| `network tenisrajec-net not found` | Run step 3 before `make up` / recreating Caddy. | +| Need a shell in the container | `docker exec -it ht-booking bash` | + +The app listens on `5150` **inside** its container only — it is intentionally +not published to the host. All traffic goes through Caddy. diff --git a/ht_booking/Dockerfile b/ht_booking/Dockerfile new file mode 100644 index 0000000..ada7ea0 --- /dev/null +++ b/ht_booking/Dockerfile @@ -0,0 +1,42 @@ +# Production image for ht_booking (Tenis Rajec). +# +# Three stages: +# css — compiles the Tailwind/daisyUI stylesheet with Node +# builder — compiles the release binary with Rust +# runtime — slim Debian image holding just the binary + assets +# +# Built and run via docker-compose.prod.yml — see DEPLOY.md. + +# ---- Stage 1 — Tailwind + daisyUI stylesheet ------------------------------- +FROM node:20-slim AS css +WORKDIR /build +COPY package.json package-lock.json tailwind.config.js ./ +RUN npm ci +COPY assets/css ./assets/css +COPY assets/views ./assets/views +RUN mkdir -p assets/static/css && npm run build:css + +# ---- Stage 2 — release binary ---------------------------------------------- +FROM rust:1.87.0-slim AS builder +WORKDIR /usr/src +COPY . . +RUN cargo build --release --bin ht_booking-cli + +# ---- Stage 3 — runtime ----------------------------------------------------- +FROM debian:bookworm-slim +# ca-certificates: outbound TLS. curl: the container healthcheck. +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /usr/app +COPY --from=builder /usr/src/target/release/ht_booking-cli ht_booking-cli +COPY --from=builder /usr/src/assets assets +COPY --from=builder /usr/src/config config +# Replace the committed CSS with one freshly built from the current templates, +# so the image is always self-consistent regardless of what was committed. +COPY --from=css /build/assets/static/css/app.css assets/static/css/app.css +# Selects config/production.yaml at startup. +ENV LOCO_ENV=production +EXPOSE 5150 +ENTRYPOINT ["/usr/app/ht_booking-cli"] +CMD ["start"] diff --git a/ht_booking/Makefile b/ht_booking/Makefile new file mode 100644 index 0000000..9c1b56f --- /dev/null +++ b/ht_booking/Makefile @@ -0,0 +1,27 @@ +# Production helpers for ht_booking — run these on the server. +COMPOSE = docker-compose -f docker-compose.prod.yml + +.PHONY: up down restart logs build ps + +# Build the image (if needed) and start the app in the background. +up: + $(COMPOSE) up -d --build + +# Stop and remove the container. The database volume is kept. +down: + $(COMPOSE) down + +# Restart with a fresh build — the usual command after `git pull`. +restart: down up + +# Follow the application logs. +logs: + $(COMPOSE) logs -f --tail=100 + +# Rebuild the image from scratch (ignores the Docker layer cache). +build: + $(COMPOSE) build --no-cache + +# Show container status. +ps: + $(COMPOSE) ps diff --git a/ht_booking/README.md b/ht_booking/README.md index e47ad4e..a591677 100644 --- a/ht_booking/README.md +++ b/ht_booking/README.md @@ -73,6 +73,16 @@ npm run watch:css # rebuild on save while developing templates remember to rebuild and commit it whenever the templates change. The Tailwind source lives in `assets/css/tailwind.css`; theme config is `tailwind.config.js`. +## Deployment + +Production runs as a single Docker container behind a Caddy reverse proxy. +See **[DEPLOY.md](DEPLOY.md)** for the full first-time setup. After that, a +deploy is just: + +```sh +git pull && make restart +``` + ## Full Stack Serving You can check your [configuration](config/development.yaml) to pick either frontend setup or server-side rendered template, and activate the relevant configuration sections. diff --git a/ht_booking/config/production.yaml b/ht_booking/config/production.yaml new file mode 100644 index 0000000..ca6a8f4 --- /dev/null +++ b/ht_booking/config/production.yaml @@ -0,0 +1,60 @@ +# Production configuration. +# +# Loaded when LOCO_ENV=production (set in the Dockerfile). This file is +# committed and contains NO secrets — the JWT secret and admin credentials +# come from environment variables (see .env.production.example / DEPLOY.md). + +logger: + enable: true + pretty_backtrace: false + level: info + format: compact + +server: + port: 5150 + # Bind on all interfaces so the Caddy container can reach the app over the + # shared Docker network. Do NOT use `localhost` here — it would be + # unreachable from outside this container. + binding: 0.0.0.0 + # Public URL of the site (used by mailers for absolute links). + host: https://tenisrajec.sk + middlewares: + static: + enable: true + must_exist: true + precompressed: false + folder: + uri: "/static" + path: "assets/static" + fallback: "assets/static/404.html" + +# In-process async workers — no Redis required. +workers: + mode: BackgroundAsync + +# The site has no SMTP server and admin login is password-based, so no mail is +# ever sent. `stub` guarantees a stray mail call can never block on a network. +mailer: + stub: true + +database: + # SQLite file on the mounted Docker volume (see docker-compose.prod.yml), + # so the data survives rebuilds and restarts. + uri: {{ get_env(name="DATABASE_URL", default="sqlite://data/production.sqlite?mode=rwc") }} + enable_logging: false + connect_timeout: 500 + idle_timeout: 500 + min_connections: 1 + max_connections: 1 + # Create the DB and run migrations automatically on first boot. + auto_migrate: true + # Never wipe data in production. + dangerously_truncate: false + dangerously_recreate: false + +auth: + jwt: + # REQUIRED. Generate once with `openssl rand -hex 32` and set JWT_SECRET in + # .env.production. The app will not start without it. + secret: {{ get_env(name="JWT_SECRET") }} + expiration: 604800 # 7 days diff --git a/ht_booking/docker-compose.prod.yml b/ht_booking/docker-compose.prod.yml new file mode 100644 index 0000000..cf57758 --- /dev/null +++ b/ht_booking/docker-compose.prod.yml @@ -0,0 +1,41 @@ +# Production stack for ht_booking (Tenis Rajec). +# +# One container: the Loco app. It publishes no host ports — the shared Caddy +# container reaches it by name over `tenisrajec-net` and terminates TLS. +# See DEPLOY.md for the full first-time setup. + +services: + ht-booking: + container_name: ht-booking + build: + context: . + dockerfile: Dockerfile + # Secrets & admin credentials — copy .env.production.example to + # .env.production on the server and fill it in. + env_file: + - .env.production + volumes: + # SQLite database — persisted across rebuilds and restarts. + - ht_booking_data:/usr/app/data + networks: + - tenisrajec-net + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-fsS", "http://localhost:5150/_ping"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + +networks: + # Shared with the central Caddy container — create it once with: + # docker network create tenisrajec-net --driver bridge \ + # --opt com.docker.network.driver.mtu=1450 + tenisrajec-net: + external: true + +volumes: + ht_booking_data: + # Explicit name so backup commands are predictable regardless of the + # Compose project name. + name: ht_booking_data