Skip to main content
The User Management API includes full Docker support for containerized deployment. This guide covers building Docker images, running containers, and using Docker Compose for local development.

Dockerfile Overview

The application uses a production-ready multi-stage Dockerfile:
Dockerfile
FROM eclipse-temurin:21-jre-jammy

RUN addgroup --system spring && adduser --system spring --ingroup spring

WORKDIR /deployments

COPY build/libs/*.jar app.jar

RUN chown -R spring:spring /deployments

USER spring:spring

EXPOSE 8080

ENTRYPOINT ["java","-jar","/deployments/app.jar"]
The image uses Eclipse Temurin JRE 21 (a lightweight Java runtime) and follows security best practices by running as a non-root user.

Building the Docker Image

1

Build the Application JAR

First, build the Spring Boot application using Gradle:
./gradlew build
This creates the JAR file in build/libs/user-management-api-0.0.1-SNAPSHOT.jar.
Use ./gradlew bootJar -x test to skip tests if you’ve already run them.
2

Build the Docker Image

Build the Docker image with a tag:
docker build -t user-management-api .
Or with a specific version:
docker build -t user-management-api:1.0.0 .
3

Verify the Image

List Docker images to confirm the build:
docker images | grep user-management-api
You should see:
user-management-api    latest    abc123def456    2 minutes ago    300MB

Running with Docker

Basic Run Command

Run the container with environment variables:
docker run -p 8080:8080 \
  -e SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/user_db \
  -e SPRING_DATASOURCE_USERNAME=postgres \
  -e SPRING_DATASOURCE_PASSWORD=password \
  -e ENVIRONMENT=local \
  user-management-api
When running in Docker, localhost refers to the container itself. Use host.docker.internal (Mac/Windows) or the host IP address (Linux) to connect to PostgreSQL on your host machine.

Corrected Connection for Host Database

docker run -p 8080:8080 \
  -e SPRING_DATASOURCE_URL=jdbc:postgresql://host.docker.internal:5432/user_db \
  -e SPRING_DATASOURCE_USERNAME=postgres \
  -e SPRING_DATASOURCE_PASSWORD=password \
  -e ENVIRONMENT=local \
  user-management-api

Run in Detached Mode

Run the container in the background:
docker run -d -p 8080:8080 \
  --name user-api \
  -e SPRING_DATASOURCE_URL=jdbc:postgresql://host.docker.internal:5432/user_db \
  -e SPRING_DATASOURCE_USERNAME=postgres \
  -e SPRING_DATASOURCE_PASSWORD=password \
  -e ENVIRONMENT=local \
  user-management-api
Check logs:
docker logs -f user-api
For local development, Docker Compose provides a complete environment with both the API and PostgreSQL database.

Docker Compose Configuration

Create a docker-compose.yml file in the project root:
docker-compose.yml
version: '3.8'

services:
  db:
    image: postgres:16
    container_name: user-db
    environment:
      POSTGRES_DB: user_db
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - user-api-network

  api:
    build: .
    container_name: user-api
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/user_db
      SPRING_DATASOURCE_USERNAME: postgres
      SPRING_DATASOURCE_PASSWORD: password
      ENVIRONMENT: local
    ports:
      - "8080:8080"
    depends_on:
      db:
        condition: service_healthy
    networks:
      - user-api-network

volumes:
  postgres_data:

networks:
  user-api-network:
    driver: bridge
The API service waits for the database to be healthy before starting, ensuring proper initialization order.

Starting with Docker Compose

1

Build the Application

Build the Spring Boot JAR:
./gradlew build
2

Start All Services

Start both the database and API:
docker compose up --build
The --build flag rebuilds the image if the code has changed.
3

Verify Services are Running

Check service status:
docker compose ps
You should see both user-db and user-api running.
4

Access the Application

Managing Docker Compose

docker compose up -d

Environment Variables Reference

All configuration can be overridden with environment variables:
VariableDefaultDescription
ENVIRONMENTlocalActive Spring profile
SPRING_DATASOURCE_URLjdbc:postgresql://localhost:5432/user_dbDatabase JDBC URL
SPRING_DATASOURCE_USERNAMEpostgresDatabase username
SPRING_DATASOURCE_PASSWORDpasswordDatabase password

Using Environment Files

Create a .env file for Docker Compose:
.env
ENVIRONMENT=local
SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/user_db
SPRING_DATASOURCE_USERNAME=postgres
SPRING_DATASOURCE_PASSWORD=password
Update docker-compose.yml:
docker-compose.yml
services:
  api:
    build: .
    env_file:
      - .env
    ports:
      - "8080:8080"
Never commit .env files with real credentials to version control. Add .env to your .gitignore.

Production Deployment

Cloud Run Deployment

The project includes Google Cloud Build configuration for automated deployment:
cloudbuild.yaml
steps:
  # Run tests
  - name: 'gradle:8.5-jdk21'
    entrypoint: 'gradle'
    args: ['test']

  # Build JAR
  - name: 'gradle:8.5-jdk21'
    entrypoint: 'gradle'
    args: ['bootJar', '-x', 'test']

  # Build and push Docker image
  - name: "gcr.io/cloud-builders/docker"
    script: |
      IMAGE_NAME="${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_REPO}/${_SERVICE}"
      docker build -t "${IMAGE_NAME}:${SHORT_SHA}" -t "${IMAGE_NAME}:latest" .
      docker push "${IMAGE_NAME}:${SHORT_SHA}"
      docker push "${IMAGE_NAME}:latest"

  # Deploy to Cloud Run
  - name: "gcr.io/google.com/cloudsdktool/cloud-sdk"
    entrypoint: gcloud
    args:
      - "run"
      - "deploy"
      - "${_SERVICE}"
      - "--image"
      - "${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPO}/${_SERVICE}:$SHORT_SHA"
      - "--set-secrets"
      - "SPRING_DATASOURCE_URL=CONTABO_DB_URL:latest,SPRING_DATASOURCE_USERNAME=CONTABO_DB_USER:latest,SPRING_DATASOURCE_PASSWORD=CONTABO_DB_PASSWORD:latest"
The CI/CD pipeline automatically runs tests, builds the Docker image, and deploys to Cloud Run with secrets from Secret Manager.

Pushing to Container Registry

# Tag the image
docker tag user-management-api:latest username/user-management-api:latest

# Push to Docker Hub
docker push username/user-management-api:latest

Troubleshooting

Problem: API container cannot reach PostgreSQL.Solution:
  • Use host.docker.internal instead of localhost
  • On Linux, add --add-host=host.docker.internal:host-gateway
  • Or use Docker Compose to run both services in the same network
Problem: Port 8080 is already bound.Solution: Change the host port mapping:
docker run -p 8081:8080 user-management-api
Problem: Docker build cannot find the JAR file.Solution: Ensure you’ve built the application first:
./gradlew build
ls -la build/libs/
docker build -t user-management-api .
Problem: Container starts but immediately exits.Solution: Check the logs:
docker logs user-api
Common issues:
  • Database connection failure
  • Missing environment variables
  • Port conflicts
Problem: Container runs out of memory.Solution: Increase container memory or add JVM options:
docker run -p 8080:8080 \
  -e JAVA_OPTS="-Xmx512m -Xms256m" \
  user-management-api
Update the Dockerfile:
ENTRYPOINT ["java","-Xmx512m","-Xms256m","-jar","/deployments/app.jar"]

Best Practices

1

Use Multi-Stage Builds

For production, consider a multi-stage build that compiles the application inside Docker:
FROM gradle:8.5-jdk21 AS builder
WORKDIR /app
COPY . .
RUN gradle bootJar -x test

FROM eclipse-temurin:21-jre-jammy
WORKDIR /deployments
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/deployments/app.jar"]
2

Optimize Image Size

  • Use JRE instead of JDK (already done)
  • Use Alpine-based images for smaller size
  • Clean up unnecessary files
  • Use .dockerignore to exclude build artifacts
3

Security Hardening

  • Run as non-root user (already implemented)
  • Use specific image versions, not latest
  • Scan images for vulnerabilities: docker scan user-management-api
  • Use secrets management for production
4

Health Checks

Add a Docker health check:
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
  CMD curl -f http://localhost:8080/actuator/health || exit 1

Next Steps

Configuration

Learn about configuration options

Testing

Understand the testing strategy

API Reference

Explore the API endpoints

Architecture

Learn about hexagonal architecture

Build docs developers (and LLMs) love