Docker - SpringBoot 加 PostgreSQL 之 compose 打包教學

今天要講解的這篇文章主要是教學如何透過 Docker 執行 docker-compose 指令一次啟動多台 container。也就是同時啟動 Springboot 的 Backend Service+PostgreSQL+pgAdmin 的範例。這算是後端服務啟動的基本款,當我們要在一台主機架設後端服務的話,勢必會用到 Backend Service + Database + Database GUI,才方便我們做後續的維護。

先來解釋名詞囉!

何謂 Docker?

先來談談為什麼我覺得要使用 Docker 呢,有以下幾點:

  • 若我們要在一台完全乾淨的主機部署我們寫好的 service,首先你可能要先在這台主機裝一堆環境,例如:程式語言環境、資料庫環境、資料庫管理介面環境、一堆你的程式額外依賴的套件等等。等於說我們要先裝好這些環境,並且還要實際運行看看才知道能不能正確的運行。在這期間會一直 try and error,其實是非常耗費時間的。
  • 再來,作業系統又是一大考量點,Windows、Linux 兩者的部署方式差距很多,你的 service 所需要依賴的環境也會因應作業系統需要去做調整。再來… 同樣的作業系統又有版本的考量,這… 會忙到焦頭爛耳。
  • 假設現在做的專案是前後端分離好了,後端工程師寫好了 service,想要給前端工程師直接在主機上使用,總不可能叫前端工程師裝一堆後端 service 所需要的套件吧。等裝好還要再測試環境,等到天荒地老。

所以,綜合以上幾點,我們可以知道 Docker 最大功用,就是自動化一鍵部署!可以輕輕鬆鬆使用簡單的指令就把我們想要的服務在主機在架設起來,而採用的技術叫做 container,簡單來說就很像是輕量化的虛擬機,啟動速度比虛擬機快多了,透過在 container 啟動服務,可以跟主機環境互相連結,讓服務外連出去。

通常用 Docker 會需要寫 Dockerfile,也就是今天我們寫好了 Dockerfile,之後不管換到哪台主機,Dockerfile 的內容都能夠沿用,只要那台主機裝 Docker 環境就可以了,就能夠透過 Dockerfile 讓 Docker 自動幫我們裝好我們所需要的環境,然後運用幾個指令幫我們啟動 service。

docker-compose 指令

為什麼會需要 docker-compose?假設我要有 Backend Service + Database + Database GUI,這些服務啟動,但是一個 Dockerfile 只能做成一個 image,而我們需要這一個 image 去開起一個 container,但我們不可能把三個服務都寫在同一個 Dockerfile,因為每個 service 有它需要設定 port、啟動的指令等等,寫在一起太麻煩了,也不推薦,而且通常一個 container 就是包含一個 service,越單純越好,後續作維護也比較方便。

所以 docker-compose 可以幫助我們 build 好需要的 image,在一一開啟需要的 container,可以說是更方便的一鍵部署的方式!

以下開始示範如何操作~

先準備好 Springboot 測試專案

專案內容:連接 PostgreSQL,透過 Restful API 秀出 Table 裡面的資料。

