- vừa được xem lúc

Dockerize project Java Spring Boot, MySQL, Redis

0 0 3

Người đăng: Mai Trung Đức

Theo Viblo Asia

Hello các bạn lại là mình đây 👋👋

Một ngày cuối năm miền bắc lạnh quá trời lạnh 🥶🥶

Tiếp nối series học Docker và CICD, ở bài hôm nay chúng ta sẽ cùng nhau Dockerize project Java Spring Boot dùng MySQL và Redis nhé.

Từ lâu rồi mình đã nghĩ là viết thêm về nhiều ngôn ngữ để chúng ta đa dạng hoá bài toán trong thực tế, mà cứ nhớ rồi quên suốt 😂😂

Triển thôi nào 🚀🚀

Clone source

Như thường lệ các bạn clone source code của mình ở đây nhé. Nhánh master

Ở bài này ta sẽ chỉ quan tâm folder docker-java-spring-boot-mysql-redis nhé

Chạy thử ở local

Khi Dev

Tổng quan project thì ở đây ta có project Spring Boot, Java 17:

  • trong bài ta sẽ có một app CRUD users, data sẽ được lưu vào MySQL
  • có login/logout, session của chúng ta sẽ được lưu vào Redis

Phần này nếu máy local của các bạn không có Java 17/MySQL/Redis thì ngồi xem thôi cũng được nhé 😄, phần sau ta vô Docker thì các bạn có thể bắt đầu làm, nhưng các bạn nhớ phải xem phần Local này nha để tí nữa sẽ có một vài điểm ta thảo luận trong bài có liên quan đấy

Trước khi chạy ta test xem đã cài và chạy đủ các thứ cần thiết chưa nha, bao gồm: Java 17, MySQL, Redis:

java --version curl localhost:3306 curl localhost:6379

Nếu port của MySQL hoặc Redis của các bạn khác thì các bạn thay đổi đi nha

Tiếp theo ở file src/main/java/resources/application.properties các bạn có thể thấy là:

  • app sẽ chạy ở cổng 8081
  • với MySQL mình đang để mặc định tên db là java_test, database user/password là root/rootpass
  • user mặc định để tí login vào app là user/Password1

Các bạn sửa lại những thông tin trên sao cho đúng với máy local của các bạn nhé

File application.properties là file cấu hình cho Spring project, nom na ná như .env ở các ngôn ngữ khác mà ta hay dùng thôi 😉

Ở root folder project mình cũng có 1 file db.sql để tạo sẵn 1 bảng trong database, các bạn tự chạy script đó để tạo bảng nhé.

Cuối cùng là ta chạy project lên thôi nào:

./gradlew bootRun

Ta mở trình duyệt ở địa chỉ http://localhost:8081 rồi login với user user/Password1 và ta sẽ thấy giao diện chính:

Các bạn thử thêm sửa xoá 1 vài users xem mọi thứ có oke không nha

Build

Ở trên ta chạy ./gradlew bootRun là dùng khi ta dev ở local, còn khi ta chạy production thì ta sẽ cần phải build ra file JAR để app được tối ưu và sẵn sàng cho deploy nhé, giống kiểu development và production mode khi làm với Frontend Javascript ấy 😉

Để build project thì ta chạy command sau:

./gradlew build

Sau khi build xong ta sẽ thấy ở folder build/libs có 2 file JAR:

Ta chạy thử lên xem nhé:

java -jar demo-0.0.1-SNAPSHOT.jar

Sau đó các bạn lại mở browser ở địa chỉ http://localhost:8081 rồi tự test nha

Vậy là các bạn đã thấy khi dev và deploy project java thì ta cần làm gì rồi, giờ ta tới phần tiếp theo là đưa tất cả vào Docker nha 🚀🚀🚀

Dockerize

Phần này ta sẽ tập trung nhiều vào việc build cho lúc Deploy (production) nhé, cuối bài ta sẽ nói tới đoạn build cho development.

