A professional portfolio website built with Java 17, Spring Boot 3, Thymeleaf, Maven, and a full Jenkins CI/CD pipeline. Features a contact form backed by a REST API and H2 database, optional Docker support, and a responsive editorial-dark UI.
- Tech Stack
- Project Structure
- Prerequisites
- Running Locally (Maven)
- Running with Docker
- Jenkins CI/CD Setup
- API Reference
- Customisation
- Deployment Notes
| Layer | Technology |
|---|---|
| Backend | Java 17, Spring Boot 3.2, Spring MVC |
| Templating | Thymeleaf 3 |
| Persistence | Spring Data JPA, H2 (in-memory) |
| Build | Apache Maven 3.9 |
| CI/CD | Jenkins (Declarative Pipeline) |
| Container | Docker (multi-stage), Docker Compose |
| Frontend | Vanilla HTML/CSS/JS (no framework) |
| Monitoring | Spring Actuator (/actuator/health) |
portfolio/
├── src/
│ ├── main/
│ │ ├── java/com/affan/portfolio/
│ │ │ ├── PortfolioApplication.java # Entry point
│ │ │ ├── config/
│ │ │ │ └── WebConfig.java # CORS, resource handlers
│ │ │ ├── controller/
│ │ │ │ ├── HomeController.java # Renders index.html
│ │ │ │ └── ContactController.java # REST API /api/contact
│ │ │ ├── model/
│ │ │ │ ├── ContactMessage.java # JPA entity
│ │ │ │ └── ContactMessageRepository.java
│ │ │ └── service/
│ │ │ └── ContactService.java # Business logic
│ │ └── resources/
│ │ ├── templates/
│ │ │ └── index.html # Thymeleaf template
│ │ ├── static/
│ │ │ ├── css/style.css
│ │ │ ├── js/main.js
│ │ │ └── images/ # Add profile.jpg here
│ │ └── application.properties
│ └── test/
│ ├── java/com/affan/portfolio/
│ │ ├── PortfolioApplicationTests.java
│ │ └── controller/
│ │ └── ContactControllerTest.java
│ └── resources/
│ └── application-test.properties
├── Dockerfile
├── docker-compose.yml
├── Jenkinsfile
├── pom.xml
└── README.md
| Tool | Version | Notes |
|---|---|---|
| Java (JDK) | 17+ | java -version |
| Maven | 3.9+ | Or use included mvnw wrapper |
| Git | Any | For cloning and version control |
| Jenkins | LTS | For CI/CD pipeline |
| Docker | 24+ | Optional — for containerised runs |
| Docker Compose | 2+ | Optional — bundled with Docker Desktop |
git clone https://github.com/<your-username>/portfolio.git
cd portfolio# Using Maven wrapper (no Maven install needed)
./mvnw spring-boot:run
# Or with system Maven
mvn spring-boot:runhttp://localhost:8080
./mvnw test./mvnw clean package -DskipTests
java -jar target/portfolio.jardocker compose up --build# Build
docker build -t affan-portfolio .
# Run
docker run -p 8080:8080 affan-portfoliocurl http://localhost:8080/actuator/healthThe included Jenkinsfile defines a 5-stage declarative pipeline:
Checkout → Build → Test → Package → Deploy
# macOS (Homebrew)
brew install jenkins-lts
brew services start jenkins-lts
# Linux (Ubuntu/Debian)
wget -q -O - https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo apt-key add -
sudo sh -c 'echo deb https://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'
sudo apt update && sudo apt install jenkins
sudo systemctl start jenkinsOpen Jenkins at http://localhost:8080 (note: if the portfolio app is already on 8080, run Jenkins on a different port by editing /etc/default/jenkins or the Homebrew plist — change HTTP_PORT to 8090).
Go to Manage Jenkins → Global Tool Configuration and add:
| Tool | Name | Version |
|---|---|---|
| JDK | JDK-17 |
Java 17 |
| Maven | Maven-3.9 |
Maven 3.9.x |
The names must match exactly what's in the Jenkinsfile.
- Click New Item → name it
portfolio→ select Pipeline → OK. - Under Pipeline, set Definition to
Pipeline script from SCM. - Set SCM to
Gitand enter your repository URL. - Set Script Path to
Jenkinsfile. - Save.
- In Jenkins job → Build Triggers → tick GitHub hook trigger for GITScm polling.
- In GitHub repo → Settings → Webhooks → Add webhook:
- Payload URL:
http://<your-jenkins-host>:8090/github-webhook/ - Content type:
application/json - Events:
Just the push event
- Payload URL:
Every push to GitHub will now automatically trigger the pipeline.
sudo mkdir -p /opt/portfolio
sudo chown $(whoami):$(whoami) /opt/portfolio| Stage | What it does |
|---|---|
| Checkout | Pulls the latest code from GitHub |
| Build | mvn clean compile — compiles all sources |
| Test | mvn test — runs unit and integration tests |
| Package | mvn package — produces target/portfolio.jar |
| Deploy | Stops old instance, copies JAR, starts new one, waits for health check |
Submit a contact form message.
Request body (JSON):
{
"name": "Jane Doe",
"email": "jane@example.com",
"subject": "Internship Opportunity",
"message": "Hi Affan, I came across your portfolio and would love to connect!"
}Success response (200):
{
"success": true,
"message": "Thank you, Jane Doe! Your message has been received.",
"id": 1
}Validation error (400):
{
"success": false,
"errors": {
"email": "Please provide a valid email address"
}
}Returns message count statistics.
{
"totalMessages": 5,
"unreadMessages": 3
}Returns all stored contact messages (protect in production).
Marks a specific message as read.
Spring Actuator health endpoint — used by Jenkins deploy health check.
{
"status": "UP"
}http://localhost:8080/h2-console
JDBC URL: jdbc:h2:mem:portfoliodb
Username: sa
Password: (empty)
- Place
profile.jpginsrc/main/resources/static/images/. - In
index.html, replace the placeholder block:
<!-- REMOVE this block: -->
<div class="hero__photo-initials">A</div>
<!-- ADD this instead: -->
<img th:src="@{/images/profile.jpg}" alt="Affan" class="hero__photo-img"/>Edit HomeController.java to change:
- Skills and proficiency levels
- Project titles, descriptions, links, and tech stacks
- Any model attributes passed to the template
Edit index.html to update:
- Contact email and social links in the Contact section
- Footer text
Replace H2 with MySQL or PostgreSQL in pom.xml and application.properties:
spring.datasource.url=jdbc:mysql://localhost:3306/portfoliodb
spring.datasource.username=root
spring.datasource.password=yourpassword
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=update- The app binds to
http://localhost:8080by default. - The
Deploystage in the Jenkins pipeline:- Reads the PID from
/opt/portfolio/portfolio.pid - Gracefully stops the old process
- Copies the new JAR to
/opt/portfolio/ - Starts the app and waits up to 30 seconds for the health check to pass
- Reads the PID from
- Logs are written to
/opt/portfolio/portfolio.log - For internet-facing deployment, put Nginx as a reverse proxy in front of port 8080.
Built by Affan — CS & Business Systems Engineer