bilhej/README.md
Joakim Mörling 7938a1620b
All checks were successful
CI / Lint, type check, unit tests, coverage (push) Successful in 1m50s
CI / E2E browser tests (push) Successful in 45s
docs: add production deployment guide to README
Adds a comprehensive 'Production Deployment' section covering:

- One-time server setup (Forgejo secrets, DNS, SSL certbot, nginx config)
- How to trigger a deploy from the Forgejo Actions UI
- What the deploy pipeline does step-by-step
- Architecture diagram showing how nginx, frontend, backend, and postgres
  containers interact on the production server
- Rollback procedure using git tags and docker compose

This documents the deploy.yml workflow and bilhej.nginx.conf added in
the previous commit.
2026-05-20 11:28:38 +02:00

317 lines
12 KiB
Markdown

# BilHej / Bilhälsning.se
Send a physical letter to a Swedish car owner — just by knowing their license plate.
The user enters a registration number, composes a letter (from a template or free text), pays, and BilHej handles the rest: owner address lookup via Transportstyrelsen, printing and mailing via PostNord. The sender never sees the recipient's name or address.
---
## Tech Stack
| Layer | Technology |
|-------------|-----------------------------------------|
| Frontend | Vue.js 3 (Composition API), Vite, Pinia |
| Backend | Java 21, Spring Boot 4, Gradle |
| Database | PostgreSQL 16 |
| Auth | Spring Security + JWT |
| Payments | Stripe (cards + Swish) |
| Deployment | Docker, Docker Compose |
---
## Prerequisites
- Docker & Docker Compose
- Java 21 (for local IDE development)
- Node.js 20+ (for local frontend dev)
- A [Stripe](https://stripe.com) account (test mode for development)
---
## Quick Start
```bash
git clone <repo-url> bilhej
cd bilhej
cp .env.example .env # fill in your keys
docker compose up -d # or: ./gradlew up
```
The app will be available at:
- Frontend: `http://localhost:3000`
- Backend API: `http://localhost:8080`
- PostgreSQL: `localhost:5432`
### Architecture inside Docker Compose
```
Browser Docker Compose network
─────── ─────────────────────
│ ┌──────────────────┐
│ http://localhost:3000 │ frontend (Vite) │
├────────────────────────→│ :3000 │
│ │ proxy: /api → │
│ GET /api/orders │ backend:8080 │
│ └────────┬─────────┘
│ │
│ ┌────────▼─────────┐
│ │ backend (Spring) │
│ │ :8080 │
│ │ profile: docker │
│ └────────┬─────────┘
│ │
│ ┌────────▼─────────┐
│ │ postgres (16) │
│ │ :5432 │
│ └──────────────────┘
```
**Vite proxy:** The Vite dev server proxies `/api/*` requests to the backend container.
No CORS configuration needed in development — the browser never calls the backend directly.
**Spring profiles:**
| Profile | Datasource | Use |
|---------|-----------|-----|
| default | H2 in-memory | Local IDE dev (`./gradlew :backend:bootRun`) |
| `docker` | PostgreSQL via `docker-compose.yml` | Docker Compose dev |
| `prod` | PostgreSQL (production config) | Deploy (`docker-compose.prod.yml`) |
---
## Environment Variables
Copy `.env.example` to `.env` and fill in:
| Variable | Description |
|---------------------------|--------------------------------------|
| `POSTGRES_DB` | Database name (default: `bilhej`) |
| `POSTGRES_USER` | Database user |
| `POSTGRES_PASSWORD` | Database password |
| `JWT_SECRET` | Secret key for JWT signing |
| `STRIPE_SECRET_KEY` | Stripe secret key |
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret |
| `STRIPE_PRICE_ID` | Stripe price ID for single letter |
---
## Project Structure
```
bilhej/
├── frontend/ # Vue.js 3 SPA
│ ├── src/
│ │ ├── components/ # Reusable UI components
│ │ ├── composables/ # Shared composition functions
│ │ ├── layouts/ # Page layouts
│ │ ├── pages/ # Route-level page components
│ │ ├── router/ # Vue Router config
│ │ ├── stores/ # Pinia stores
│ │ ├── api/ # API client and endpoints
│ │ ├── assets/ # Static assets, CSS
│ │ ├── App.vue
│ │ └── main.ts
│ ├── index.html
│ ├── vite.config.ts
│ └── package.json
├── backend/ # Spring Boot 4
│ ├── src/main/java/se/bilhalsning/
│ │ ├── BilHejApplication.java
│ │ ├── config/ # Security, CORS, Stripe config
│ │ ├── controller/ # REST controllers
│ │ ├── dto/ # Data transfer objects
│ │ ├── entity/ # JPA entities
│ │ ├── repository/ # Spring Data repositories
│ │ ├── service/ # Business logic
│ │ └── security/ # JWT filter, user details
│ └── src/main/resources/
│ ├── application.yml # default profile (H2)
│ ├── application-docker.yml # docker profile (PostgreSQL)
│ └── db/migration/ # Flyway migrations
├── docker-compose.yml # dev: postgres + backend (bootRun) + frontend (Vite HMR)
├── docker-compose.prod.yml # prod: multi-stage builds, no source mounts, restart: unless-stopped
├── docker/
│ ├── backend.Dockerfile # dev: JDK + gradle :backend:bootRun
│ ├── backend.prod.Dockerfile # prod: multi-stage (Gradle → JRE Alpine, non-root)
│ ├── frontend.Dockerfile # dev: Node + vite dev server
│ ├── frontend.prod.Dockerfile # prod: multi-stage (Node → nginx)
│ ├── nginx.conf # prod: SPA fallback + /api proxy
│ └── entrypoint.sh # prod: self-signed cert generation
├── gradlew # Gradle wrapper (run from repo root)
├── gradle/
│ └── wrapper/
├── settings.gradle # rootProject.name + include 'backend'
├── build.gradle # convenience tasks: check, up, down, reset
├── .env.example
├── README.md
├── REQUIREMENTS.md
└── CODING_GUIDELINES.md
```
---
## Development vs Production
| Aspect | `docker compose up -d` | `docker compose -f docker-compose.prod.yml up -d` |
|--------|------------------------|---------------------------------------------------|
| Backend | `./gradlew :backend:bootRun` (compiles on change) | Multi-stage build → `java -jar app.jar` |
| Backend image | `eclipse-temurin:21-jdk` (~400 MB) | `eclipse-temurin:21-jre-alpine` (~200 MB) |
| Backend user | root | `bilhej` (non-root) |
| Frontend | Vite dev server (HMR, `--host 0.0.0.0`) | nginx serving static `dist/` |
| API proxy | Vite built-in proxy (`/api` → `backend:8080`) | nginx `proxy_pass` |
| SSL | None | Self-signed cert auto-generated on first start (`.certs/` volume) |
| Source mounts | Yes (live edit) | No (files baked into image) |
| Restart policy | Manual | `unless-stopped` |
| Purpose | Write code, instant feedback | Verify production build |
---
## Production Deployment
Deployments are fully automated via Forgejo Actions. The pipeline builds production Docker images and starts them on the server.
### One-time Setup
Before the first deploy, complete these steps on the production server (`srvr.nu`):
1. **Add Forgejo Actions Secrets**
Go to **Forgejo → Repository Settings → Actions → Secrets** and add:
| Secret | Description |
|--------|-------------|
| `POSTGRES_DB` | Database name (e.g., `bilhej`) |
| `POSTGRES_USER` | Database user |
| `POSTGRES_PASSWORD` | Strong database password |
| `JWT_SECRET` | `openssl rand -hex 32` |
| `STRIPE_SECRET_KEY` | Stripe secret key |
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret |
| `STRIPE_PRICE_ID` | Stripe price ID for single letter |
| `SWISH_NUMBER` | Swish phone number for payment instructions |
2. **Point DNS**
Set `bilhej.se` (and `www.bilhej.se`) A record to the server's public IP.
3. **Obtain SSL Certificate**
Run certbot in the nginx container:
```bash
docker exec certbot certbot certonly \
--webroot -w /var/www/certbot \
-d bilhej.se -d www.bilhej.se
```
4. **Add Nginx Config**
Copy the Bilhej server block into the nginx container:
```bash
docker cp docker/bilhej.nginx.conf nginx:/etc/nginx/conf.d/bilhej.conf
docker exec nginx nginx -s reload
```
### Deploy
1. Go to **Actions → Deploy to Production** in Forgejo.
2. Click **Run workflow**.
3. Enter a version tag (e.g., `v0.1.0`).
4. Click **Run workflow**.
### What Happens
| Step | Action |
|------|--------|
| Tag | Git tag `v0.1.0` is created and pushed |
| Build | Production backend JAR and frontend bundle are built |
| Images | Multi-stage Docker images are built locally on the server |
| Start | `docker compose -f docker-compose.prod.yml up -d` |
| Verify | Health checks confirm backend API and frontend are responding |
### Architecture on Server
```
User
│ https://bilhej.se
┌─────────────────────────────────────┐
│ nginx (srvr.nu) │
│ SSL termination (Let's Encrypt) │
│ proxy_pass → bilhej-frontend-prod │
└─────────────┬───────────────────────┘
│ Docker 'web' network
┌─────────────▼───────────────────────┐
│ bilhej-frontend-prod (nginx) │
│ :80 │
│ /api/* → bilhej-backend-prod:8080 │
└─────────────┬───────────────────────┘
┌─────────────▼───────────────────────┐
│ bilhej-backend-prod (Spring Boot) │
│ :8080 │
└─────────────┬───────────────────────┘
┌─────────────▼───────────────────────┐
│ bilhej-postgres-prod (PostgreSQL) │
│ :5432 │
└─────────────────────────────────────┘
```
### Rollback
To rollback to a previous version:
```bash
# On srvr.nu
cd /path/to/bilhej/repo
git fetch --tags
git checkout v0.1.0 # or any previous tag
docker compose -f docker-compose.prod.yml up --build -d
```
---
## Development
### All-in-one (from repo root)
```bash
./gradlew check # lint → frontend test → backend test → integration test
./gradlew up # docker compose up -d
./gradlew down # docker compose down
./gradlew reset # docker compose down -v && docker compose up -d (full DB reset)
```
### Frontend (dev server with HMR)
```bash
cd frontend
npm install # first time only
npm run dev # :3000 with HMR
```
### Backend (IDE or CLI)
```bash
./gradlew :backend:bootRun # :8080, profile: default (H2)
```
### Stripe Webhooks (local testing)
```bash
stripe listen --forward-to localhost:8080/api/webhooks/stripe
```
### Database reset
```bash
./gradlew reset # wipes DB volume and restarts containers
```
---
## Related Documents
- [REQUIREMENTS.md](./REQUIREMENTS.md) — Full product requirements and business model
- [CODING_GUIDELINES.md](./CODING_GUIDELINES.md) — Code conventions and standards