How to Use Docker and Docker-Compose for MySql, PhpMyAdmin, Spring Boot, Angular Stack CRUD Web Application Development

A Star Wars Quotes Web Application

Image for post
Image for post

👋Introduction

Most developers use Docker daily base or once in a while in the product development or at the deployment life cycle. It is very powerful and fun to play with the tool. But if you use quite a deal of docker containers, a management issue arises. Docker-composer provides a simple and fast solution for container management, on the other hand, it has its own shortcomings which lead us to systems like Kubernetes and Docker Swarm but they are more DevOps tools nevertheless knowing how they work and tampering with them don’t hurt. So I decided to show you how I use docker and docker-compose to create a CRUD web application. I used MySql for database, PhpMyAdmin for database visualization, Spring Boot(Java) as a back-end, and Angular 9 as a front-end. I am also planning to use Kubernetes for this project in the future.

The project is going to be about Quotes with Start Wars Theme and I am going to use Star Wars: The Clone Wars Episode Opening Quotes (I don’t know why but secretly I always want to do an app about that 😁) as initial data.

The final, front-end going to look like this;

Image for post
Image for post

You can find the final code in this GitHub repo;

👉What This Post NOT About👈

  1. Not step by step guide. I am just going to explain some important parts, some code snippets, some screenshots, and a GitHub repository for the entire project which you can dive into.
  2. Not about Docker inner mechanism or Docker 101
  3. Not a deep down explanation for Docker-compose just an example of its usage
  4. Not a Spring Boot CRUD web application tutorial. There are enough of them out there.
  5. Not a production-ready code but definitely helps.

👉What This Post About👈

  1. To show how I work with Docker and Docker-compose
  2. To show how you can use them to simplify your workflow
  3. To give you a reference codebase.

📖TLTR;

  1. Create a Spring Boot BE

— By spring initializr

— By IntelliJ

2. Spring Boot “Hello World!”

3. Creating Application REST API Endpoints

4. Creating a MySql Container

5. Create a PhpMyAdmin Container and Connect the MySql Container

6. What is Docker Compose

7. Use Docker Compose to Create MySql and PhpMyAdmin Containers

8. Connect Spring Boot to Docker MySql Container for Local Development

9. Creating Angular FE

10. Reconfigure File Structure

11. ‍Dockerize Spring Boot Application

12. Dockerize Angular Application

13. Use Docker Compose to Manage Life Cycle of Containers

14. Configure FE to Communicate BE Over Docker Network

— What is Our BE URL at Docker Network

— NGINX Reverse Proxy

— Configure Environment

Image for post
Image for post

Create a Spring Boot BE

By spring initializr

Image for post
Image for post

content of generated .zip file

Image for post
Image for post

By IntelliJ

Image for post
Image for post
Image for post
Image for post
Image for post
Image for post

Spring Boot “Hello World!”

Separating project into sub-packages is one of the common approaches so this will be our final our file structure

Image for post
Image for post

but for now, the following one is enough,

Image for post
Image for post

🧠Note: We changed the location of CrudApplication.java so we should say Spring where it should scan for components for proper creation of beans. We are going to add more annotation here in the future.

@SpringBootApplication
//the following line has been added
@ComponentScan("com.example.crudapplication")
public class CrudApplication {

public static void main(String[] args) {
SpringApplication.run(CrudApplication.class, args);
}

}

And I create the QuoteController

@RestController
@RequestMapping("api/v1")
public class QuoteController {

@GetMapping
public String helloWorld(){
return "Hello World!";
}
}
Image for post
Image for post

Creating Application REST API Endpoints

Tips for Lombok users: If you are using Lombok as I am, there is a nice touch that you can use with @Autowired. I used to use the constructor approach at Dependency Injection so my code looks like this

Image for post
Image for post

but now, I use Lombok @AllArgsConstructor and my code looks like this

Image for post
Image for post

it is helpful especially if there are lots of dependencies 😍

REST APIs

GET api/v1/quotes → to retrieve all quotes

GET api/v1/quotes/:id → to retrive single quoteById

POST api/v1/quotes → to save a quote

PUT api/v1/quotes/:id → to update a quote

DELETE api/v1/quotes/:id → to delete a quote

Image for post
Image for post

As you see it is a common rest endpoint pattern and there are things we can tweak more but I am not.

Creating a MySql Container

We are going to use the following MySql Docker image.

Image for post
Image for post
docker run -d -p 3306:3306 --name=docker-mysql --env="MYSQL_ROOT_PASSWORD=password" --env="MYSQL_DATABASE=crud-application" mysql
  • d → detached mode means docker runs container at the background
  • -p 3306:3306 → container’s port 3306(MySql default port) redirected host machine’s port 3306
  • -name=docker-mysql → docker gives a random name for every container but instead, I prefer to name my own container name
  • -env= → environment variables that you can find at docker image documentation
  • mysql → the docker image, as default the last version, is pulled

It is going to pull the image first because I don’t have it on my local. After it will run the container with the downloaded image.