假設專案名稱叫做 docker-demo。

  • 建立 Person 的 Class,作為 PostgreSQL 裡面的 table 叫做 Person。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    @Entity
    public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String password;
    private String email;

    public Person(String name, String password, String email) {
    this.name = name;
    this.password = password;
    this.email = email;
    }

    public Person() {
    }

    public Long getId() {
    return id;
    }

    public void setId(Long id) {
    this.id = id;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public String getPassword() {
    return password;
    }

    public void setPassword(String password) {
    this.password = password;
    }

    public String getEmail() {
    return email;
    }

    public void setEmail(String email) {
    this.email = email;
    }
    }
  • 建立 PersonRepository 的 interface,來藉此使用 JPA ORM 的框架功能。

    1
    2
    3
    4
    @Repository
    public interface PersonRepository extends JpaRepository<Person, Long> {

    }
  • 建立 Controller 的 class,建立 Restful API。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @RestController
    @RequestMapping("/api")
    public class Controller {

    @Autowired
    private PersonRepository personRepository;

    @GetMapping("/people")
    public List<Person> getAllPerson() {
    return personRepository.findAll();
    }
    }

    也就是建立 GET METHOD 的路由來取得所有 person 的資料~

  • 設定 application.properties

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
      # PostgreSQL連線設定
    spring.datasource.url=jdbc:postgresql://db:5432/postgres
    spring.datasource.username=postgres
    spring.datasource.password=root
    spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
    spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
    # 服務啟動就自動執行schema.sql
    spring.datasource.initialization-mode=always
    spring.datasource.schema=classpath:schema.sql
    spring.datasource.data=classpath:data.sql

    前半段就是連線 PostgreSQL 的設定,在這邊假設我們的 database 名稱叫做 postgres,也就是 PostgreSQL 預設就會有的資料庫名稱。

    請注意有個地方可能會覺得疑惑

    為什麼連線的 url 是採用 db:5432 的方式,而不是填 localhost,或是其他有意義的 IP。沒錯,如果你是要在自己的主機上做測試,的確是要改成 localhost,但現在不是阿,我們是要透過 docker 去幫我們建立,那這個 db 名稱怎麼來的,留在後續講 docker-compose 就知道了。

    後半段的設定就是當我們一啟動 Springboot Service,可以幫助我們自動建立資料庫表格及輸入資料進去。

  • 所以我們需要在 resources 資料下輸入新增兩個檔案,分別是 schema.sql、data.sql

    • schema.sql

      1
      2
      3
      4
      5
      6
      CREATE TABLE IF NOT EXISTS person (
      id SERIAL PRIMARY KEY,
      name varchar(11) NOT NULL ,
      password varchar(100) NOT NULL,
      email varchar(100) NOT NULL
      );
    • data.sql

      1
      2
      3
      4
      5
      INSERT INTO person (name, password, email) VALUES ('Kenny', '0000', '[email protected]');
      INSERT INTO person (name, password, email) VALUES ('Nicole', '0000', '[email protected]');
      INSERT INTO person (name, password, email) VALUES ('Jack', '0000', '[email protected]');
      INSERT INTO person (name, password, email) VALUES ('Mary', '0000', '[email protected]');
      INSERT INTO person (name, password, email) VALUES ('David', '0000', '[email protected]');
  • 設定 pom.xml

    我們需要在 properties 裡面加入這段:

    1
    2
    3
    <properties>
    <skipTests>true</skipTests>
    </properties>

    這個作用是方便當我們要利用 maven 將專案 build 成 jar 檔案的時候,跳過一些檢查,例如如果我們現在 build 的話是不會成功的,因為剛剛我們資料庫連線的 url 是寫 db:5432,但是我們還沒用 docker 所以,是會連不上資料庫的,導致 build 失敗。導致 build 失敗。因此我們需要加入這行來避免 build 失敗。

  • 接著我們就可以利用 maven 來 build 出專案的 jar 檔案,這樣我們就可以透過啟動 jar 檔案來啟動我們的 service。

    這邊建議大家採用 Intellij IDEA 來做開發測試喔,不然還需要特別去 maven 網站下載 maven 套件到主機進行 build 的操作。這邊我是直接使用 Intellij 內建 maven 套件直接幫我們 build 就可以了。

    如下圖:

點擊 package 指令即可。成功的話會得到以下畫面:

接著我們就可以在當前專案根目錄,有一個 target 目錄,點進去裡面會產生一個 docker-demo-0.0.1-SNAPSHOT.jar,名稱會依你的專案名稱而定。將這個 jar 檔案複製並放置在專案的根目錄下。

當以上的動作都搞定後,也就完成了 Springboot 測試專案的流程了。

準備 Dockerfile

接著我們需要定義 Springboot 專案的 Dockerfile,新增 Dockerfile 在專案的根目錄下,並輸入以下內容:

1
2
FROM adoptopenjdk/openjdk11:latest
COPY docker-demo-0.0.1-SNAPSHOT.jar app.jar

非常簡單,就兩個指令即可,要做更複雜的設定可以在 docker-compose 去做設定就可以了。

這邊也就是我們需要 JDK 的環境,這樣才可以執行我們的 Springboot 專案!再來將我們剛剛的 jar 檔案,複製到 docker 裡面的環境,並命名為 app.jar。詳細的 Dockerfile 指令這邊就不多講了,因為會花費許多時間,有時間再另外發 docker 的教學文。

準備 docker-compose.yml

