Skip to content

Commit 33e2987

Browse files
committed
Add Docker packaging and CI pipeline
1 parent 84265eb commit 33e2987

File tree

16 files changed

+3290
-1
lines changed

16 files changed

+3290
-1
lines changed

.dockerignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Root VCS metadata and docs
2+
.git
3+
.gitmodules
4+
.github
5+
6+
# Local configuration files
7+
.env
8+
.env.*
9+
10+
# Development assets not needed in build context
11+
compose/
12+
scripts/
13+
14+
# Module internals
15+
app/urlaubsverwaltung/.git
16+
app/urlaubsverwaltung/.github
17+
app/urlaubsverwaltung/target/
18+
19+
# OS noise
20+
**/.DS_Store
21+
**/Thumbs.db

.env.example

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Global defaults consumed by Docker Compose and GitHub Actions.
2+
# Copy to .env and override per environment.
3+
4+
# --- Core Spring Boot configuration ---
5+
SERVER_PORT=8080
6+
SPRING_PROFILES_ACTIVE=default
7+
SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/urlaubsverwaltung
8+
SPRING_DATASOURCE_USERNAME=urlaubsverwaltung
9+
SPRING_DATASOURCE_PASSWORD=urlaubsverwaltung
10+
11+
SPRING_MAIL_HOST=mailpit
12+
SPRING_MAIL_PORT=1025
13+
SPRING_MAIL_USERNAME=
14+
SPRING_MAIL_PASSWORD=
15+
MANAGEMENT_HEALTH_MAIL_ENABLED=false
16+
17+
UV_MAIL_FROM=[email protected]
18+
UV_MAIL_FROMDISPLAYNAME=Urlaubsverwaltung
19+
UV_MAIL_REPLYTO=[email protected]
20+
UV_MAIL_REPLYTODISPLAYNAME=Urlaubsverwaltung
21+
UV_MAIL_APPLICATIONURL=http://localhost:8080
22+
UV_CALENDAR_ORGANIZER=[email protected]
23+
24+
# --- Database (docker compose) ---
25+
POSTGRES_DB=urlaubsverwaltung
26+
POSTGRES_USER=urlaubsverwaltung
27+
POSTGRES_PASSWORD=urlaubsverwaltung
28+
29+
# --- Mailpit (docker compose) ---
30+
MAILPIT_DASHBOARD_PORT=8025
31+
MAILPIT_SMTP_PORT=1025
32+
33+
# --- Optional OpenID Connect (docker-compose.oidc.yml) ---
34+
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_DEFAULT_CLIENT_ID=urlaubsverwaltung
35+
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_DEFAULT_CLIENT_SECRET=urlaubsverwaltung-secret
36+
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_DEFAULT_CLIENT_NAME=urlaubsverwaltung
37+
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_DEFAULT_SCOPE=openid,profile,email,roles
38+
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_DEFAULT_AUTHORIZATION_GRANT_TYPE=authorization_code
39+
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_DEFAULT_REDIRECT_URI=http://localhost:8080/login/oauth2/code/{registrationId}
40+
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_DEFAULT_ISSUER_URI=http://keycloak:8080/realms/urlaubsverwaltung
41+
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=http://keycloak:8080/realms/urlaubsverwaltung
42+
UV_SECURITY_OIDC_CLAIM_MAPPERS_GROUP_CLAIM_ENABLED=true
43+
UV_SECURITY_OIDC_CLAIM_MAPPERS_GROUP_CLAIM_CLAIM_NAME=groups
44+
45+
# --- Docker image publication ---
46+
IMAGE_REGISTRY=docker.io
47+
IMAGE_NAME=flex420/urlaubsverwaltung
48+
IMAGE_TAG=dev