Image for post
Image for post

List of images docker image ls

Image for post
Image for post

List of containers docker container ls

Image for post
Image for post

To access the running container docker exec -it <container-name> bash or docker exec -it <cotainer-name> sh one of them should work depending on the base Linux image of the downloaded image.

Image for post
Image for post

After accessing the container use mysql -u root -p the command to connect MySQL and use SHOW DATABASES; the command to list databases. crud-application the database is there as we said in -env variable.

Image for post
Image for post

Create a PhpMyAdmin Container and Connect the MySql Container

We are going to use the following PhpMyAdmin Docker image.

Image for post
Image for post
docker run -d --name docker-phpmyadmin --link docker-mysql:db -p 8081:80 phpmyadmin

Same drill as before and as an addition to the previous command we have link which provides access to another container running in the host.

Image for post
Image for post
Image for post
Image for post

We redirected docker container’s 80 port to host 8081 port which you can see above so to reach PhpMyAdmin UI go tohttp://localhost:8081 user: root and password: password.

Image for post
Image for post

and you should connect the DB.

Image for post
Image for post

What is Docker Compose

Creation of MySql and PhpMyAdmin are connected to each other and every time you need to change something, you have to run docker codes again and imagine what you have to do if the container number increase. Of course, you can write some script to automize this process but there is an easy way and called as docker-compose. We just need to create a YAML file and docker-compose is going to handle the rest for us🥰

Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration. To learn more about all the features of Compose, see the list of features.

🧠Note: Docker-compose is written in python, if you curious, you can check its GitHub repository.

Use Docker Compose to Create MySql and PhpMyAdmin Containers

What docker-compose is explained in the previous section so there is a small example. I create a YAML(.yaml or .yml) file as docker-compose.yaml. Its directory not important but generally it placed under the same project so I do the same and put it under the project directory.

version: "3.3"
services:
db:
image: mysql
restart: always
container_name: mysql-db
environment:
MYSQL_ROOT_PASSWORD: 'password'
MYSQL_DATABASE: 'crud-application'
ports:
- 3306:3306
phpmyadmin:
image: phpmyadmin
restart: always
container_name: php-my-admin
ports:
- 8081:80
environment:
PMA_HOST: mysql-db

two create container use following command

sudo docker-compose -f docker-compose.yaml up

you can use -d option to use as detached mode, which I do for the rest.

🧠Note: Creating network "..." with default driver this line important quote from documentation;

By default Compose sets up a single network for your app. Each container for a service joins the default network and is both reachable by other containers on that network, and discoverable by them at a hostname identical to the container name.

$docker-compose -f docker-compose.yaml up -d
Image for post
Image for post

and our containers are ready and everything works just fine.

Image for post
Image for post

Connect Spring Boot to Docker MySql Container for Local Development

1- Add dependencies to pom.xml

<dependency>
<
groupId>org.springframework.boot</groupId>
<
artifactId>spring-boot-starter-data-jpa</artifactId>
</
dependency>
<dependency>
<
groupId>mysql</groupId>
<
artifactId>mysql-connector-java</artifactId>
<
scope>runtime</scope>
</
dependency>

2- Update application.properties

There are 4 properties and 3 variable, I want to cover 2 cases. Case 1 spring-boot should connect containers while local development and case 2 spring boot container should connect database-container when docker-compose run them. So I entered hardcoded values for local development and not hardcoded once for docker-compose enviromet variables.

we should use MySql container URL asspring.datasource.url in our case localhost:3306

Image for post
Image for post
Image for post
Image for post

3- Create an entity

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Quote {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String title;
private String quote;
private String source;
@Builder.Default private long date = Instant.now().getEpochSecond();
}

4- Create repository

public interface QuoteRepository extends CrudRepository<Quote, Integer> {
}

🧠Note: For spring boot to find our Repository and Entity we should add the following annotations

@SpringBootApplication
@ComponentScan("com.example.crudapplication")
@EnableJpaRepositories("com.example.crudapplication.repository")
@EntityScan("com.example.crudapplication.model")
public class CrudApplication {

public static void main(String[] args) {
SpringApplication.run(CrudApplication.class, args);
}

}

This is our current file structure.

Image for post
Image for post

Testing … ( we run via ide for IntelliJ in my case)

Image for post
Image for post
Image for post
Image for post
Image for post
Image for post

Creating Angular FE

For the front-end, I create a very simple Angular application that includes a service for HTTP calls, a model for Quote, and a modal(dialog) for quote creat-update operations.

ng new quote
ng g s services/quote
ng g c components/modal
ng generate class models/quote --type=model
Image for post
Image for post

I put the following screenshots for fast demonstration, you can check the GitHub repository for further code detail.

quote.service.ts

Image for post
Image for post

modal.component.ts

Image for post
Image for post

app.component.ts

Image for post
Image for post
ng serve
Image for post
Image for post
Image for post
Image for post

Reconfigure File Structure

