Demystifying Docker for Spring Boot: Automation with GitHub Actions
In this blog article, I am going to discuss about Docker fundamentals, its component architecture, dockerizing a Spring Boot application, and automating docker processes with GitHub Actions.
1. Understanding Docker
1.1. What is docker?
Docker is an open-source platform that enables developers to automate the deployment, scaling, and management of applications in lightweight, portable containers. Containers package an application and its dependencies together, ensuring that it runs consistently across different computing environments.
Key Points in the Definition:
- Open-source platform: Docker is freely available for anyone to use and contribute to.
- Automate the deployment, scaling, and management: Docker simplifies and automates the process of getting an application up and running.
- Lightweight and portable containers: Containers are small, self-contained units that can run anywhere.
- Application and dependencies: Everything needed to run the application (code, libraries, configurations) is included in the container.
- Consistency across environments: Containers ensure that the application behaves the same way regardless of where it’s run, eliminating the “it works on my machine” problem.
Let’s try a simpler analogy to explain Docker:
Think of Docker as a shipping container for software.
A shipping container can carry different types of goods (furniture, electronics, clothes) across different modes of transport (ships, trains, trucks) without any changes to the container itself.
A Docker container carries all the parts needed to run an application (code, libraries, settings) across different computing environments (developer’s laptop, testing server, production cloud) without any changes to the container itself.
1.2. Key Components of Docker
- Image: An image is a read-only template used to create containers. Think of it as a snapshot of your application at a particular point in time.
- Container: A container is like a lightweight, portable, and self-sufficient unit that contains everything needed to run an application. A Docker container is a runnable instance of an image.
- Dockerfile: A Dockerfile is a script containing a series of commands used to assemble an image. It tells Docker how to build the image step by step.
- Docker Hub: A public registry where Docker images are stored and shared. Developers can push their images to Docker Hub or pull and run images created by others.
- Docker Engine: The core of Docker, it is the runtime that builds and runs containers using images. It includes the Docker Daemon and Docker Client.
- Docker Daemon: A background service running on the host machine. It manages Docker objects such as images, containers, networks, and volumes. The Docker Daemon listens for Docker API requests and processes them.
- Docker Client: A command-line tool that allows users to interact with the Docker Daemon. When you type a Docker command, the client sends these commands to the Docker Daemon via the Docker API.
- Docker Network: Docker networks enable containers to communicate with each other, either on the same host or across multiple hosts. Networks can be used to isolate groups of containers or to enable communication between them.
- Docker Volumes: Docker volumes provide a way to persist data generated by and used by Docker containers. Volumes are stored on the host filesystem and can be shared between containers.
- Docker Compose: Docker Compose is a tool used for defining and running multi-container Docker applications. With Docker Compose, you use a YAML file to configure your application’s containers, networks, and volumes.
- Docker Desktop (Windows/Mac): A user-friendly application that includes Docker Engine, Docker Client, Docker Compose, and other features.
1.3. Benefits of Using Docker
- Consistency Across Environments: This ensures that the application behaves the same way in development, testing, and production environments.
- Simplified Dependency Management: With Docker, dependencies required for an application to run are packaged within the container. This eliminates conflicts between dependencies on the host system and allows different versions of dependencies to coexist peacefully.
- Scalability and Isolation: Docker containers provide lightweight, isolated environments that can run independently on the same host machine. Docker containers can be quickly started, stopped, and replicated, making it easy to scale applications horizontally based on demand.
- Microservices Architecture: Docker supports microservices-based applications, where each service can be packaged and deployed independently in its own container.
- Version Control and Rollback: Docker images are versioned, allowing developers to roll back to previous versions or track changes easily.
- DevOps and Continuous Integration/Continuous Deployment (CI/CD): Docker facilitates automation in CI/CD pipelines by providing consistent environments for testing, deployment, and production.
- Portability: Docker containers can run on any system that supports Docker, whether it’s a developer’s laptop, on-premises servers, or cloud platforms like AWS, Azure, or Google Cloud.
1.4. Docker Architecture: OS-Level Functionality and VM Comparison
Linux
- Docker was initially designed to run on Linux. It takes advantage of Linux kernel features such as namespaces and cgroups.
- Docker Daemon runs natively, using host kernel features.
Windows/macOS
- Docker Desktop is used on Windows and macOS, which includes a lightweight VM to run Docker Daemon.
- Docker Desktop uses Hyper-V or Windows Subsystem for Linux 2 (WSL 2) to create a Linux environment in which the Docker Daemon runs.
- Docker Desktop uses HyperKit to create a Linux VM on macOS.
- Docker Client on Windows/macOS communicates with the Docker Daemon in the VM.
Docker and Virtual Machines
- VMs run on a hypervisor (e.g., VMware, Hyper-V, VirtualBox), which virtualizes hardware for multiple VMs. Containers run on the Docker Engine, which leverages the host OS kernel for operations.
- Each VM includes a full OS instance, including its own kernel. Containers share the host OS kernel, isolating applications but not requiring a separate OS for each container.
- VMs are resource-intensive as each VM requires its own OS and virtualized hardware resources. Containers are more resource-efficient and light weight because they share the host OS kernel and only contain application-specific dependencies.
- VMs take longer to boot since they need to initialize a full OS. Containers start almost instantly since they do not need to boot a full OS, only the application environment.
2. Setting Up Your Environment
2.1. Installing Docker
- Follow the instructions for your operating system on the official website of docker.
- After installation, verify the installation by checking the version.
docker version
2.2. Create a spring boot application
- Generate a new project using spring initializer.
- Create a simple REST controller.
@RestController
public class HelloController {
@GetMapping("/welcome")
public String welcome() {
return "Let's learn Docker!";
}
}
- Run the application to make sure that the application works fine on local. Open your web browser and go to
http://localhost:8080/welcome
. You should see the message “Let’s learn Docker!”.
3. Dockerizing the Spring Boot Application
3.1. Creating a Dockerfile
As I mentioned earlier, A Dockerfile is a script that contains a series of instructions on how to build a Docker image for your application.
Each instruction in a Dockerfile creates a layer in the image, making it possible to efficiently reuse layers that haven’t changed.
Here are some common Dockerfile instructions:
FROM
: Sets the base image for the Docker image.COPY
orADD
: Copies files from the host system into the Docker image.RUN
: Executes a command in the Docker image, typically used to install dependencies.CMD
orENTRYPOINT
: Specifies the command to run when the container starts.EXPOSE
: Specifies the port on which the application will run inside the container.
Create Dockerfile in the root directory.
# Dockerfile
# Use a base image containing Java runtime
FROM openjdk:17-jdk-alpine # Replace with your desired JDK version
# Set the working directory inside the container
WORKDIR /app
# Copy the Spring Boot jar file into the container
COPY target/spring-boot-docker-example.jar /app/spring-boot-docker-example.jar
# Expose the port that the Spring Boot application runs on
EXPOSE 8080
# Run the Spring Boot application
ENTRYPOINT ["java", "-jar", "/app/spring-boot-docker-example.jar"]
The JAR (Java ARchive) file is the compiled and packaged version of your Java application. It contains all the compiled Java classes, resources, and libraries needed to run your Spring Boot application.
Set the finalName
in the build
section of your pom.xml
to the name of the jar file. This setting ensures that the JAR file generated will be named spring-boot-docker-example.jar
.
<build>
<finalName>spring-boot-docker-example</finalName>
</build>
3.2. Building the Project
mvn clean install
3.3. Building the Docker Image
To build the Docker image, navigate to the directory containing your Dockerfile and run the following command:
docker build -t <image-name>:<tag> .
<image-name>
: This is the name you want to give to your Docker image.
<tag>
: This is the tag you want to assign to the image. ags allow you to differentiate between different versions or variants of an image. For example, you might tag an image as v1.0
, latest
, or with a specific build number (build-123
). If an issue arises, you can easily roll back to a previous version tagged with a known stable release.
The .
(dot) means the current directory is used as the build context. The build context is where Docker looks for files needed to build the image, such as the Dockerfile and any files referenced within it.
Let’s build our docker image.
docker build -t spring-boot-docker-example:v1.0 .
During the build process, Docker reads the Dockerfile line by line, executing each instruction to create layers in the image. The output provides feedback on each step, indicating the progress and any potential issues.
To Verify that the docker image is created, run docker images
command, which lists all available images.
docker images
3.4. Running the Docker Container
To run a container from the built image, use the docker run
command:
docker run -p <host-port>:<container-port> <image-name>:<tag>
<host-port>
: This is the port number on your host machine where you want to map the container port.<container-port>
: This is the port number inside the Docker container that your application is listening on.
Let’s run our docker image.
docker run -p 8080:8080 spring-boot-docker-example:v1.0
-p 8080:8080
: This command maps port 8080
from the container to port 8080
on the host machine.
Requests to localhost:8080
on your host machine are forwarded to port 8080
inside the Docker container where your Spring Boot application is running.
Now, you can access this application from your web browser on http://localhost:8080
.
To Verify that your application is running inside a Docker container, run docker ps
command, which lists all running containers.
docker ps
To list all the containers (Running and Stopped), run below command.
docker ps -a
3.5. Pushing Docker Images to Docker Hub
Log in to Docker Hub. Sign up for a free account if you don’t already have one.
Create a New Repository. Click on the “Create Repository” button on the Docker Hub dashboard. Enter a name for your repository. For example, spring-boot-docker-example-repo
. Optionally, provide a description and set the visibility (public or private). Click on the “Create” button to create your repository.
Once you have your Docker Hub account and repository set up, you can push your Docker image to Docker Hub.
Tag your local Docker image with the repository name on Docker Hub. Use the format <username>/<repository-name>:<tag>
.
docker tag spring-boot-docker-example:v1.0 yourusername/spring-boot-docker-example-repo:v1.0
Push the tagged image to Docker Hub using docker push
.
docker push yourusername/spring-boot-docker-example-repo:v1.0
After pushing, you can verify the image on Docker Hub by logging into Docker Hub, navigating to your repository (yourusername/spring-boot-docker-example-rep
), and checking the "Tags" tab to see the pushed image.
3.6. Pulling Docker Images from Docker Hub
Once your Docker image is pushed to Docker Hub, others can pull and run it on any Docker-enabled environment.
On the machine where you want to run your Docker container, pull the image from Docker Hub.
docker pull yourusername/spring-boot-docker-example-repo:v1.0
Run the pulled image as a container using docker run
.
docker run -p 8080:8080 yourusername/spring-boot-docker-example-repo:v1.0
Open your web browser and navigate to http://localhost:8080
to access your Spring Boot application running inside the Docker container.
4. Continuous Integration and Deployment (CI/CD) with Docker and GitHub actions.
4.1. Understanding CI/CD
1. Continuous Integration: CI is a practice in software development where developers frequently integrate (merge) their code changes into a shared repository.
- You write new code for a feature.
- You push your code to the shared repository.
- Automated tests run to check if your code works.
- If tests pass, the code is merged. If not, you fix the issues and push again.
2. Continuous Deployment: CD is a practice where code changes are automatically deployed (released) to a production environment after they pass CI stage.
- Once the code is merged and all tests pass, the CI system builds a new version of the application.
- The new version is automatically deployed to a live server where users can access it.
- The system monitors the deployment to ensure everything works smoothly.
In summary, CI/CD automates the process of integrating code changes and deploying them to production, making software development faster, more reliable, and less error-prone.
4.2. Understanding GitHub Actions
Traditionally, if we’re using Docker without automation tools like GitHub Actions, we would indeed need to manually build our application, create a Docker image, and push it to a Docker registry (like Docker Hub) every time we make changes to our code.
GitHub Actions is a way to automate these processes.
With GitHub Actions, we can define workflows in YAML files that specify what actions to take when certain events happen (e.g., pushing to a specific branch).
By this way, we can automate tasks like building our application, running tests, building Docker images, pushing them to Docker Hub, and even deploying to a server or cloud platform.
Automating Tasks with GitHub Actions saves time, reduces manual effort and ensures that every code change triggers a consistent build and deployment process.
Firstly we have understood what CI/CD is. Now let’s explore some terminologies in github actions.
Workflow
- A workflow is an automated process that you define in your GitHub repository.
- It is made up of one or more jobs that can be triggered by an event. —
- Workflows are defined in YAML files located in the
.github/workflows
directory of your repository.
Events
- Events are specific activities that trigger a workflow.
- Common events include
issues
,push
,pull_request
,release
, andschedule
.
on:
push:
branches:
- main
Jobs
- A job is a set of steps executed on the same runner.
- Jobs can run in parallel or be dependent on other jobs.
- Runners are servers that run your workflows when they’re triggered.
jobs:
build: #Job 1
runs-on: ubuntu-latest # Runner for Job 1
test: # Job 2
runs-on: ubuntu-latest # Runner for Job 2
needs: build #test will only run if the build job completes successfully.
- By default, jobs in a workflow run in parallel. You can control the order of execution using the
needs
keyword.
Steps
- Steps are individual tasks within a job.
- They can run commands, set up dependencies, or use actions.
- Each step runs in its own process.
jobs:
build: #Job 1
runs-on: ubuntu-latest # Runner for Job 1
steps:
# Step 1
- name: Checkout code
uses: actions/checkout@v2
# Step 2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
# Step 3
- name: Build with Maven
run: mvn clean install
Actions
- Actions are reusable units of code that can be used to perform common tasks.
- You can use actions created by the community or create your own. Actions can be used in steps with the
uses
keyword. - Example of using a pre-built action: Checkout
steps:
- name: Checkout code
uses: actions/checkout@v2
- Example of creating an action by our own.
- name: Build with Maven
run: mvn clean install
Secrets
- Secrets are encrypted variables that you create in your repository to store sensitive information, such as API keys or login credentials.
- Secrets are accessed in workflows using the
secrets
context.
- name: Login to dockerhub
run: docker login -u ${{secrets.DOCKER_USERNAME}} -p ${{secrets.DOCKER_PASSWORD}}
4.3. Writing GitHub Actions Workflow
Go to GitHub and create a new repository.
Add the created repository origin to your project.
In the root directory of our project, We have already created the dockerfile. So move on to next step.
In the root directory, create a directory for your GitHub Actions workflows.
mkdir .github/workflows
Create a workflow file named docker.yml
in the .github/workflows
directory.
# Name of the workflow
name: CI/CD Pipeline for Docker
# Define the events that trigger the workflow
on:
push: # Trigger the workflow on push events
branches:
- main # Only trigger on pushes to the main branch
pull_request: # Trigger the workflow on pull request events
branches:
- main # Only trigger on pull requests to the main branch
# Define the jobs to be run as part of the workflow
jobs:
build:
runs-on: ubuntu-latest # Specify the runner environment
steps:
- name: Checkout code # Step to checkout the repository code
uses: actions/checkout@v2 # Use the pre-built checkout action
- name: Set up JDK 17 # Step to set up Java Development Kit version 17
uses: actions/setup-java@v2 # Use the pre-built setup-java action
with:
java-version: '17' # Specify the JDK version
- name: Build with Maven # Step to build the project using Maven
run: mvn clean install # Run the Maven clean and install commands
- name: Login to Docker Hub # Step to log in to Docker Hub
run: docker login -u ${{secrets.DOCKER_USERNAME}} -p ${{secrets.DOCKER_PASSWORD}} # Login with secrets
- name: Build Docker image # Step to build the Docker image
run: docker build -t yourusername/spring-boot-docker-example-repo:v1.0 . # Build the Docker image with the specified tag
- name: Push Docker image # Step to push the Docker image to Docker Hub
run: docker push yourusername/spring-boot-docker-example-repo:v1.0 # Push the Docker image with the specified tag
Let’s change the message of our endpoint.
@RestController
public class HelloController {
@GetMapping("/welcome")
public String welcome() {
return "Let's learn Docker with GitHub actions!";
}
}
For configuring the secrets, Go to your GitHub repository settings. Navigate to the “Secrets and variables” section and then to “Actions”. Add new repository secrets for DOCKER_USERNAME
and DOCKER_PASSWORD
.
Add, commit, and push your changes to GitHub.
git add .
git commit -m "Set up CI/CD with Docker and GitHub Actions"
git push origin main
Your GitHub Actions workflow will trigger automatically on every push or pull request to the main
branch.
Go to the “Actions” tab in your GitHub repository.
You should see your workflow running.
Once the workflow completes, navigate to your Docker Hub account. Verify that the Docker image has been successfully pushed to your repository on Docker Hub.
Now open your terminal, and once again pull and run the image.
Open your web browser and navigate to http://localhost:8080
to access your Spring Boot application running inside the Docker container.
With GitHub Actions, you no longer need to manually build and push your Docker image every time you make changes.
The workflow you set up automates the entire process. Every time you push code to the repository, GitHub Actions will build the Docker image and push it to Docker Hub for you.
5. Next Steps: Exploring More with Docker
Congratulations on making it through the guide! By now, you should have a solid understanding of Docker, its components, and how to build, push, pull, and run Docker images for your Spring Boot application. You’ve also learned how to automate these processes with GitHub Actions. But this is just the beginning. Here are some exciting areas you can explore next:
5.1 . Advanced Dockerfile Optimization
Dive deeper into Dockerfile best practices and optimizations. Learn how to create more efficient Docker images by minimizing image size, leveraging build caching, and using multi-stage builds.
5.2. Docker Compose
Learn how to manage multi-container Docker applications using Docker Compose. This tool allows you to define and run multi-container applications with ease, making it perfect for developing and testing complex microservices architectures.
5.3. Docker Networking
Explore Docker’s networking capabilities. Understand how containers communicate with each other, how to configure custom networks, and how to secure network traffic between containers.
5.4. Docker Volumes
Learn about Docker volumes and how to manage data persistence in your containers. This is crucial for applications that require data storage beyond the container lifecycle.
5.5. Docker with Microservices
Delve into building and deploying microservices with Docker. Understand how to containerize each service independently, manage inter-service communication, and ensure scalability and resilience in a microservices architecture.
5.6. Continuous Deployment
Extend your CI/CD pipeline to include continuous deployment. Learn how to automatically deploy your Dockerized applications to various environments, such as staging and production, using tools like AWS ECS, Google Kubernetes Engine, or Azure Kubernetes Service.
Keep learning, exploring, and creating amazing things with Docker!
If you found my articles useful, please consider giving it claps and sharing it with your friends and colleagues.
Happy coding!
-Dharshi Balasubramaniyam-