feat: add Docker Compose setup with dev and prod configurations
- docker-compose.yml (dev): 3 services — postgres:16, backend (gradle bootRun with JDK 21, spring-boot-devtools), frontend (Vite HMR on node:24-alpine). Source volume mounts for live editing, Gradle cache volume for fast rebuilds, pg_isready healthcheck on postgres. - docker-compose.prod.yml (prod): same 3 services but with multi-stage Dockerfiles. Backend: Gradle bootJar → JRE Alpine, non-root user. Frontend: npm ci + vite build → nginx:alpine serving static dist/. SSL termination via self-signed cert (auto-generated in entrypoint). No source mounts, restart: unless-stopped, separate volumes. - application-docker.yml: Spring profile overriding H2 with PostgreSQL via env vars. Hostname "postgres" resolved by Docker Compose DNS. - Vite proxy /api → backend:8080 for dev. nginx nginx.conf handles /api proxy + SPA fallback + gzip + SSL in prod. - AGENTS.md, README.md: architecture diagram, dev vs prod comparison table, Spring profiles docs, file reference updates.
This commit is contained in:
parent
9931061cb6
commit
4d449d54d0
14 changed files with 293 additions and 15 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -37,6 +37,7 @@ Thumbs.db
|
|||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
certs/
|
||||
|
||||
# Java
|
||||
*.hprof
|
||||
|
|
|
|||
19
AGENTS.md
19
AGENTS.md
|
|
@ -15,7 +15,7 @@ the recipient's name or address.
|
|||
integration yet. Owner address is obtained manually by a human and entered into
|
||||
the admin panel.
|
||||
|
||||
Tech stack: Vue.js 3 (Vite, Pinia) frontend + Java 21 Spring Boot 3 backend +
|
||||
Tech stack: Vue.js 3 (Vite, Pinia) frontend + Java 21 Spring Boot 4 backend +
|
||||
PostgreSQL 16. Deployed via Docker Compose.
|
||||
|
||||
---
|
||||
|
|
@ -80,7 +80,7 @@ bilhej/
|
|||
│ │ ├── router/ # Vue Router config
|
||||
│ │ └── assets/ # Static files, CSS
|
||||
│ └── ...
|
||||
├── backend/ # Spring Boot 3 (Java 21)
|
||||
├── backend/ # Spring Boot 4 (Java 21)
|
||||
│ ├── src/main/java/se/bilhalsning/
|
||||
│ │ ├── config/ # @Configuration classes
|
||||
│ │ ├── controller/ # REST controllers
|
||||
|
|
@ -92,11 +92,18 @@ bilhej/
|
|||
│ │ ├── exception/ # Custom exceptions + @ControllerAdvice
|
||||
│ │ └── mapper/ # Entity ↔ DTO mapping
|
||||
│ └── src/main/resources/
|
||||
│ ├── application.yml
|
||||
│ └── db/migration/ # Flyway migrations
|
||||
│ ├── application.yml # default (H2, IDE dev)
|
||||
│ ├── application-docker.yml # docker profile (PostgreSQL)
|
||||
│ └── db/migration/ # Flyway migrations
|
||||
├── docker/ # Dockerfiles
|
||||
├── docker-compose.yml
|
||||
├── docker-compose.prod.yml
|
||||
│ ├── backend.Dockerfile # dev: JDK 21 + gradle bootRun
|
||||
│ ├── backend.prod.Dockerfile # prod: multi-stage (Gradle build → JRE Alpine, non-root)
|
||||
│ ├── frontend.Dockerfile # dev: Node 24 + vite dev server
|
||||
│ ├── frontend.prod.Dockerfile # prod: multi-stage (Node build → nginx)
|
||||
│ ├── nginx.conf # prod: SPA fallback + /api reverse proxy
|
||||
│ └── entrypoint.sh # prod: self-signed cert generation
|
||||
├── docker-compose.yml # dev: postgres + backend (bootRun) + frontend (Vite HMR)
|
||||
├── docker-compose.prod.yml # prod: multi-stage images, no source mounts, restart always
|
||||
├── .env.example
|
||||
├── AGENTS.md # This file
|
||||
├── README.md
|
||||
|
|
|
|||
74
README.md
74
README.md
|
|
@ -11,7 +11,7 @@ The user enters a registration number, composes a letter (from a template or fre
|
|||
| Layer | Technology |
|
||||
|-------------|-----------------------------------------|
|
||||
| Frontend | Vue.js 3 (Composition API), Vite, Pinia |
|
||||
| Backend | Java 21, Spring Boot 4 |
|
||||
| Backend | Java 21, Spring Boot 4, Gradle |
|
||||
| Database | PostgreSQL 16 |
|
||||
| Auth | Spring Security + JWT |
|
||||
| Payments | Stripe (cards + Swish) |
|
||||
|
|
@ -42,6 +42,40 @@ The app will be available at:
|
|||
- 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 bootRun`) |
|
||||
| `docker` | PostgreSQL via `docker-compose.yml` | Docker Compose dev |
|
||||
| `prod` | PostgreSQL (production config) | Deploy (`docker-compose.prod.yml`) |
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
|
@ -75,11 +109,11 @@ bilhej/
|
|||
│ │ ├── api/ # API client and endpoints
|
||||
│ │ ├── assets/ # Static assets, CSS
|
||||
│ │ ├── App.vue
|
||||
│ │ └── main.js
|
||||
│ │ └── main.ts
|
||||
│ ├── index.html
|
||||
│ ├── vite.config.js
|
||||
│ ├── vite.config.ts
|
||||
│ └── package.json
|
||||
├── backend/ # Spring Boot 3
|
||||
├── backend/ # Spring Boot 4
|
||||
│ ├── src/main/java/se/bilhalsning/
|
||||
│ │ ├── BilHejApplication.java
|
||||
│ │ ├── config/ # Security, CORS, Stripe config
|
||||
|
|
@ -90,12 +124,18 @@ bilhej/
|
|||
│ │ ├── service/ # Business logic
|
||||
│ │ └── security/ # JWT filter, user details
|
||||
│ └── src/main/resources/
|
||||
│ ├── application.yml
|
||||
│ └── db/migration/ # Flyway migrations
|
||||
├── docker-compose.yml
|
||||
│ ├── 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
|
||||
│ └── frontend.Dockerfile
|
||||
│ ├── backend.Dockerfile # dev: JDK + gradle 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
|
||||
├── .env.example
|
||||
├── README.md
|
||||
├── REQUIREMENTS.md
|
||||
|
|
@ -105,6 +145,22 @@ bilhej/
|
|||
|
||||
---
|
||||
|
||||
## Development vs Production
|
||||
|
||||
| Aspect | `docker compose up -d` | `docker compose -f docker-compose.prod.yml up -d` |
|
||||
|--------|------------------------|---------------------------------------------------|
|
||||
| Backend | `./gradlew 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 |
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### Frontend (dev server with HMR)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ dependencies {
|
|||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
|
||||
implementation 'org.flywaydb:flyway-database-postgresql'
|
||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
runtimeOnly 'com.h2database:h2'
|
||||
runtimeOnly 'org.postgresql:postgresql'
|
||||
|
|
|
|||
13
backend/src/main/resources/application-docker.yml
Normal file
13
backend/src/main/resources/application-docker.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
spring:
|
||||
datasource:
|
||||
url: jdbc:postgresql://postgres:5432/${POSTGRES_DB}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
username: ${POSTGRES_USER}
|
||||
password: ${POSTGRES_PASSWORD}
|
||||
|
||||
h2:
|
||||
console:
|
||||
enabled: false
|
||||
|
||||
jpa:
|
||||
database-platform: org.hibernate.dialect.PostgreSQLDialect
|
||||
57
docker-compose.prod.yml
Normal file
57
docker-compose.prod.yml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
container_name: bilhej-postgres-prod
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- pgdata-prod:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
backend:
|
||||
build:
|
||||
dockerfile: docker/backend.prod.Dockerfile
|
||||
context: .
|
||||
container_name: bilhej-backend-prod
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
SPRING_PROFILES_ACTIVE: docker
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
|
||||
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
|
||||
STRIPE_PRICE_ID: ${STRIPE_PRICE_ID}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
frontend:
|
||||
build:
|
||||
dockerfile: docker/frontend.prod.Dockerfile
|
||||
context: .
|
||||
container_name: bilhej-frontend-prod
|
||||
ports:
|
||||
- "3000:80"
|
||||
- "443:443"
|
||||
depends_on:
|
||||
- backend
|
||||
volumes:
|
||||
- certs:/etc/nginx/certs
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
pgdata-prod:
|
||||
certs:
|
||||
58
docker-compose.yml
Normal file
58
docker-compose.yml
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
container_name: bilhej-postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
backend:
|
||||
build:
|
||||
dockerfile: docker/backend.Dockerfile
|
||||
context: .
|
||||
container_name: bilhej-backend
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
SPRING_PROFILES_ACTIVE: docker
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
|
||||
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
|
||||
STRIPE_PRICE_ID: ${STRIPE_PRICE_ID}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- gradle-cache:/root/.gradle
|
||||
|
||||
frontend:
|
||||
build:
|
||||
dockerfile: docker/frontend.Dockerfile
|
||||
context: .
|
||||
container_name: bilhej-frontend
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
- backend
|
||||
volumes:
|
||||
- ./frontend/src:/app/src
|
||||
- ./frontend/public:/app/public
|
||||
- ./frontend/index.html:/app/index.html
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
gradle-cache:
|
||||
3
docker/backend.Dockerfile
Normal file
3
docker/backend.Dockerfile
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
FROM eclipse-temurin:21-jdk
|
||||
WORKDIR /app
|
||||
ENTRYPOINT ["./gradlew", "bootRun", "--no-daemon"]
|
||||
16
docker/backend.prod.Dockerfile
Normal file
16
docker/backend.prod.Dockerfile
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
FROM eclipse-temurin:21-jdk AS builder
|
||||
WORKDIR /app
|
||||
COPY backend/gradlew ./
|
||||
COPY backend/gradle/ ./gradle/
|
||||
COPY backend/build.gradle backend/settings.gradle ./
|
||||
RUN chmod +x gradlew && ./gradlew dependencies --no-daemon -q
|
||||
COPY backend/src ./src
|
||||
RUN ./gradlew bootJar --no-daemon -q
|
||||
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
RUN addgroup -S bilhej && adduser -S bilhej -G bilhej
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/build/libs/*-SNAPSHOT.jar ./app.jar
|
||||
USER bilhej
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
10
docker/entrypoint.sh
Normal file
10
docker/entrypoint.sh
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/sh
|
||||
CERT_DIR="/etc/nginx/certs"
|
||||
if [ ! -f "$CERT_DIR/cert.crt" ] || [ ! -f "$CERT_DIR/cert.key" ]; then
|
||||
mkdir -p "$CERT_DIR"
|
||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||
-keyout "$CERT_DIR/cert.key" \
|
||||
-out "$CERT_DIR/cert.crt" \
|
||||
-subj "/CN=localhost"
|
||||
fi
|
||||
exec /docker-entrypoint.sh "$@"
|
||||
7
docker/frontend.Dockerfile
Normal file
7
docker/frontend.Dockerfile
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
FROM node:24-alpine
|
||||
WORKDIR /app
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm install
|
||||
COPY frontend/ .
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||
16
docker/frontend.prod.Dockerfile
Normal file
16
docker/frontend.prod.Dockerfile
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
FROM node:24-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY frontend/ .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
RUN apk add --no-cache openssl
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
EXPOSE 80 443
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
30
docker/nginx.conf
Normal file
30
docker/nginx.conf
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
server {
|
||||
listen 80;
|
||||
listen 443 ssl;
|
||||
server_name _;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/cert.crt;
|
||||
ssl_certificate_key /etc/nginx/certs/cert.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
|
||||
gzip_vary on;
|
||||
gzip_min_length 256;
|
||||
}
|
||||
|
|
@ -12,6 +12,9 @@ export default defineConfig({
|
|||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': 'http://backend:8080',
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
|
|
|
|||
Loading…
Reference in a new issue