Để Dockerize project Java thì mình thấy có 2 cách phổ biến:

  • Build môi trường ngoài -> copy JAR vào Docker image và chạy
  • Build + run toàn bộ trong Docker

Ta bắt đầu nha

Build ngoài chạy trong

Vì bước này build ở môi trường ngoài nên yêu cầu máy các bạn có Java, nếu các bạn không có thì ta cũng ngồi xem chờ phần sau sẽ có việc để làm nha 😄

Nếu ta search trên Google "Dockerize Java Spring" thì hầu hết các tutorial đều hướng dẫn ta làm theo cách Build ngoài chạy trong này cả, từ trang chủ của Spring đến như trang blog đại ca như BaelDung:

Thực tế là ở trang chủ Spring mãi cuối họ có nói tới những cách để viết Dockerfile tốt hơn, nhưng mình thấy người đọc có thể dễ dàng bỏ qua nó 😃

Cụ thể là ta sẽ build JAR ở môi trường ngoài trước, tức là yêu cầu môi trường ngoài phải có Java, sau đó copy file JAR vào trong image và chạy file JAR ở trong image đó

"Môi trường ngoài" ở đây có thể là:

  • máy local của chúng ta (hoặc của đồng đội chúng ta 😃)
  • môi trường CICD (Gitlab Runner, Github Actions...)

Ta triển thôi.

Đầu tiên ta tạo Dockerfile như thường lệ nhé:

FROM openjdk:17-jdk-alpine3.14
WORKDIR /app
COPY ./build/libs/demo-0.0.1-SNAPSHOT.jar app.jar
CMD ["java","-jar","app.jar"]

Ở trên code ngắn gọn rõ ràng thì không cần phải giải thích gì thêm nhỉ 😉 Đơn giản là ta COPY file JAR mà tí nữa ta sẽ build từ môi trường ngoài vào trong image

Tiếp theo ta build project nhé (yêu cầu máy gốc phải có Java):

./gradlew build

Nếu bạn nào bị lỗi test fail ở bước này thì thêm dùng ./gradlew build -x test để bỏ test lúc build nhé

Xong rồi thì ta build image thôi

docker build -t java-spring:v1 .

Sau đó ở root folder project các bạn tạo cho mình file docker_application.properties, file này là cấu hình cho app của chúng ta chạy trong container với nội dung như sau nhé:


server.port=8081 spring.datasource.url=jdbc:mysql://db:3306/demo_app
spring.datasource.username=root
spring.datasource.password=rootpass spring.security.user.name=user
spring.security.user.password=Password1
spring.security.user.roles=USER spring.session.store-type=redis
spring.data.redis.host=redis
spring.data.redis.password=
spring.data.redis.port=6379

Tiếp theo ta tạo file docker-compose.yml để chuẩn bị chạy project nè 😉:

version: '3.7' services: app: image: java-spring:v1 restart: always ports: - "8081:8081" volumes: - ./docker_application.properties:/app/application.properties:ro db: image: mysql:8 restart: always volumes: - ./.docker/data/db:/var/lib/mysql - ./db.sql:/docker-entrypoint-initdb.d/db.sql:ro environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: demo_app redis: image: redis:6-alpine restart: always volumes: - ./.docker/data/redis:/data

Ở trên có 1 số file ta mount volume và set :ro ý bảo là đừng có ai tính thay đổi file này nha (cho chắc chắn thôi 😉)

Cuối cùng là ta chạy project lên thôi nào:

docker compose up -d

Sau đó truy cập trình duyệt ở địa chỉ http://localhost:8081, thấy. như sau là xinh tươi rồi nhé 😎😎:

Ta đăng nhập và thêm sửa xoá user tí nha 😘😘:

Thế là xong rồi, học từ đầu series đến bài này thì lại thấy đơn giản quá rồi phải không các bạn 🤣🤣

Ta tiếp tục với các làm thứ 2 nha

Build + Run trong Docker