.github/workflows/ci.yml

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
name: Build and Publish
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
tags: [ 'v*' ]
7+
pull_request:
8+
branches: [ main ]
9+
10+
env:
11+
IMAGE_NAME: flex420/urlaubsverwaltung
12+
13+
jobs:
14+
ci:
15+
runs-on: ubuntu-latest
16+
permissions:
17+
contents: read
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
with:
22+
fetch-depth: 0
23+
submodules: recursive
24+
25+
- name: Set up Java
26+
uses: actions/setup-java@v4
27+
with:
28+
distribution: temurin
29+
java-version: '21'
30+
31+
- name: Cache Maven repository
32+
uses: actions/cache@v4
33+
with:
34+
path: ~/.m2/repository
35+
key: ${{ runner.os }}-m2-${{ hashFiles('app/urlaubsverwaltung/pom.xml') }}
36+
restore-keys: ${{ runner.os }}-m2-
37+
38+
- name: Run unit tests
39+
run: ./app/urlaubsverwaltung/mvnw -B -DskipITs test
40+
41+
- name: Verify Docker and Compose
42+
run: bash ./scripts/verify.sh
43+
44+
- name: Extract project version
45+
id: version
46+
run: |
47+
VERSION=$(./app/urlaubsverwaltung/mvnw -q help:evaluate -Dexpression=project.version -DforceStdout)
48+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
49+
if [[ "$VERSION" == *-SNAPSHOT ]]; then
50+
echo "sanitized=${VERSION%-SNAPSHOT}" >> "$GITHUB_OUTPUT"
51+
else
52+
echo "sanitized=${VERSION}" >> "$GITHUB_OUTPUT"
53+
fi
54+
55+
- name: Compute image tags
56+
id: tags
57+
run: |
58+
IMAGE=${{ env.IMAGE_NAME }}
59+
VERSION=${{ steps.version.outputs.sanitized }}
60+
SHORT_SHA=${GITHUB_SHA::12}
61+
TAGS="$IMAGE:$VERSION\n$IMAGE:sha-$SHORT_SHA"
62+
if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then
63+
if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then
64+
TAGS="$TAGS\n$IMAGE:latest"
65+
elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then
66+
REF_TAG="${GITHUB_REF#refs/tags/}"
67+
TAGS="$TAGS\n$IMAGE:$REF_TAG"
68+
fi
69+
fi
70+
printf 'list<<EOF\n%s\nEOF\n' "$TAGS" >> "$GITHUB_OUTPUT"
71+
72+
- name: Login to Docker Hub
73+
if: github.event_name == 'push'
74+
uses: docker/login-action@v3
75+
with:
76+
username: ${{ secrets.DOCKERHUB_USERNAME }}
77+
password: ${{ secrets.DOCKERHUB_TOKEN }}
78+
79+
- name: Build and (optionally) push image
80+
id: build
81+
uses: docker/build-push-action@v5
82+
with:
83+
context: .
84+
push: ${{ github.event_name == 'push' }}
85+
load: true
86+
tags: ${{ steps.tags.outputs.list }}
87+
cache-from: type=gha
88+
cache-to: type=gha,mode=max
89+
90+
- name: Generate SBOM
91+
uses: anchore/sbom-action@v0
92+
with:
93+
image: ${{ env.IMAGE_NAME }}:${{ steps.version.outputs.sanitized }}
94+
format: spdx-json
95+
output-file: sbom.spdx.json
96+
97+
- name: Upload SBOM artifact
98+
uses: actions/upload-artifact@v4
99+
with:
100+
name: sbom
101+
path: sbom.spdx.json

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "app/urlaubsverwaltung"]
2+
path = app/urlaubsverwaltung
3+
url = https://github.com/urlaubsverwaltung/urlaubsverwaltung.git