再來,需要定義 docker-compose.yml,因為將其新增在專案的根目錄下,並輸入以下內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
version: '3'
services:
db:
container_name: 'postgres'
image: postgres
environment:
POSTGRES_PASSWORD: root
volumes:
- pgdata:/var/lib/postgresql/data/
ports:
- "5432:5432"
dbGUI:
container_name: 'pgadmin'
image: dpage/pgadmin4
environment:
PGADMIN_DEFAULT_EMAIL: "[email protected]"
PGADMIN_DEFAULT_PASSWORD: "kenny"
ports:
- "16543:80"
depends_on:
- db
web:
container_name: 'spring-docker-demo'
build: ./
image: spring-docker-jar:latest
entrypoint: ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
restart: always
ports:
- "8080:8080"
depends_on:
- db
volumes:
pgdata:

這邊的設定就是一次設定好 Backend Service、Database、Database GUI。那你可能會好奇說怎麼不用寫 Database 或 Database GUI 的 Dockerfile,是可以的,但是我認為並沒有太大的意義,因為 Database 跟 Database GUI 我們只需要 image 就可以了,並不用做其他複雜的動作,所以在 docker-compose 這邊一次設定就可以了。

首先一一來講解 services:

  • db

    這個就是我們的 PostgreSQL 的設定,可以設定之後的 container_name,還有去抓取怎樣的 image。在這邊透過 docker-compose,Docker 可以自動幫我們抓取 PostgreSQL 的 image。接著 environment,是 PostgreSQL 會吃的環境變數,最重要的就是要定義 POSTGRES_PASSWORD,當然也可以定義 POSTGRES_USER,但這邊 PostgreSQL 預設的 USER 就是 postgres,加上我們在 Springboot 專案那邊也是採用預設的設定,因此這邊只需要定義 PASSWORD 即可,那基本上就是要跟我們在 Springboot 專案那邊定義的一模一樣。而 ports,採用 5432:5432,也就是將主機的 5432 PORT MAPPING 到 container 裡面的 5432 PORT。

    在這邊相信大家也了解為什麼我在 Springboot 專案裡面連線資料庫的 url 要寫 db,沒錯那個名稱就是 service 的名稱。因為實際上 Backend Service 是在一個 container,Database 也是在一個 container,兩個 container 要互相連線的話,在這邊 docker-compose 會自動幫我們做一個 link 的動作,但也就是說 host 這個要改成 service 的 name,docker container 這邊會自動去 mapping。

  • dbGUI

    這個基本上就是,在維護的時候總不可能去下命令列慢慢看當前資料庫的資料有哪些吧,因此還是需要資料庫介面來幫我們做操作,那基本上比較特別的是它的 environment 這邊,PGADMIN_DEFAULT_EMAILPGADMIN_DEFAULT_PASSWORD 內容自己取就可以了。它跟一般主機版本的 pgAdmin 有點不一樣,我也是在 Docker 第一次用才發現的。至於它的 ports,內部是直接開放 80 PORT 的。主機的 PORT 也是自己決定就可以了。不要跟妳主機上的服務或是其他 container 上的服務衝到就好。

    在這邊要特別注意的是 depends_on

    這個功用是告訴 Docker 說,一定要 db service 先執行,才會在執行 dbGUI service。可以理解成,一定是資料庫服務先搞定,你的資料庫管理介面服務再啟動才有意義。你的資料庫管理介面服務再啟動才有意義。但是要注意的是,這個所謂的啟動 service 的順序,並不是會等到 service 完全啟動成功,在開啟下一個 service。那是需要用額外的指令,但基本上也可以不用那麼麻煩,假設第一次沒成功連線成功就有可能是這個問題,再重新啟動 conatiner 重新連線就可以了。

  • web

    這個就是我們 jar 檔案,開啟的 Backend Service。其中 build 指令就代表說,去哪裡執行我們的 Dockerfile,沒錯也就是說,它可以幫我們先自動執行 Dockerfile,取得 Backend Service 需要的環境後,在幫我們建立 container。而 entrypoint 可以想成是服務的啟動點,也就是要怎麼執行 jar 檔案。這個指令就不多加講解了。restart 基本上,就是如果有遇到錯誤,service 可以在自動重啟再去做 check 的動作。

    要注意的是,我們的 web 也是需要依賴 db 這個 service 優先開啟的。

最後沒講的 volumes 指令,這個簡單來說就是主機上的硬碟可以跟我們 container 裡面的虛擬空間互相連結,也就是說當 container 刪掉了,這個空間並不會被刪掉,會一直留著,畢竟它就在你主機上的硬碟上而已,除非你主動去刪除,那基本上這個功用就是用來保存資料庫的資料,不然每次我只是要開個 container 去做測試,或是我的 container 壞掉了要重用,那我的資料庫的資料我還要先 export 出來,最後 import 進去,實在太麻煩了,只要有個共用空間讓資料庫可以去存取你的資料就可以一直用下去了!