Qua phần trên thì có vài câu hỏi đặt ra:

  • Thế máy local của tôi không có Java thì sao? Mà có thì nó không phải Java 17 mà là bản cũ Java 8/11 thì sao?
  • Máy mình có đủ vậy có chắc là tất cả các team member đều có Java?
  • Có chắc là ở trong CICD cũng có đủ như vậy? nhỡ môi trường CICD không có?

Thì phải bảo team cài cho có, đòi CICD nơi ta đang mua dịch vụ cài vào cho là xong, đơn giản mà 😂😂😂

Ta thấy vấn đề rồi chứ, đây chính là những lúc mà Docker lên tiếng với đúng mục đích của nó, giúp ta đồng bộ/tối giản quá trình build/deploy trên mọi môi trường, local/CICD/dev/production, Win/Mac/Linux... mọi nơi

Giờ việc của ta là đưa quá trình build vào Docker luôn là oke rồi 😉

Đầu tiên là ta down project đi đã nhé:

docker compose down

Ta sửa lại Dockerfile như sau nhé:

FROM openjdk:17-jdk-alpine3.14 as build
WORKDIR /app
COPY . .
RUN ./gradlew build -x test
CMD ["java","-jar","build/libs/demo-0.0.1-SNAPSHOT.jar"]

Ở trên ta thêm -x test lúc build để skip đoạn test nhé, vì lúc test nó cũng tạo 1 instance của app chúng ta và nó cố gắng kết nối tới DB để test và sẽ failed đó

Tiếp theo ta tạo file .dockerignore để loại bỏ các files/folders không cần thiết lúc COPY vào image nhé:

HELP.md
.gradle
build
### VS Code ###
.vscode/
.docker
.idea .git* .dockerignore
Dockerfile
docker-compose.yml
db.sql

Cuối cùng là ta build image nha:

docker build -t java-spring:v2 .

Sau đó ta sửa lại tag của app ở docker-compose.yml thành v2 và chạy project lên nha:

docker compose up -d

Chờ chút cho app Java khởi động nha (chừng 10-15s) sau đó ta lại mở trình duyệt ở địa chỉ http://localhost:8081 và lại test như thường nha

Tối ưu image size

Giờ ta thử check các images mà ta đã build xem nhé:

docker images --->> REPOSITORY TAG IMAGE ID CREATED SIZE
java-spring v2 f59c55951845 2 minutes ago 679MB
java-spring v1 fcebd8f22de2 25 minutes ago 396MB

Sao image v2 build + run trong container lại nặng thế nhỉ?? Ta có .dockerignore rồi mà 🤔🤔🤔

Thực tế thì đúng là ta đã cho các files/folders không cần thiết vào .dockerignore để không COPY chúng lúc build image.

Nhưng trong quá trình build thì cũng có nhiều thứ được sinh ra thêm, ví dụ folder .gradle, folder build. Ta có thể kiểm tra việc này bằng cách chạy command sau để list file trong container:

docker compose exec app ls -la

Cùng với đó là chúng ta để ý rằng, với app Java ta chỉ cần file JAR để chạy production, vậy thì ta cần gì những thứ khác nữa sau khi build. Giống như phần trên Build ngoài chạy trong vậy.

Vậy giờ có kiểu gì mà vẫn Build + Run trong container, nhưng ta chỉ giữ lại mỗi file JAR và bỏ hết những cái khác đi không nhỉ?

May quá ở bài Dockerize ReactJS/VueJS ta đã có multi-stage build. Ý tưởng y hệt: chia quá trình build thành 2 giai đoạn, 1 để build, build xong thì lấy mỗi bundle file cần thiết để chạy production.

Đầu tiên là ta down app đi đã nhé:

docker compose down

Ta sửa lại Dockerfile như sau nha:

FROM openjdk:17-jdk-alpine3.14 as build
WORKDIR /app
COPY . .
RUN ./gradlew build -x test FROM openjdk:17-jdk-alpine3.14 as production
WORKDIR /app
COPY --from=build /app/build/libs/demo-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java","-jar","app.jar"]

