diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index ab4e5e5..e8d47b9 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build Production Images +name: Build Production Image on: pull_request_review: @@ -6,7 +6,7 @@ on: jobs: build: - name: Build Production Images + name: Build Production Image if: github.event.review.state == 'approved' && github.event.pull_request.base.ref == 'main' runs-on: ubuntu-latest steps: @@ -18,8 +18,5 @@ jobs: - name: Verify Docker CLI run: docker version - - name: Build backend image - run: docker build -t condado-newsletter-backend:latest -f backend/Dockerfile ./backend - - - name: Build frontend image - run: docker build -t condado-newsletter-frontend:latest -f frontend/Dockerfile ./frontend \ No newline at end of file + - name: Build all-in-one image + run: docker build -t condado-newsletter:latest -f Dockerfile.allinone . \ No newline at end of file diff --git a/.github/agents/infra.agent.md b/.github/agents/infra.agent.md index 72c05aa..ea5883e 100644 --- a/.github/agents/infra.agent.md +++ b/.github/agents/infra.agent.md @@ -15,14 +15,14 @@ You are a senior DevOps / infrastructure engineer and software architect for the | `backend/Dockerfile` | Backend-only multi-stage build image | | `frontend/Dockerfile` | Frontend build + Nginx image | | `docker-compose.yml` | Dev stack (postgres + backend + nginx + mailhog) | -| `docker-compose.prod.yml` | Prod stack (postgres + backend + nginx, no mailhog) | +| `docker-compose.prod.yml` | Prod stack (single all-in-one image) | | `nginx/nginx.conf` | Nginx config for multi-container compose flavours | | `nginx/nginx.allinone.conf` | Nginx config for the all-in-one image (localhost backend) | | `frontend/nginx.docker.conf` | Nginx config embedded in frontend image | | `docker/supervisord.conf` | Supervisor config (manages postgres + java + nginx inside allinone) | | `docker/entrypoint.sh` | Allinone container entrypoint (DB init, env wiring, supervisord start) | | `.gitea/workflows/ci.yml` | CI: backend tests + frontend tests on pull requests to `develop` | -| `.gitea/workflows/build.yml` | Build: create local backend/frontend images on approved PRs to `main` | +| `.gitea/workflows/build.yml` | Build: create the local all-in-one image on approved PRs to `main` | | `.env.example` | Template for all environment variables | ## System Topology @@ -54,7 +54,7 @@ Docker volume → /var/lib/postgresql/data | Flavour | Command | Notes | |---|---|---| | Dev | `docker compose up --build` | Includes Mailhog on :1025/:8025 | -| Prod (compose) | `docker compose -f docker-compose.prod.yml up -d` | External DB/SMTP using prebuilt local images | +| Prod (compose) | `docker compose -f docker-compose.prod.yml up -d` | Prebuilt all-in-one image with internal PostgreSQL | | All-in-one | `docker run -p 80:80 -e APP_PASSWORD=... ` | Everything in one container | ## Key Environment Variables @@ -81,9 +81,9 @@ All injected at runtime — never hardcoded in images. | Workflow | Trigger | What it does | |---|---|---| | `ci.yml` | Pull request to `develop` | Backend `./gradlew test` + Frontend `npm run test` | -| `build.yml` | Approved PR review to `main` | Builds `condado-newsletter-backend` and `condado-newsletter-frontend` on the target Docker host | +| `build.yml` | Approved PR review to `main` | Builds `condado-newsletter` on the target Docker host | -The runner shares the target Docker host, so this workflow produces local images directly on that host. `docker-compose.prod.yml` must reference images and not local build directives. +The runner shares the target Docker host, so this workflow produces the local `condado-newsletter` image directly on that host. `docker-compose.prod.yml` must reference that image and not local build directives. ## Implementation Rules diff --git a/CLAUDE.md b/CLAUDE.md index fd8a318..cafcdb3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,7 +83,7 @@ The cycle for every step is: | Reverse Proxy | Nginx (serves frontend + proxies `/api` to backend) | | Dev Mail | Mailhog (SMTP trap + web UI) | | All-in-one image | Single Docker image: Nginx + Spring Boot + PostgreSQL + Supervisor | -| Image registry | Local Docker images on the deployment host (`condado-newsletter-backend`, `condado-newsletter-frontend`) | +| Image registry | Local Docker image on the deployment host (`condado-newsletter`) | | CI/CD | Gitea Actions — test PRs to `develop`, deploy approved PRs targeting `main` | ## Deployment Flavours @@ -93,7 +93,7 @@ There are **three ways to run the project**: | Flavour | Command | When to use | |---------------------|---------------------------------|------------------------------------------------| | **Dev** | `docker compose up` | Local development — includes Mailhog | -| **Prod (compose)** | `docker compose -f docker-compose.prod.yml up -d` | Production with prebuilt backend/frontend images | +| **Prod (compose)** | `docker compose -f docker-compose.prod.yml up -d` | Production with the prebuilt all-in-one image | | **All-in-one** | `docker run ...` | Simplest deploy — everything in one container | ### All-in-one Image @@ -213,7 +213,7 @@ condado-news-letter/ ← repo root ├── .env.example ← template for all env vars ├── .gitignore ├── docker-compose.yml ← dev stack (Nginx + Backend + PostgreSQL + Mailhog) -├── docker-compose.prod.yml ← prod stack (Nginx + Backend + PostgreSQL) +├── docker-compose.prod.yml ← prod stack (single all-in-one image) ├── Dockerfile.allinone ← all-in-one image (Nginx + Backend + PostgreSQL + Supervisor) │ ├── .github/ @@ -575,9 +575,9 @@ Good examples: | Workflow file | Trigger | What it does | |----------------------------|----------------------------|-----------------------------------------------------------| | `.gitea/workflows/ci.yml` | PR to `develop` | Backend tests (`./gradlew test`) + Frontend tests (`npm run test`) | -| `.gitea/workflows/build.yml` | Approved PR review on `main` | Build `condado-newsletter-backend` and `condado-newsletter-frontend` locally on the runner host | +| `.gitea/workflows/build.yml` | Approved PR review on `main` | Build `condado-newsletter` locally on the runner host | -Build policy: the runner shares the target Docker host, so the build workflow produces local Docker images directly on that host. `docker-compose.prod.yml` is image-based and can be started separately without build directives. +Build policy: the runner shares the target Docker host, so the build workflow produces the local `condado-newsletter` image directly on that host. `docker-compose.prod.yml` is image-based and can be started separately without build directives. --- diff --git a/Dockerfile.allinone b/Dockerfile.allinone index 5f411d3..b842548 100644 --- a/Dockerfile.allinone +++ b/Dockerfile.allinone @@ -15,6 +15,7 @@ FROM gradle:8-jdk21-alpine AS backend-build WORKDIR /app/backend COPY backend/build.gradle.kts backend/settings.gradle.kts ./ +COPY backend/gradle.properties ./ COPY backend/gradle ./gradle RUN gradle dependencies --no-daemon --quiet || true diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index cc16bfa..9a85feb 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,33 +1,10 @@ services: - - # ── PostgreSQL ─────────────────────────────────────────────────────────────── - postgres: - image: postgres:16-alpine - restart: always - environment: - POSTGRES_DB: condado - POSTGRES_USER: ${SPRING_DATASOURCE_USERNAME} - POSTGRES_PASSWORD: ${SPRING_DATASOURCE_PASSWORD} - volumes: - - postgres-data:/var/lib/postgresql/data - networks: - - condado-net - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${SPRING_DATASOURCE_USERNAME} -d condado"] - interval: 10s - timeout: 5s - retries: 5 - - # ── Backend (Spring Boot) ──────────────────────────────────────────────────── - backend: - image: condado-newsletter-backend:latest - restart: always - depends_on: - postgres: - condition: service_healthy + condado-newsletter: + image: condado-newsletter:latest + container_name: condado-newsletter + restart: unless-stopped environment: SPRING_PROFILES_ACTIVE: prod - SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL} SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME} SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD} APP_PASSWORD: ${APP_PASSWORD} @@ -48,23 +25,22 @@ services: extra_hosts: - "celtinha.desktop:host-gateway" - "host.docker.internal:host-gateway" - networks: - - condado-net - - # ── Frontend + Nginx ───────────────────────────────────────────────────────── - nginx: - image: condado-newsletter-frontend:latest - restart: always - ports: - - "80:80" - depends_on: - - backend - networks: - - condado-net + volumes: + - postgres-data:/var/lib/postgresql/data + labels: + - "traefik.enable=true" + - "traefik.http.routers.condado.rule=Host(`condado-newsletter.lab`)" + - "traefik.http.services.condado.loadbalancer.server.port=80" + - "homepage.group=Hyperlink" + - "homepage.name=Condado Newsletter" + - "homepage.description=Automated newsletter generator using AI" + - "homepage.logo=https://raw.githubusercontent.com/celtinha/condado-newsletter/main/docs/logo.png" + - "homepage.url=http://condado-newsletter.lab" volumes: postgres-data: networks: - condado-net: - driver: bridge + default: + name: traefik + external: true diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 0493261..6a6f2c1 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,6 +1,10 @@ #!/bin/bash set -e +APP_DB_NAME=${APP_DB_NAME:-condado} +APP_DB_USER=${SPRING_DATASOURCE_USERNAME:-condado} +APP_DB_PASSWORD=${SPRING_DATASOURCE_PASSWORD:-condado} + # ── Initialise PostgreSQL data directory on first run ───────────────────────── if [ ! -f /var/lib/postgresql/data/PG_VERSION ]; then echo "Initialising PostgreSQL data directory..." @@ -9,8 +13,8 @@ if [ ! -f /var/lib/postgresql/data/PG_VERSION ]; then # Start postgres temporarily to create the app database and user su -c "/usr/lib/postgresql/16/bin/pg_ctl -D /var/lib/postgresql/data -w start" postgres - su -c "psql -c \"CREATE USER condado WITH PASSWORD 'condado';\"" postgres - su -c "psql -c \"CREATE DATABASE condado OWNER condado;\"" postgres + su -c "psql -v ON_ERROR_STOP=1 -c \"CREATE USER ${APP_DB_USER} WITH PASSWORD '${APP_DB_PASSWORD}';\"" postgres + su -c "psql -v ON_ERROR_STOP=1 -c \"CREATE DATABASE ${APP_DB_NAME} OWNER ${APP_DB_USER};\"" postgres su -c "/usr/lib/postgresql/16/bin/pg_ctl -D /var/lib/postgresql/data -w stop" postgres echo "PostgreSQL initialised." @@ -20,9 +24,9 @@ fi mkdir -p /var/log/supervisor # ── Defaults for all-in-one local PostgreSQL ───────────────────────────────── -export SPRING_DATASOURCE_URL=${SPRING_DATASOURCE_URL:-jdbc:postgresql://localhost:5432/condado} -export SPRING_DATASOURCE_USERNAME=${SPRING_DATASOURCE_USERNAME:-condado} -export SPRING_DATASOURCE_PASSWORD=${SPRING_DATASOURCE_PASSWORD:-condado} +export SPRING_DATASOURCE_URL=${SPRING_DATASOURCE_URL:-jdbc:postgresql://localhost:5432/${APP_DB_NAME}} +export SPRING_DATASOURCE_USERNAME=${SPRING_DATASOURCE_USERNAME:-${APP_DB_USER}} +export SPRING_DATASOURCE_PASSWORD=${SPRING_DATASOURCE_PASSWORD:-${APP_DB_PASSWORD}} # ── Start all services via supervisord ─────────────────────────────────────── exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf