This commit is contained in:
Priec
2026-05-16 23:03:29 +02:00
parent fcaf2038ad
commit 326062b3a0
10 changed files with 450 additions and 2 deletions

22
ht_booking/.dockerignore Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -1,6 +1,5 @@
**/config/local.yaml **/config/local.yaml
**/config/*.local.yaml **/config/*.local.yaml
**/config/production.yaml
# Generated by Cargo # Generated by Cargo
# will have compiled files and executables # will have compiled files and executables
@@ -19,8 +18,10 @@ target/
*.sqlite *.sqlite
*.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
.env.production
todo.md todo.md
*report.html *report.html

20
ht_booking/Caddyfile Normal file
View File

@@ -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
}

203
ht_booking/DEPLOY.md Normal file
View File

@@ -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 <server-ip>
www.tenisrajec.sk A <server-ip>
```
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 <your-git-remote-url> 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 <https://tenisrajec.sk> — 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.

42
ht_booking/Dockerfile Normal file
View File

@@ -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"]

27
ht_booking/Makefile Normal file
View File

@@ -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

View File

@@ -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 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`. 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 ## 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. You can check your [configuration](config/development.yaml) to pick either frontend setup or server-side rendered template, and activate the relevant configuration sections.

View File

@@ -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

View File

@@ -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