Tutorial 12: Docker and CI with Jenkins
Containerise the backend and frontend, compose the full stack, and automate builds and Docker Hub pushes with Jenkins.
← Back to TutorialsThe final step is packaging the application for deployment. This tutorial covers the Dockerfile that containerises the Spring Boot backend, the docker-compose.yml that runs the entire stack (database, backend, frontend) with a single command, and the Jenkins pipeline that automates building and pushing images to Docker Hub on every commit.
1. Backend Dockerfile
FROM openjdk:17-jdk-slim
VOLUME /tmp
COPY target/RestaurantOrder-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
EXPOSE 8080
- openjdk:17-jdk-slim: the
slimvariant strips out tools not needed at runtime, reducing the image size significantly compared to the full JDK image. - VOLUME /tmp: Spring Boot uses
/tmpfor embedded Tomcat's working directory. Declaring it as a volume moves it to Docker's storage layer, improving performance on some hosts. - COPY target/...jar app.jar: assumes the JAR has already been built by
mvn packagebeforedocker buildruns. The Jenkins pipeline does this in the stage before building the image. - ENTRYPOINT vs CMD:
ENTRYPOINTis used here so the container always runs the JAR.CMDcan be overridden at runtime;ENTRYPOINTcannot (without--entrypoint). - EXPOSE 8080: documents the port the app listens on: it doesn't actually publish it. Port publishing happens in
docker-compose.ymlor thedocker runcommand.
2. docker-compose.yml: the full stack
services:
db:
image: postgres:14
container_name: restaurant_postgres
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- "5432:5432"
volumes:
- db_data:/var/lib/postgresql/data # persists data across container restarts
backend:
image: wassim4592/restaurant_backend:latest
container_name: restaurant_backend
depends_on:
- db
environment:
SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL}
SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME}
SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD}
JWT_SECRET: ${JWT_SECRET}
ports:
- "8080:8080"
frontend:
image: wassim4592/restaurant_frontend:latest
container_name: restaurant_frontend
depends_on:
- backend
ports:
- "3000:80" # React build served by nginx inside the container on port 80
volumes:
db_data:
- All secrets via environment variables: no credentials are in the compose file itself. A
.envfile (excluded from git) provides the values at runtime. - depends_on: controls start order:
backendstarts afterdb,frontendstarts afterbackend. Note: this only waits for the container to start, not for the service inside to be healthy. A health check would be needed for a stricter guarantee. - Named volume
db_data: data persists even if thedbcontainer is removed and recreated. Without this, every restart wipes the database. - 3000:80 for frontend: the React app is built as static files and served by nginx inside the container on port 80. The host maps 3000 to 80 so users hit
localhost:3000. - The three services form a chain: requests enter through the frontend, which calls the backend API on port 8080, which reads/writes to PostgreSQL on port 5432.
3. Jenkinsfile: the CI pipeline
pipeline {
agent any
environment {
DOCKERHUB_CREDENTIALS = credentials('dockerhub-credentials') // Jenkins credential ID
BACKEND_IMAGE = "wassim4592/restaurant_backend:latest"
FRONTEND_IMAGE = "wassim4592/restaurant_frontend:latest"
}
stages {
stage('Checkout') {
steps {
checkout scm // pull the latest code from the configured SCM (GitHub)
}
}
stage('Build Backend JAR') {
steps {
sh './mvnw clean package -DskipTests' // compile + package, skip tests for speed
}
}
stage('Build and Push Backend Docker Image') {
steps {
script {
docker.withRegistry('https://index.docker.io/v1/', DOCKERHUB_CREDENTIALS) {
def backendImage = docker.build(env.BACKEND_IMAGE, '.')
backendImage.push('latest')
}
}
}
}
stage('Build and Push Frontend Docker Image') {
steps {
script {
docker.withRegistry('https://index.docker.io/v1/', DOCKERHUB_CREDENTIALS) {
def frontendImage = docker.build(env.FRONTEND_IMAGE, 'frontend')
frontendImage.push('latest')
}
}
}
}
}
post {
always {
cleanWs() // delete workspace after every build to avoid stale artifacts
}
}
}
- credentials('dockerhub-credentials'): Jenkins retrieves the Docker Hub username/password from its credential store: never hardcoded in the pipeline file.
- -DskipTests: tests are skipped in this pipeline to keep the CI cycle fast. A dedicated test stage (or a separate pipeline) should run the full test suite.
- docker.build(image, context): the second argument is the Docker build context.
'.'for the backend (Dockerfile at root),'frontend'for the React app (Dockerfile inside the frontend directory). - push('latest'): tags the image as
:lateston Docker Hub. In production pipelines you'd also push a version tag (e.g., the Git commit SHA) to keep a history of deployable images. - post { always { cleanWs() } }: workspace cleanup runs even if a stage fails, preventing disk space from accumulating on the Jenkins agent.
4. Running the full stack locally
# 1. Build the backend JAR
./mvnw clean package -DskipTests
# 2. Create a .env file with your secrets
cp .env.example .env
# edit .env with your DB password, JWT secret, etc.
# 3. Start everything
docker compose up -d
# 4. Check it's running
docker compose ps
# 5. Stop and tear down
docker compose down # keeps the db_data volume
docker compose down -v # also deletes the volume (wipes the database)
-druns containers in detached mode (background). Usedocker compose logs -fto tail logs.docker compose down -vis destructive: it deletes all named volumes includingdb_data. Useful to reset a dev environment; never run it in production.
Key Takeaways
- Use
openjdk:17-jdk-slim(not the full JDK image) to keep the backend image as lean as possible. - All secrets (DB password, JWT secret, Docker Hub credentials) live in environment variables: never in source-controlled files.
- Named volumes make database data persistent across container restarts; without them, every
compose downwipes your data. depends_oncontrols start order but not readiness: add health checks if you need to wait for PostgreSQL to be fully ready before the backend starts.- Push a version tag alongside
:latestin CI so you can roll back to a specific image if a deployment goes wrong. cleanWs()in apost { always }block prevents Jenkins agents from running out of disk space after many builds.