Dockerfile

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# syntax=docker/dockerfile:1.7
2+
3+
ARG BUILDER_IMAGE=eclipse-temurin:21-jdk
4+
ARG RUNTIME_IMAGE=eclipse-temurin:21-jre
5+
6+
FROM ${BUILDER_IMAGE} AS builder
7+
8+
WORKDIR /workspace
9+
10+
# Warm up Maven dependency cache
11+
COPY app/urlaubsverwaltung/.mvn/ .mvn/
12+
COPY app/urlaubsverwaltung/mvnw mvnw
13+
COPY app/urlaubsverwaltung/pom.xml pom.xml
14+
RUN --mount=type=cache,target=/root/.m2 ./mvnw -B -DskipTests -DskipITs dependency:go-offline
15+
16+
# Copy sources and build the Spring Boot fat jar
17+
COPY app/urlaubsverwaltung/. ./
18+
RUN --mount=type=cache,target=/root/.m2 ./mvnw -B -DskipTests -DskipITs package
19+
20+
FROM ${RUNTIME_IMAGE} AS runtime
21+
22+
# Install runtime dependencies needed for health checks
23+
RUN apt-get update \
24+
&& apt-get install --no-install-recommends --yes curl \
25+
&& rm -rf /var/lib/apt/lists/*
26+
27+
ENV APP_HOME=/opt/uv \
28+
TZ=UTC \
29+
JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75 -XX:MinRAMPercentage=25 -XX:InitialRAMPercentage=10 -XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -Djava.security.egd=file:/dev/urandom"
30+
ENV SPRING_PROFILES_ACTIVE=default
31+
ENV SERVER_PORT=8080
32+
33+
WORKDIR ${APP_HOME}
34+
35+
RUN groupadd --system uv \
36+
&& useradd --system --create-home --gid uv uv
37+
38+
COPY --from=builder /workspace/target/*.jar app.jar
39+
COPY config/ ./config/
40+
41+
USER uv:uv
42+
43+
EXPOSE 8080
44+
45+
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=5 \
46+
CMD curl --fail http://127.0.0.1:${SERVER_PORT}/actuator/health/readiness || exit 1
47+
48+
ENTRYPOINT ["java","-jar","app.jar"]

README.md

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Urlaubsantraege Platform
2+
3+
Lead-engineered packaging for the [urlaubsverwaltung](https://github.com/urlaubsverwaltung/urlaubsverwaltung) HR leave management application. This repository vendors the upstream source as a git submodule and adds everything required to build, ship, and operate the service with Docker, Docker Compose, and GitHub Actions.
4+
5+
## Repository layout
6+
7+
```
8+
app/urlaubsverwaltung # upstream source (git submodule)
9+
compose/ # docker compose stacks
10+
config/ # baseline Spring Boot configuration overlays
11+
.github/workflows/ # CI/CD pipelines
12+
scripts/ # helper scripts for local validation
13+
Dockerfile # production-grade image build
14+
.dockerignore # docker build context filter
15+
.env.example # base environment configuration
16+
README.md # this file
17+
```
18+
19+
Run `git submodule update --init --recursive` after cloning to hydrate `app/urlaubsverwaltung`.
20+
21+
## Upstream requirements snapshot
22+
23+
| Concern | Value |
24+
| --- | --- |
25+
| JDK | 21 (Temurin) |
26+
| Build tool | Maven Wrapper (`./mvnw`) |
27+
| Database | PostgreSQL 15.x |
28+
| Mail | SMTP (sample: Mailpit) |
29+
| Profiles | `default` (production), `demodata` (demo/dev) |
30+
| Optional auth | OpenID Connect (Keycloak example included) |
31+
32+
The upstream application exposes Spring Boot actuators at `/actuator/health`, `/actuator/health/readiness`, and `/actuator/health/liveness`. Our Docker image enables these endpoints for container health checks.
33+
34+
## Configuration defaults
35+
36+
Configuration lives under `config/` and is loaded by Spring when placed on the classpath (`--spring.config.additional-location=classpath:/config/`). Environment variables override every secret or deployment specific value.
37+
38+
| Environment variable | Purpose | Default |
39+
| --- | --- | --- |
40+
| `SERVER_PORT` | HTTP listener | `8080` |
41+
| `SPRING_PROFILES_ACTIVE` | Active Spring profiles | `default` (production) |
42+
| `SPRING_DATASOURCE_URL` | JDBC URL | `jdbc:postgresql://postgres:5432/urlaubsverwaltung` |
43+
| `SPRING_DATASOURCE_USERNAME` | DB user | `urlaubsverwaltung` |
44+
| `SPRING_DATASOURCE_PASSWORD` | DB password | `urlaubsverwaltung` |
45+
| `SPRING_MAIL_HOST` | SMTP host | `mailpit` |
46+
| `SPRING_MAIL_PORT` | SMTP port | `1025` |
47+
| `SPRING_MAIL_USERNAME` | SMTP user | _empty_ |
48+
| `SPRING_MAIL_PASSWORD` | SMTP password | _empty_ |
49+
| `UV_MAIL_FROM` | Sender email | `[email protected]` |
50+
| `UV_MAIL_FROMDISPLAYNAME` | Sender display name | `Urlaubsverwaltung` |
51+
| `UV_MAIL_REPLYTO` | Reply-to email | `[email protected]` |
52+
| `UV_MAIL_REPLYTODISPLAYNAME` | Reply-to display name | `Urlaubsverwaltung` |
53+
| `UV_MAIL_APPLICATIONURL` | Public base URL | `http://localhost:8080` |
54+
| `UV_CALENDAR_ORGANIZER` | Calendar organiser | `[email protected]` |
55+
| `MANAGEMENT_HEALTH_MAIL_ENABLED` | Disable expensive mail health probe | `false` |
56+
57+
Optional OpenID Connect variables (used in the OIDC compose stack):
58+
59+
| Environment variable | Purpose | Default (OIDC stack) |
60+
| --- | --- | --- |
61+
| `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_DEFAULT_CLIENT_ID` | OIDC client id | `urlaubsverwaltung` |
62+
| `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_DEFAULT_CLIENT_SECRET` | OIDC client secret | `urlaubsverwaltung-secret` |
63+
| `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_DEFAULT_CLIENT_NAME` | Display name | `urlaubsverwaltung` |
64+
| `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_DEFAULT_SCOPE` | Requested scopes | `openid,profile,email,roles` |
65+
| `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_DEFAULT_AUTHORIZATION_GRANT_TYPE` | Flow | `authorization_code` |
66+
| `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_DEFAULT_REDIRECT_URI` | Redirect template | `http://localhost:8080/login/oauth2/code/{registrationId}` |
67+
| `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_DEFAULT_ISSUER_URI` | Issuer URL | `http://keycloak:8080/realms/urlaubsverwaltung` |
68+
| `SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI` | Resource server issuer | `http://keycloak:8080/realms/urlaubsverwaltung` |
69+
| `UV_SECURITY_OIDC_CLAIM_MAPPERS_GROUP_CLAIM_ENABLED` | Enable group claim mapping | `true` |
70+
| `UV_SECURITY_OIDC_CLAIM_MAPPERS_GROUP_CLAIM_CLAIM_NAME` | Group claim name | `groups` |
71+
72+
All secrets (DB password, OIDC secret, SMTP credentials) must be provided via env vars or external secret managers. They never live in git.
73+
74+
## Building the Docker image
75+
76+
Requirements: Docker 24+, git submodules initialised.
77+
78+
```
79+
docker build \
80+
-t flex420/urlaubsverwaltung:local \
81+
.
82+
```
83+
84+
Key characteristics:
85+
86+
- Multi-stage build: Temurin 21 JDK for Maven build -> Temurin 21 JRE runtime.
87+
- Uses Maven wrapper with dependency download caching via buildkit.
88+
- Produces a non-root image (`uv` user, UID 1000) exposing port 8080.
89+
- Boots with sensible `JAVA_TOOL_OPTIONS` (container aware heap and GC tuning).
90+
- Declares an `HEALTHCHECK` hitting `/actuator/health/readiness`.
91+
92+
## Docker Compose stacks
93+
94+
We ship two Compose bundles under `compose/`.
95+
96+
### `docker-compose.dev.yml`
97+
98+
- Services: `app` (built locally), `postgres`, `mailpit`.
99+
- Activates `demodata` profile by default for seeded demo accounts.
100+
- Persists the database via the `urlaubsverwaltung-data` named volume.
101+
- Mailpit listens on `localhost:8025` (UI) and `localhost:1025` (SMTP).
102+
103+
Usage:
104+
105+
```
106+
cp .env.example .env
107+
# edit credentials if needed
108+
docker compose -f compose/docker-compose.dev.yml up --build
109+
```
110+
111+
Visit http://localhost:8080 and sign in with the pre-seeded demo users described in the upstream README (`[email protected]` / `secret`).
112+
113+
### `docker-compose.oidc.yml`
114+
115+
Extends the dev stack with Keycloak for OpenID Connect testing:
116+
117+
- Adds `keycloak` with an imported realm and demo users.
118+
- Binds Keycloak to `localhost:8090` and wires the issuer into the app.
119+
- Documents how to obtain tokens via `scripts/keycloak-demo.sh`.
120+
121+
Start it with:
122+
123+
```
124+
docker compose \
125+
-f compose/docker-compose.dev.yml \
126+
-f compose/docker-compose.oidc.yml \
127+
up --build
128+
```
129+
130+
## Helper scripts
131+
132+
`scripts/verify.sh` runs `docker build` and `docker compose config` validation locally. `scripts/keycloak-demo.sh` (requires curl and jq) demonstrates obtaining an OIDC token from the sample realm.
133+
134+
## CI/CD pipeline
135+
136+
`.github/workflows/ci.yml` performs:
137+
138+
1. Checks out this repo and pulls the upstream submodule.
139+
2. Executes the upstream unit test suite via `./app/urlaubsverwaltung/mvnw -B -DskipITs test`.
140+
3. Runs `scripts/verify.sh` to lint the Docker context and Compose files.
141+
4. Builds the production image with caching and pushes tagged images to Docker Hub (`flex420/urlaubsverwaltung`) on `main` and version tags.
142+
5. Publishes a lightweight SBOM and attaches it as workflow artifact.
143+
GitHub secrets required (already provisioned):
144+
145+
- `DOCKERHUB_USERNAME`
146+
- `DOCKERHUB_TOKEN`
147+
148+
## Updating upstream
149+
150+
To update the upstream application:
151+
152+
```
153+
git submodule update --remote app/urlaubsverwaltung
154+
# optionally pin to a release tag, then commit
155+
```
156+
157+
Re-run the CI pipeline to publish a refreshed image.

app/urlaubsverwaltung

Submodule urlaubsverwaltung added at fe92f4c

0 commit comments

Comments
 (0)