Deploying a Spring Boot Application with PostgreSQL Using Docker, GitHub Actions, and AWS EC2

In this blog post, I’ll walk you through a complete guide on how to deploy a Spring Boot application with PostgreSQL as the database using Docker, GitHub Actions for CI/CD, and an AWS EC2 instance for hosting. This approach automates the entire process, from building the application to deploying it on a remote server.

Prerequisites

  • Basic knowledge of Docker, Spring Boot, and GitHub Actions.
  • An AWS account with access to an EC2 instance.
  • A Docker Hub account.
  • GitHub repository for your project.

Step 1: Setting Up the Spring Boot Application

We have a Spring Boot application configured to use PostgreSQL as its database. In the application.properties file, we use environment variables to keep sensitive information like database credentials secure.

application.properties

# Use environment variables for database connection configuration
spring.datasource.url=${SPRING_DATASOURCE_URL}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
# Hibernate / JPA Configuration
spring.jpa.hibernate.ddl-auto=${SPRING_JPA_HIBERNATE_DDL_AUTO:update}  # Default to 'update' if not provided
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
# Spring Profile
spring.profiles.active=${SPRING_PROFILES_ACTIVE:default}  # Default to 'default' profile if not provided
server.port=${SERVER_PORT:8080}

In the configuration file, we rely on environment variables for the database URL, username, password, and other properties. This allows for flexibility when deploying to different environments, as we will inject these variables at runtime.

Step 2: Dockerizing the Application

The Dockerfile for this application is straightforward. It uses a lightweight Java 19 JDK base image and copies the pre-built JAR file into the Docker image.

Dockerfile