Ở trên ta có 2 stage (2 lần FROM), stage build sẽ chạy buildcommand, sau đó ở stage production ta COPY lấy mỗi file JAR ở stage build là xong. Cũng đơn giản nhỉ 😃

Tiếp theo ta build image nha:

docker build -t java-spring:v3 .

Sau đó ta sửa lại docker-compose.yml update tag image thành v3, chạy lên test thử nha

Bây giờ ta thử check image xem size như thế nào nha:

docker images -->>
REPOSITORY TAG IMAGE ID CREATED SIZE
java-spring v3 87b851cd4d65 33 seconds ago 396MB
java-spring v2 f59c55951845 7 hours ago 679MB
java-spring v1 fcebd8f22de2 8 hours ago 396MB

Giờ đây ta đã thấy là image v3 size nhỏ đi rất nhiều và y hệt bằng v1 rồi 🥳🥳🥳🥳🥳

Chọn image

Hiện tại ta đang FROM từ image openjdk, vậy nhưng trên trang chủ của image đó đã báo DEPRECATION NOTICE:

Ý họ bảo là image này đã lỗi thời và ta nên tìm các image thay thế.

Ta dùng theo trang chủ Spring Boot dùng image eclipse-temurin nhé. Các bạn sửa lại Dockerfile như sau:

FROM eclipse-temurin:17-jdk-alpine as build
WORKDIR /app
COPY . .
RUN ./gradlew build -x test FROM eclipse-temurin:17-jdk-alpine as production
WORKDIR /app
COPY --from=build /app/build/libs/demo-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java","-jar","app.jar"]

Sau đó ta build image:

docker build -t java-spring:v4 .

Build xong các bạn sửa lại docker-compose.yml thành v4 rồi chạy test lại xem mọi thứ oke không nhé. Phần này các bạn tự làm nha 😄

Tiếp theo ta thử check image size xem có gì thay đổi không:

docker images ---->>
REPOSITORY TAG IMAGE ID CREATED SIZE
java-spring v4 c2ac16deb3c4 26 minutes ago 386MB
java-spring v3 87b851cd4d65 5 hours ago 396MB
java-spring v2 f59c55951845 12 hours ago 679MB
java-spring v1 fcebd8f22de2 13 hours ago 396MB

Giảm được 10MB so với khi FROM từ image của openjdk 😄.

Tối ưu image size hơn nữa

Ta để ý rằng sau khi ta đã build ra file JAR, thì trong phần lớn các app ta hầu như không cần cả bản đầy đủ JDK khi chạy nữa (runtime), mà ta có thể chuyển qua dùng JRE.

Ta sửa lại Dockerfile chút như sau nhé:

FROM eclipse-temurin:17-jdk-alpine as build
WORKDIR /app
COPY . .
RUN ./gradlew build -x test FROM eclipse-temurin:17-jre-alpine as production
WORKDIR /app
COPY --from=build /app/build/libs/demo-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java","-jar","app.jar"]

Các bạn để ý rằng ở stage production ta đã chuyển qua dùng image jre rồi

Ta build lại image:

docker build -t java-spring:v5 .

Build xong các bạn nhớ test lại image v5 xem chạy oke không nhé 😉

Cuối cùng là ta check image:

docker images ---->>>
REPOSITORY TAG IMAGE ID CREATED SIZE
java-spring v5 cf660bffa5d9 16 seconds ago 248MB
java-spring v4 c2ac16deb3c4 26 minutes ago 386MB
java-spring v3 87b851cd4d65 5 hours ago 396MB
java-spring v2 f59c55951845 12 hours ago 679MB
java-spring v1 fcebd8f22de2 13 hours ago 396MB

Giảm được gần 150MB (gần 40% so với lúc đầu - 396MB)😮😮

Ngon phết hê 😘😘

Nên chọn cách nào?

Như các bạn thấy thì cách số 2 sẽ build + run project của chúng ta trên chính xác cùng 1 môi trường (2 FROM giống nhau) nhưng cách 2 thì phần test hiện tại đang failed mà ta sẽ cần phải setup thêm để nó chạy được.