So to prevent confusion I change the FE Angular project’s name as FE and the BE Spring Boot- Java project’s name as BE also create a wrapper folder as Quote-CRUD-Application and moved the projects inside it.

🧠Note: Basically I turned them polyrepo to a monorepo.

Image for post
Image for post

‍🏭Dockerize Spring Boot Application

Dockerizing a Spring Boot web application pretty straightforward but there are 3 approaches you can find at this link. I use the multi-stage docker image build approach. The purposes of this approach are decreasing the final image size and leverage the docker cache mechanism to faster image build time. Please check docker documentation for further information about docker inner mechanism that topic not in the scope of the post.

1- Create a Docker file under the root directory of the project

$touch Dockerfile

2- copy&paste following code

# Build Stage
FROM maven:3.6-openjdk-8-slim AS build
COPY pom.xml /app/
COPY src /app/src
RUN mvn -DskipTests -f /app/pom.xml clean package

# Run Stage
FROM openjdk:8-jre-alpine
COPY --from=build /app/target/crud-application*.jar /app/app.jar
ENTRYPOINT ["java", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap","-Djava.security.egd=file:/dev/./urandom","-jar","/app/app.jar"]

As you notice there is 2 stage;

Build stage → Use maven to create a jar

Run stage → Copy jar file from the previous build and define an ENTRYPOINT for docker container.

‍💻Dockerize Angular Application

I am going to use the same multi-stage Docker image build approach.

1- Create a Docker file under the root directory of the project

$touch Dockerfile

2- copy&paste following code

# Build Stage
FROM node:12.7-alpine AS build
WORKDIR /app
COPY package*.json /app/
RUN npm install
RUN npm install -g @angular/cli@9.0.0-rc.2
COPY ./ /app/
RUN ng build --extract-css --output-path=dist --prod=true


# Run Stage
FROM nginx:1.17.1-alpine
COPY default.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html

3- Create a default.conf file under the root directory of the project for nginx

$touch default.conf

4- copy&paste following code

server {
listen 4200;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
error_page 404 =200 /index.html;
}
}

💃Use Docker Compose to Manage Life Cycle of Containers

We finally reach the end, more excited, and more advanced part of the post 🙂

Image for post
Image for post

This is our current folder structure. I added a dumb.sql file. If you want the application’s data to persist regardless of your container life cycle you can assign a volume from the host machine to the MySql container but I don't have such a concern so my choice is to insert dummy data every time the MySql container run and delete all data when container removed but data will insist when container just stopped.

1- Create a docker-compose file under the root directory of all the projects

$touch docker-compose.yaml

2- copy&paste following code

services → docker container template created under it

mysql-db, php-my-admin, springboot-app, angular-app → service names

image → If you use an image from a Registry, image source as default DocherHub

build → If use a Dockerfile instead of an already created image, Dockerfile path

container_name → custom container name

environment → environment variables

If you want more information you can check docker-compose documentation.

$docker-compose -f docker-compose.yaml up -d

🧠Note: if you change application code and need to get new build use --build argument

$docker-compose -f docker-compose.yaml up -d --build
Image for post
Image for post
Image for post
Image for post

👊 everything works as it should

Image for post
Image for post

Configure FE to Communicate BE Over Docker Network

This is the final touch. I mentioned about creation of a network by docker-compose in one of the previous sections. You can learn more about docker networking via documentation.

Image for post
Image for post

When you say docker-compose up it creates a default network. Use the following command to see docker networks

$docker network ls
Image for post
Image for post

Our FE communication with BE over http://localhost:4200/. It is okay for the local development but I want my FE to communicate over the Docker network.

What is Our BE URL at Docker Network

This is the first question that comes to our mind. The answer is BE container name: in our casespringboot-app . So it is easy we just change http://localhost:4200/ to http://springboot-app:8080 and it is done. NO unfortunately not that easy😞

Image for post
Image for post

NGINX Reverse Proxy

So to solve this problem, we should configure the NGNIX to redirect our HTTP call to our BE service. Update default.conf as follows;

When NGINX proxies a request, it sends the request to a specified proxied server, fetches the response, and sends it back to the client. It is possible to proxy requests to an HTTP server (another NGINX server or any other server) or a non-HTTP server (which can run an application developed with a specific framework, such as PHP or Python) using a specified protocol. Supported protocols include FastCGI, uwsgi, SCGI, and memcached.

server {
listen 4200;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
error_page 404 =200 /index.html;
}

location /api/v1{
proxy_pass http://springboot-app:8080/api/v1;
}
}

So when FE made a call to /api/v1 Nginx redirect it to http://springboot-app:8080/api/v1 .

Configure Environment

environment.ts

export const environment = {
production: false,
apiUrl: 'http://localhost:8080/api/v1'
};

environment.prod.ts

export const environment = {
production: true,
apiUrl: '/api/v1'
};
Image for post
Image for post
$docker-compose -f docker-compose.yaml up --build

--build →docker-compose get a new build.

And everything works🎉🎉

Image for post
Image for post

Junior Full-Stack Developer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store