# Use a base image with Java
FROM eclipse-temurin:19-jdk-alpine
# Set the working directory inside the container
WORKDIR /app
# Copy the jar file from the build directory to the container
COPY build/libs/*.jar app.jar
# Expose the application port
EXPOSE 8080
# Run the jar file
ENTRYPOINT ["java", "-jar", "app.jar"]

Explanation:

  • Uses the eclipse-temurin:19-jdk-alpine image for a minimal Java runtime environment.
  • Copies the pre-built JAR file (created by Gradle) into the container.
  • Exposes port 8080 for the application.
  • Runs the JAR file when the container starts.

Step 3: Using Docker Compose for Multi-Container Setup

The docker-compose.yml file defines the multi-container setup with two services: the Spring Boot app and PostgreSQL.

docker-compose.yml

```yaml
version: '3.8'
services:
  db:
    image: postgres:latest
    container_name: postgres-db
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    networks:
      - app-network
  app:
    image: tanmay031/user-management-app:latest
    container_name: spring-boot-app
    depends_on:
      - db
    environment:
      SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL}
      SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME}
      SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD}
      SPRING_JPA_HIBERNATE_DDL_AUTO: ${SPRING_JPA_HIBERNATE_DDL_AUTO}
      SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE}
    ports:
      - "8080:8080"
    networks:
      - app-network
volumes:
  postgres_data:
networks:
  app-network:

Explanation:

  • Defines two services: db (PostgreSQL) and app (Spring Boot application).
  • Uses environment variables for sensitive data like database credentials.
  • Creates a Docker network (app-network) for communication between the db and app services.

In this step, we use GitHub Actions to automate the entire process of building the Spring Boot application, creating a Docker image, pushing it to Docker Hub, and deploying the application to an AWS EC2 instance. This is done through a CI/CD pipeline defined in the build-and-deploy.yml file.

name: Build and Deploy Spring Boot App
on:
  push:
    branches:
      - main
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      # Checkout the code
      - name: Checkout code
        uses: actions/checkout@v3
      # Set up JDK 19
      - name: Set up JDK 19
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '19'
      # Grant execute permission for Gradle Wrapper
      - name: Make Gradle Wrapper executable
        run: chmod +x ./gradlew
      # Cache Gradle packages
      - name: Cache Gradle packages
        uses: actions/cache@v3
        with:
          path: ~/.gradle/caches
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-
      # Build the application with Gradle
      - name: Build with Gradle (skip tests)
        run: ./gradlew build -x test
      # Set up Docker Buildx and Docker Compose in a single step
      - name: Set up Docker
        uses: docker/setup-buildx-action@v2
      # Log in to Docker Hub
      - name: Log in to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      # Build and push Docker image
      - name: Build and push Docker image
        run: |
          docker buildx build --platform linux/amd64 -t tanmay031/user-management-app:latest --push .
  deploy:
    runs-on: ubuntu-latest
    needs: build
    steps:
      # Checkout code
      - name: Checkout code
        uses: actions/checkout@v3
      # Transfer docker-compose.yml to server
      - name: Transfer docker-compose.yml to server
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          source: ./docker-compose.yml
          target: /home/ec2-user/my-app/
      # Deploy to Server via SSH
      - name: Deploy to Server via SSH
        uses: appleboy/ssh-action@v0.1.6
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            # Navigate to the app directory
            cd /home/ec2-user/my-app
            # Export environment variables for docker-compose
            export POSTGRES_DB=${{ secrets.POSTGRES_DB }}
            export POSTGRES_USER=${{ secrets.POSTGRES_USER }}
            export POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}
            export SPRING_DATASOURCE_URL=${{ secrets.SPRING_DATASOURCE_URL }}
            export SPRING_DATASOURCE_USERNAME=${{ secrets.SPRING_DATASOURCE_USERNAME }}
            export SPRING_DATASOURCE_PASSWORD=${{ secrets.SPRING_DATASOURCE_PASSWORD }}
            export SPRING_JPA_HIBERNATE_DDL_AUTO=update
            export SPRING_PROFILES_ACTIVE=prod
            # Bring down any existing Docker containers
            docker-compose down
            # Bring up Docker containers using exported environment variables
            docker-compose up -d

Here’s a breakdown of each part of this step:

Triggering the Workflow

name: Build and Deploy Spring Boot App
on:
  push:
    branches:
      - main

Trigger: This workflow is triggered whenever changes are pushed to the main branch of the repository. You can change the branch to suit your project’s workflow (e.g., use a dev or staging branch).

Workflow Name: "Build and Deploy Spring Boot App" is a human-readable name for this workflow, making it easy to identify in the GitHub Actions dashboard.

Job 1: Building the Application and Docker Image

This job is named build and runs on an Ubuntu-based virtual machine provided by GitHub.

Setting Up the Build Job

jobs:
  build:
    runs-on: ubuntu-latest

runs-on: ubuntu-latest: Specifies the environment in which the job will run. ubuntu-latest is a pre-configured virtual machine with all necessary tools for building and deploying the application.

Step 1: Checkout Code

- name: Checkout code
  uses: actions/checkout@v3

Uses the actions/checkout action to pull the latest code from the repository. This is essential as it provides the workflow with the codebase to build.

Step 2: Set Up Java Development Kit (JDK)

- name: Set up JDK 19
  uses: actions/setup-java@v3
  with:
    distribution: 'temurin'
    java-version: '19'

This step uses actions/setup-java to install JDK 19. The temurin distribution is an open-source, production-ready JDK provided by the Eclipse Adoptium project.

Step 3: Grant Execute Permission for the Gradle Wrapper

- name: Make Gradle Wrapper executable
  run: chmod +x ./gradlew

Why: The Gradle wrapper script (gradlew) needs execution permissions to build the application. This command (chmod +x ./gradlew) sets those permissions.

Step 4: Cache Gradle Packages

- name: Cache Gradle packages
  uses: actions/cache@v3
  with:
    path: ~/.gradle/caches
    key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
    restore-keys: |
      ${{ runner.os }}-gradle-

Purpose: To cache downloaded Gradle dependencies, speeding up subsequent builds.

Explanation:

  • path: Specifies the directory (~/.gradle/caches) where Gradle caches its downloaded dependencies.
  • key: Generates a unique key for the cache based on the OS and hash of Gradle files (e.g., build.gradle). If these files change, the cache will automatically refresh.
  • restore-keys: Allows using a partial match for the cache key if an exact match is not found.

Step 5: Build the Application with Gradle

- name: Build with Gradle (skip tests)
  run: ./gradlew build -x test

This command compiles the Java application and packages it into a JAR file, skipping tests to save time (-x test flag).

Output: The resulting JAR file will be placed in the build/libs directory, ready for Docker to use in the next steps.

Step 6: Set Up Docker Buildx

- name: Set up Docker
  uses: docker/setup-buildx-action@v2

Purpose: This action sets up Docker Buildx, a tool that allows building Docker images with more flexibility, including cross-platform builds (e.g., building for linux/amd64 architecture).

Step 7: Log in to Docker Hub

- name: Log in to Docker Hub
  uses: docker/login-action@v2
  with:
    username: ${{ secrets.DOCKER_USERNAME }}
    password: ${{ secrets.DOCKER_PASSWORD }}

Uses the docker/login-action to log in to Docker Hub using credentials stored in GitHub Secrets.

Security: The credentials (DOCKER_USERNAME and DOCKER_PASSWORD) are stored securely in the GitHub repository settings as secrets to avoid exposing sensitive information in the workflow file.

Step 8: Build and Push Docker Image

- name: Build and push Docker image
  run: |
    docker buildx build --platform linux/amd64 -t tanmay031/user-management-app:latest --push .

Explanation:

  • docker buildx build: Uses Docker Buildx to create a Docker image.
  • --platform linux/amd64: Specifies the target platform for the Docker image, ensuring compatibility.
  • -t tanmay031/user-management-app:latest: Tags the Docker image with the name tanmay031/user-management-app and latest version.
  • --push: After building the image, it automatically pushes it to Docker Hub.

Job 2: Deploying to the AWS EC2 Instance

The second job (deploy) depends on the successful completion of the build job (needs: build).

Step 1: Checkout Code

- name: Checkout code
  uses: actions/checkout@v3

Similar to the build job, this step checks out the repository to access the docker-compose.yml file needed for deployment.

Step 2: Transfer docker-compose.yml to the Server

- name: Transfer docker-compose.yml to server
  uses: appleboy/scp-action@master
  with:
    host: ${{ secrets.SERVER_HOST }}
    username: ${{ secrets.SERVER_USER }}
    key: ${{ secrets.SERVER_SSH_KEY }}
    source: ./docker-compose.yml
    target: /home/ec2-user/my-app/

Uses the appleboy/scp-action to securely transfer the docker-compose.yml file to the EC2 instance.

Credentials: Uses SSH keys stored in GitHub Secrets to securely connect to the remote server (SERVER_HOST, SERVER_USER, and SERVER_SSH_KEY).

Step 3: Deploy to Server via SSH

```yaml
- name: Deploy to Server via SSH
  uses: appleboy/ssh-action@v0.1.6
  with:
    host: ${{ secrets.SERVER_HOST }}
    username: ${{ secrets.SERVER_USER }}
    key: ${{ secrets.SERVER_SSH_KEY }}
    script: |
      # Navigate to the app directory
      cd /home/ec2-user/my-app
      # Export environment variables for docker-compose
      export POSTGRES_DB=${{ secrets.POSTGRES_DB }}
      export POSTGRES_USER=${{ secrets.POSTGRES_USER }}
      export POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}
      export SPRING_DATASOURCE_URL=${{ secrets.SPRING_DATASOURCE_URL }}
      export SPRING_DATASOURCE_USERNAME=${{ secrets.SPRING_DATASOURCE_USERNAME }}
      export SPRING_DATASOURCE_PASSWORD=${{ secrets.SPRING_DATASOURCE_PASSWORD }}
      export SPRING_JPA_HIBERNATE_DDL_AUTO=update
      export SPRING_PROFILES_ACTIVE=prod
      # Bring down any existing Docker containers
      docker-compose down
      # Bring up Docker containers using exported environment variables
      docker-compose up -d

Explanation:

  • SSH Connection: Uses the appleboy/ssh-action to connect to the EC2 instance using SSH keys stored in GitHub Secrets.
  • Environment Variables: Exports necessary environment variables (database credentials, Spring Boot configuration) for use in the docker-compose.yml file.
  • Deploy the Application:
    • docker-compose down: Stops and removes any running containers to prepare for the new deployment.
    • docker-compose up -d: Starts the containers in detached mode, using the environment variables and the latest Docker image pushed to Docker Hub.

Summary of Step 4

  • The build job compiles the Spring Boot application, builds a Docker image, and pushes it to Docker Hub.
  • The deploy job connects to the AWS EC2 instance, transfers the docker-compose.yml file, sets environment variables, and runs docker-compose to deploy the latest version of the application.
  • This automated pipeline ensures that every change pushed to the main branch is built, containerized, and deployed seamlessly to the remote server.

Conclusion

With this setup, you can quickly and reliably build, package, and deploy a Spring Boot application with a PostgreSQL database using Docker. GitHub Actions provides an automated CI/CD pipeline, while Docker and AWS EC2 allow for consistent and scalable deployments. By leveraging environment variables, you can keep sensitive information secure and easily adapt to different environments.

Leave a Reply

Your email address will not be published. Required fields are marked *.

*
*
You may use these <abbr title="HyperText Markup Language">HTML</abbr> tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>