那基本上如上面的內容的話是寫成:

pgdata:/var/lib/postgresql/data/

冒號後面的路徑代表的是 container 內部存放路徑,那基本上每個資料庫都會有預設的 data 的位置,PostgreSQL 就用上述路徑就可以,而冒號前面的路徑名稱,就是我們自己自訂了。那可能會好奇究竟這個 pgdata 路徑在哪裡呢?這之後我們啟動 container 後就會知道了。

運行 docker-compose 指令

好的,當以上手續都準備好後,就能運行 docker-compose 指令。至於你的主機如何安裝 docker 那些等設定這邊就不說了,之後會各發一篇是 Windows10 安裝 Docker 教學及 Ubuntu 安裝 Docker 教學,敬請期待。

那我這邊是用 windows 的環境去運行 Docker 指令。

在專案的根目錄下,執行以下指令:

1
docker-compose up -d

也就是叫 Docker 去執行你的 docker-compose.yml 並且,啟動多台 container 在背景執行!完成一鍵部署!

要注意的是,如果你的 docker 裡面沒有這些 image 檔案,那麼部署起來會需要一些時間的。

部署成功後,基本上會得到以下訊息:

接著我們運行以下 docker 指令,確認是否三個 container 服務有沒有啟動成功:

1
docker ps -a

如果成功的話,應該會得到以下的畫面:

就是這麼神奇~輕輕鬆鬆,不需要在主機上灌一堆環境,就這樣部署完成你的後端所有服務!

接著要知道有沒有成功呢,很簡單,還記得我們專案是有個 RestfulAPI,我們來連線看看!

在主機上輸入以下網址:

localhost:8080/api/people

會得到以下內容:

沒錯,這就是你資料庫裡面的內容,有沒有~

你可能還是不相信就這麼簡單就部署起來,那我們去資料庫介面看資料庫到底有沒有資料~

輸入以下網址:

localhost:16543

會得到以下畫面:

並輸入你的設定好的帳號密碼進行登入即可。

你會覺得怎麼是空空如也,別急,我們需要將 pgAdmin 去連線我們的資料庫才行啊!

主機名稱就是 service 名稱,也就是 db。其他資訊就是按照我們在 docker-compose.yml 怎麼定義的,就怎麼填進去就可以了。點擊右下角的保存,即可連線資料庫!

沒錯就是如上圖所示,你發現 person table 裡面真的有資料~

以上就是 SpringBoot 加 PostgreSQL 之 compose 打包教學囉!

接著我們來進行一個小實驗,來驗證說究竟 volume 這個指令有沒有用處,是不是我移除了 container 那我的資料庫資料也就不見了呢?執行以下指令:

1
docker-compose down

這個指令也是非常方便,一鍵將我們三個 container 一次移除乾淨,但是 image 是會保留的喔。

接著我們在運行一鍵部署的指令:

1
docker-compose up -d

我們在去看資料庫的資料,理論上,原本有五筆資料,只要我們 volume 沒被刪掉的話,那我們理論上啟動 Backend Service 的時候會自動塞入五筆,那所以現在 person table 資料應該要有十筆喔!

沒看錯,就是十筆,也就是我們這邊就可以知道 volume 的好處,只要我們移除的 container 的時候不移除 volume,那麼這個路徑的資料就會一直保存著。所以今天當我的資料庫服務掛掉了怎麼辦?我需要把 container 移除再重新建立,再也不用擔心資料會不見囉。

最後如果想要在移除所有 container 順便移除 volume 的話,執行以下指令即可:

1
docker-compose down --volume

那麼當你在重新重啟 container 後,你會發現資料庫只會有五筆資料而已。

總結

以上就是所有 SpringBoot 加 PostgreSQL 之 compose 打包教學,當然撰寫 Dockerfile 及 docker-compose.yml 還有很多指令及細節去一一注意,有機會再發專屬 Docker 新手教學文章。畢竟這東西,如果一段時間沒寫,要查指令也是很麻煩的!

所有細節的 source code 可以在我的 Github 參考:https://github.com/KennyChenFight/spring-docker-demo

最後最後!請聽我一言!

如果你還沒有註冊 Like Coin,你可以在文章最下方看到 Like 的按鈕,點下去後即可申請帳號,透過申請帳號後可以幫我的文章按下 Like,而 Like 最多可以點五次,而你不用付出任何一塊錢,就能給我寫這篇文章的最大的回饋!