Dockerize Your Angular App with a Java Backend

In today's fast-paced digital landscape, developers face numerous challenges when building and deploying web applications. With the rise of microservices architecture and distributed systems, it's becoming increasingly important to ensure that your application is scalable, secure, and efficient.

One effective way to achieve these goals is by containerizing your application with Docker. In this blog post, we'll explore how to dockerize an Angular app with a Java backend using Docker containers.

What is Docker?

Docker is a lightweight and portable way to package, ship, and run applications in containers. Containers are similar to virtual machines but more lightweight and efficient. They provide a consistent and reliable environment for your application to run in, regardless of the underlying infrastructure.

Benefits of Containerizing Applications with Docker

Containerizing your application with Docker offers several benefits:

  • Improved Portability: Containers can run on any platform that supports Docker, including Linux, Windows, and macOS.
  • Consistent Environment: Containers provide a consistent environment for your application to run in, which ensures that it behaves the same way on different systems.
  • Efficient Resource Utilization: Containers share the host machine's kernel, which means they require less resources (such as memory and CPU) compared to virtual machines.
  • Easy Collaboration: Docker containers make it easy to collaborate with others by providing a consistent environment for development, testing, and production.

Prerequisites

  • List the required tools and software:
    • Docker Desktop (or a similar Docker environment)
    • Docker Hub account (optional)

Dockerizing Angular App

We will start with creating file named Dockerfile in the root of angular app. Then add the following content in it

# Stage 1: Build the Angular application
FROM node:22-alpine as build_stage

# Set the working directory
WORKDIR /app

# Copy package.json and package-lock.json to the working directory
COPY package.json package-lock.json* ./

# Clear npm cache and install dependencies with clean slate
RUN npm cache clean --force && \
    npm install --force

# Copy the rest of the application code
COPY . .

# Run build
RUN npm run build:production

# Stage 2: Serve the Angular application using nginx
FROM nginx:alpine

# Copy the built Angular application from the build_stage to nginx's web directory
COPY --from=build_stage /app/dist/sample-app-frontend/browser /usr/share/nginx/html
# Expose port 80
EXPOSE 80

# Start nginx
CMD ["nginx", "-g", "daemon off;"]

Dockerfile

Here's an explanation of each line in the provided Dockerfile:

FROM node:22-alpine as build_stage

  • This tells Docker to use a pre-existing image (in this case, node:22-alpine) as a base for our new image and assign a temporary tag (build_stage) to the resulting intermediate image.

WORKDIR /app

  • This sets the working directory inside the container to /app. All subsequent COPY and RUN commands will operate from this directory.

COPY package.json package-lock.json* ./

  • This command copies files or directories from the current build context (the directory containing the Dockerfile) into the container.

RUN npm cache clean --force && npm install --force

  • This command executes a shell command inside the container. Clears the npm cache to ensure dependencies are reinstalled from scratch. Then install the dependecies.

COPY . .

  • This line copies the rest of the application code (everything except package.json and package-lock.json) into the container.

RUN npm run build:production

  • This command builds the Angular application using the npm script build:production.

FROM nginx:alpine

  • This starts a new stage for serving the built Angular application with Nginx. This is done to keep the final image light weight, we don't need full source code and compiler to run the application

COPY --from=build_stage /app/dist/sample-app-frontend/browser usr/share/nginx/html

  • The command copies the built Angular application from the previous build_stagestage into the Nginx container.
  • The name sample-app-frontend in /app/dist/sample-app-frontend/browser is the name of app defined in angular.json

EXPOSE 80

  • This line exposes port 80, which is used by Nginx to serve HTTP requests.

CMD ["nginx", "-g", "daemon off;"]

  • The final command sets the default command for the container. It starts Nginx with the daemon off flag, ensuring it runs in foreground mode.

Dockerizing Java App

We will create a file named Dockerfile in the root of java app. And add the following content in it

FROM maven:3.9.9-sapmachine-21 as build_stage

#Change working directory to /app
WORKDIR /app

# Copy files to /app directory
COPY pom.xml ./
COPY . .

#Execute maven build
RUN mvn package

#Copy build from /app to /
RUN cp target/sample-app-backend*.jar /sample-app-backend.jar

#Change working directory to /
WORKDIR /

FROM eclipse-temurin:21-jre-alpine

# COPY compiled jar file to new image
COPY --from=build_stage sample-app-backend.jar .

# Deploy Build
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "sample-app-backend.jar"]

Dockerfile

Here's an explanation of each line in the provided Dockerfile for the Java app:

FROM maven:3.9.9-sapmachine-21 as build_stage

  • This tells Docker to use a pre-existing image (in this case, maven:3.9.9-sapmachine-21) as a base for our new image and assigns a temporary tag (build_stage) to the resulting intermediate image.

WORKDIR /app

  • This sets the working directory inside the container to /app. All subsequent COPY and RUN commands will operate from this directory.

COPY pom.xml ./

  • This command copies pom.xml file from the current build context (the directory containing the Dockerfile) into the container.

COPY . .

  • This command copies src directory from the current build context (the directory containing the Dockerfile) into the container.

RUN mvn package

  • This line runs Maven's package goal to build and compile the Java application. Maven's output (including the compiled JAR file) is stored in the /target/ directory.

RUN cp target/sample-app-backend*.jar /sample-app-backend.jar

  • This line copies all files with names matching sample-app-backend*.jar from the /target/ directory to a new location (/sample-app-backend.jar).
  • The name sample-app-backend.jar is the name of app defined in pom.xml