Ưu điểm của cách 2 là ta có thể vác cái Dockerfile đấy đi mọi môi trường sẽ đều build và cho ra kết quả mong đợi, trong khi cách 1 yêu cầu máy gốc chúng ta phải có Java.

Mình thì hơi nghiêng về cách số 2 đấy, còn các bạn thì sao? :😃

Chạy khi dev ở local

Mặc dù project này của mình đã cài spring-devtools, nhưng để trigger restart thì ta cần phải build project thì mới có sự thay đổi về files trong classpath, cái này thường hữu ích khi ta code dùng IDE kiểu Intellij hay Android Studio, ở đó ta có thể trigger Build/Make Project trực tiếp từ IDEA và app sẽ tự restart

Do vậy mình thấy trong trường hợp dùng Docker thì có vẻ nó không thích hợp lắm cho lúc dev ở local. Tức là ở local ta cứ code trực tiếp trên môi trường gốc

Suy ngẫm một chút

Nếu các bạn để ý, xuyên suốt series, mình hay nói tới sự tiện lợi khi vừa là Software Engineer vừa làm đc DevOps.

Là người trực tiếp tạo product, code software, ta biết chính xác app của chúng ta cần những gì có thể chạy được, ví dụ:

  • Sau khi build JAR thì ta chỉ cần lấy file JAR đó để deploy thôi, những cái khác không cần nữa, vậy nên ta dùng multi-stage build
  • Khi chạy production thì hầu như ta không cần bản full JDK nữa mà dùng JRE cũng được rồi
  • .....

Nếu ta chỉ đưa project cho DevOps Engineer và hỏi họ cách Dockerize, thì có thể họ sẽ không có đủ context để đưa ra cho chúng ta giải pháp tốt nhất

Vậy nên nếu được, hãy làm chủ công việc mà ta đang làm và bớt phụ thuộc vào người khác, nếu có thể, bạn nhé 😊😊

Kết bài

Như mọi khi, hi vọng các bạn đã hiểu đc cách để dockerize project Java và áp dụng vào công việc thực tế.

Thân ái, chào tạm biệt, hẹn gặp lại các bạn ở các bài sau 👋

Bình luận

Bài viết tương tự

- vừa được xem lúc

Caching đại pháp 2: Cache thế nào cho hợp lý?

Caching rất dễ. Mình không nói đùa đâu, caching rất là dễ. Ai cũng có thể làm được chỉ sau 10 phút đọc tutorial. Nó cũng giống như việc đứa trẻ lên 3 đã có thể cầm bút để vẽ vậy.

0 0 114

- vừa được xem lúc

Caching đại pháp 1: Nấc thang lên level của developer

Bí quyết thành công trong việc đáp ứng hệ thống triệu user của những công ty lớn (và cả công ty nhỏ). Tại sao caching lại là kỹ thuật tối quan trọng để phù phép ứng dụng rùa bò của chúng ta thành siêu phẩm vạn người mê.

0 0 67

- vừa được xem lúc

Cache dữ liệu Nodejs với Redis

Một tí gọi là lý thuyết để anh em tham khảo. Cache là gì. Lợi ích của việc cache data. .

0 0 97

- vừa được xem lúc

Nguyên tắc hoạt động của redis server

Sự ra đời của Redis. . Câu chuyện bắt đầu khi tác giả của Redis, Salvatore Sanfilippo. (nickname: antirez), cố gắng làm những công việc gần như là không.

0 0 81

- vừa được xem lúc

Viết ứng dụng chat realtime với Laravel, VueJS, Redis và Socket.IO, Laravel Echo

Xin chào tất cả các bạn, đây là một trong những bài post đầu tiên của mình. Sau bao năm toàn đi đọc các blog tích luỹ được chút kiến thức của các cao nhân trên mạng.

0 0 905

- vừa được xem lúc

Tìm hiểu tổng quan về Redis

1. Lời mở đầu.

0 0 352