FROM eclipse-temurin:21-jre-alpine

  • This line starts a new stage for serving the built jar file with jre. This is done to keep the final image light weight, we don't need full source code and jdk to run the application

COPY --from=build_stage sample-app-backend.jar .

  • This command copies the built jar file from the previous build_stagestage into the JRE container.

EXPOSE 8080

  • This line exposes port 8080, which is used by Java applications for HTTP requests.

ENTRYPOINT ["java", "-jar", "sample-app-backend.jar"]

  • The final command sets the default command for the container. It start the jar file and bring up the backend

With this we are finished with updating our angular and java apps. Now we can move to utilize these newly created DockerFile and build the images

Building the images

We will start will creating a new folder named deployment where backend and frontend folder are present. Inside this folder we will create a file to build the images. We will name this file as docker-compose.build.yml. The content of the file will be :

services:
  sample-app-frontend:
    image: "sample-app-frontend:latest"
    build:
      context: ../frontend
      dockerfile: Dockerfile
  sample-app-backend:
    image: "sample-app-backend:latest"
    build:
      context: ../backend
      dockerfile: Dockerfile

docker-compose.build.yml

The file will be used build both frontend and backend docker images using it respective DockerFile . Once done, we can run the following command in terminal/command prompt.

docker compose -f .\deployment\docker-compose.build.yml build

command

Result will be something like this

You can also check the build image from docker desktop

You can also use the following command

docker images

command

Result will be something like this

Running the images

We are now in the home stretch of the process. We will create three more file in the deployment directory

networks:
  webnet-sample-app:
    driver: bridge

services:
  sample-app-frontend:
    image: "sample-app-frontend:latest"
    restart: unless-stopped
    ports:
      - "8081:80"
    networks:
      - webnet-sample-app
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro # Mount NGINX configuration file
  sample-app-backend:
    image: "sample-app-backend:latest"
    restart: unless-stopped
    env_file:
      - .env.local
    networks:
      - webnet-sample-app

docker-compose.yml

The file is mostly similar to docker-compose.build.yml file, but this time instead of building the apps, this file will be used to run the built images. The networks section creates a network named webnet-sample-app using the default bridge driver, which allows containers to communicate with each other within this network. Both services are then configured to use this network for communication. Additionally, several key configuration options have been added:

  • ports:: Exposes port 8081 on the host machine and maps it to port 80 inside the container, allowing external access to the application.
  • restart: unless-stopped: Configures Docker Compose to restart the containers if they exit due to a failure or signal, but not when stopped manually.
  • volumes:: Mounts an NGINX configuration file located in ./nginx.conf as read-only (ro) within the container at /etc/nginx/nginx.conf, allowing custom configurations to be applied without modifying the image itself. .env.local is an environment variables file that stores sensitive information, such as database credentials. This file is not committed to version control, which means it's not shared with others or stored in the repository.
events {}

http {

include       mime.types;
default_type  application/octet-stream;

server {
	listen       80;
	server_name  localhost;

	root   /usr/share/nginx/html;

	index  index.html;

	# Serve the index.html for all requests to support client-side routing
	location / {
		try_files $uri $uri/ /index.html;
	}

	location /api {
		# 'sample-app-backend' is the docker image name of the backend 
		proxy_pass http://sample-app-backend:8080; 
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection "upgrade";
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header X-Forwarded-Proto $scheme;
		proxy_ssl_server_name on;
		proxy_ssl_verify off;
		}
	}
}

nginx.conf

This is an Nginx configuration file that sets up a web server to serve the Angular application. /api { ... }: This block is used for proxying requests to the backend server (sample-app-backend:8080). It sets up proxy headers to forward information like the original request URL and IP address.

#Database
MARIADB_HOST_NAME=
MARIADB_PORT=
MARIADB_DB_NAME=
MARIADB_USER=
MARIADB_PASSWORD=

.env.local

Deploying the Images

We can run the following command in terminal/command prompt.

docker compose -f .\deployment\docker-compose.yml up

command

Result will be something like this

At this point you should be able to access the app on your browser using the following url

http://localhost:8081

Pushing the image to repo

With the app built and running successfully on your machine, it can be easily shared with rest of your team or even deployed on cloud. All you need is to push the image to a docker repository using this command

docker push sample-app-frontend:latest
docker push sample-app-backend:latest

command

The commands will push the images to you dockerhub repo.

Pushing to custom repo

If you want to push the images to a custom location say example.repo.com, you can tag the image with it and push the custom repo


docker tag sample-app-frontend:latest example.repo.com/sample-app-frontend:latest
docker tag sample-app-backend:latest example.repo.com/sample-app-backend:latest

command

Now when you will push the image, it will saved in custom repo


docker push example.repo.com/sample-app-frontend:latest
docker push example.repo.com/sample-app-backend:latest

command

Deploying on other computer/server

To deploy the image somewhere else, you just need these

  1. Access to image repo
  2. docker-compose.yml, nginx.conf, .env.local files

Once you set the database credentials in .env.local file, you can just run the following command and the app will spin up.

docker compose -f docker-compose.yml up

command

Conclusion

Through this blog post, we've demonstrated how to dockerize an Angular app with a Java backend using Docker containers, making it easier to deploy, manage, and scale your application. By following these steps and embracing containerization, you'll be well-equipped to tackle the challenges of building complex web applications in today's digital landscape.

GitHub Repository:

You can find all the code from this blog on GitHub:

GitHub - navalgandhi1989/angular-java-docker: This is an example project of angular with java
This is an example project of angular with java. Contribute to navalgandhi1989/angular-java-docker development by creating an account on GitHub.