選單
GSS 技術部落格
在這個園地裡我們將從技術、專案管理、客戶對談面和大家分享我們多年的經驗,希望大家不管是喜歡或是有意見,都可以回饋給我們,讓我們有機會和大家對話並一起成長!
若有任何問題請來信:gss_crm@gss.com.tw
9 分鐘閱讀時間 (1726 個字)

Docker簡介

front-view-mockup-of-macbook-pro-in-the-coffee
tags: Docker CentOS

Docker簡介

一、前言

在看書之前,我完全不知道Docker是什麼,
只知道它的Logo是一隻可愛的鯨魚,但Docker究竟是什麼呢?

Docker其實是一個利用Container執行Application的工具,
Container裡包含了所有相依的套件、類別庫…等等,
在傳統上要分開分別以繁瑣的流程打包或建置環境的一切,
都由Docker的Container全部包起來跑在Docker Engine上,
這樣比較不會有可是在我的電腦上跑得起來啊的問題。

會用Container這個詞跟海運用到的貨櫃其實是有一點典故的。
很久以前海運的貨物都是分開打包再搬上船的,
但不同的貨物會因為性質不同有不同的打包、移動方式,
讓海運人員搞得焦頭爛額,直到有一天有了貨櫃的發明改善了這一切。
貨物可以統一以計算過的方式置放在貨櫃內,
移動的單位由散裝的貨物變成一體成型的貨櫃,方便許多。
Docker也是引用了相似的概念把所有相依的部件與應用程式打包在一起透過Container達成。

簡單說,Docker Container有幾個特性:

  1. Container跟Host共享資源。
  2. Container可攜性很高。
  3. Container輕量,可以同時跑很多Container。
  4. 下個指令跑Container就好了,不用花時間配置環境,安裝軟體…等等。

二、環境配置

在安裝之前,要特別聲明筆者是用Linux安裝Docker的,
因為Windows版的Docker要求要Windows 10 Pro才有的Hyper-V,
否則就要透過VirtualBox來執行(但我試了無數次都會失敗)

作業的境大致如下:

OS : CentOS 7 64-bit
RAM: 4G
CPU: 2-Core
HDD: 32GB

由於Docker只支援kernel版本3.10以上的Linux,
先用uname -a檢查一下是x64且3.10以上,如下圖:


三、安裝

安裝的部份就是一些指令而已,依官方的文件有三個步驟:

  1. 刪除舊版(如果有的話):
    yum remove docker \
               docker-client \
               docker-client-latest \
               docker-common \
               docker-latest \
               docker-latest-logrotate \
               docker-logrotate \
               docker-engine
  2. 設定repository:
    yum install -y yum-utils \
      device-mapper-persistent-data \
      lvm2
    yum-config-manager \
        --add-repo \
        https://download.docker.com/linux/centos/docker-ce.repo
  3. 安裝:
    yum install docker-ce

但其實我自己的經驗還要再加一個步驟才會正常運作,
就是將docker加到systemd裡:

 

systemctl daemon-reload
systemctl enable docker
systemctl restart docker

 

這個時候輸入指令docker version會出現以下畫面:


請留意,一定要上下都有才是對的喔!
否則所有的基本操作都不會動的。

四、基本操作

大家動起來 -> docker run(以hello world為例)

不管到什麼地方,身為一位專業的工程師,Hello World是一定要會寫的,
同理,Docker的Hello World要怎麼樣執行呢?

雖然不到一鍵完成,但一行指令是OK的:

docker run hello-world

 

如上圖,有兩個重點:
  1. 黃框的部份是告知目前沒有hello-world:latest這個image,
    接著下一行就把這個image拉下來了。
  2. 紅框的部份 -> 執行的結果。

說文解字 -> Image?

ImageDocker來說就是一個類似藍圖的東西,
Docker Engine會透過Image裡的描述建立一個Container
Container就是一個完整的可以跑的應用程式了。

以上面的例子來說hello-world:latest就是image,
而冒號後面接的是它的Tag,latest代表最新的版本,
當然是可以用什麼6.6.1之類的來取代,看建立Image的人如何定義的。

小結:Docker透過Docker Engine將Image轉換成Container後執行。

查詢容器 -> docker ps

好,那docker run hello-world之後,我們能看到什麼關於它的資訊嗎?
答案就在docker ps這個指令裡,實際輸入之後:


咦?怎麼一片空白?
別急,docker ps還有一個參數-a,可以列出完整的清單:


如上圖,我們可以看到剛剛的docker run建出來的hello-world Container:

  1. Container ID -> 就是一個識表性的ID值。
  2. Image -> 這個Container是由哪個Image建起的。
  3. Command -> 這個Container內部執行了什麼指令。
  4. Created -> 什麼時間點建立這個Container
  5. Status -> Container目前的狀態,以此例是Exited,代表Container已經關閉了。
  6. Ports -> 這個Container相關的網路連接埠,hello-world只是單純輸出訊息所有沒有。
  7. Names -> 這是一個可以設定的Container的代稱,其他的docker指令可以用這個代稱操作。

查詢映像 -> docker images

既然可以看到container的資訊,是不是也能看到image的資訊呢?
答案是肯定的,透過docker images,可以看到目前的docker engine下已經存在什麼image:

如上圖,除了repository之外大概都很淺顯易懂,針對repository有幾個重點:

  1. 如果是ooo/image-name的(e.g. gc/tomcat-demo),代表是個人建立的image。
  2. 如果是library/image-name或是直接image-name的(e.g. tomcat),代表是docker官方認證過的image。
  3. 如果是domain-name/image-name的,代表是非官方的企業釋出的image(e.g. docker.elastic.co/logstash/logstash)

去去容器走 -> docker rm

目前我們已經知道如何利用docker run讓hello-world跑起來,
那是不是如果再也用不到這個Container可以把它移除呢?

利用docker rm就可以達到這個效果,如下圖:


透過docker rm hardcore_wu我們移除了這個container,
再透過docker ps -a檢查一下,真的被刪除了!
(p.s. docker rm也可以依container id作用)

刪除映象 -> docker rmi

同理,image也是可以刪除的!


如上圖,有兩個重點:

  1. 跟docker rm不一樣,docker rmi只能指定Image ID。
  2. Image ID可以只提供最前面的部份,但要小心只要符合的都會被移除(e.g.圖中ID只輸入了fc)。

完整範例(1) -> 利用JDBC連線至postgresql docker container

首先要在Docker建立一個postgresql的container:

 

docker run --name db -e POSTGRES_PASSWORD=letmein -d -p 5432:5432 postgres

 

這段指令有幾個重點:

  1. –name -> 在建立Container的時候給它一個名字,在此例是db
  2. -e -> 設定Container要用到的環境變數,在此是設定postgresql預設的密碼為letmein
  3. -d -> daemon mode,代表這個Container在背景執行。
  4. -p 5432:5432 -> 這是把host的5432 port對上Container的5432 port。

接著我們下指令看一下是不是服務真的有起來了:

 

docker ps -a

 


如上圖,Up 2 seconds告訴我們服務起來了。
接著我們要透過postgresql的cli建立一個table,
但在那之前我們要先連線到這個Container的bash,
才能執行命令列的指令:

 

docker exec -it db /bin/bash

 


如上圖,有3個重點:

  1. exec -> 針對Container執行某個命令,在此例是開啟bash。
  2. -i -> 這個選項是interactive的縮寫,代表可以跟Container互動。
  3. -t -> ttl的縮寫,是互動的介面。

接著我們可以登入postgresql,並建立一個table叫account:


如上圖,其實是postgresql的CLI操作,不在此多作說明。

建立好table之後我們來寫一段Java程式透過JDBC連線到postgre這個預設的Database,
並對account這個table下一個INSERT的指令:

 

public class JDBCConnectionTest {
public static void main(String[] args) {
JDBCConnectionTest.testPostgreSQLConnection();
}
private static boolean testPostgreSQLConnection() {
try {
final String url = "jdbc:postgresql://192.168.11.86:5432/postgres";
final String user = "postgres";
final String password = "letmein";
Connection conn = DriverManager.getConnection(url, user, password);
if(conn == null)
return false;
final String sql = "INSERT INTO account(username, password, email) VALUES(?,?,?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "Tom");
pstmt.setString(2, "sunshine");
pstmt.setString(3,  "test@gmail.com");
int updated = pstmt.executeUpdate();
if(updated > 0) {
System.err.println("Inserted 1 record");
}
else {
System.err.println("Nothing inserted");
}
}
catch(SQLException ex) {
System.err.println("Cannot establish connection!");
System.err.println(ex);
return false;
}
System.err.println("Connection to PostgreSQL Server established!");
return true;
}

}

執行結果如下:

再來我們回到postgresql的cli下SELECT看看是不是真的有插了一筆記錄:

如上圖,到此看起來JDBC的程式是有連通並成功插入一筆記錄的!

睡吧 -> docker stop && 醒來吧 -> docker start

如果要暫時關閉這個postgresql的Container,
只要下docker stop db 就可以了,如下圖:

有幾個重點:

  1. 這裡是利用前面–name指定的名字停止容器的,也可以用Container ID。
  2. 如果要再重啟這個Container,可以利用docker start db再重新將它打開,所有本來在裡面的資料都還會在的,直到這個Container被刪除為止。
  3. 如果要直接重啟的話也可以透過docker restart db達到這個效果。

咦?檔案呢? -> docker volume

Docker預設是透過一種叫作Union File System的機制在存放資料的,
透過這個機制,多個Container間若有重覆性的資料則不會重新下載,
而各自Container保有它獨特的一個區塊供自己作Read/Write。

然而隨著docker rm的指令執行,那個Read/Write區塊也會消失不見,
這對於需要保存已經寫進Container資訊的人來說是一個困擾,
還好還好,有docker volume這個指令可以將檔案放在Host,
以下我們將透過一個簡單的例子介紹如何利用上述的指令保存檔案:

首先,我們透過docker run的選項-v建立一個debian的container:

 

docker run -it -v $(pwd):/home/inside debian:latest /bin/bash

 


如上圖,有幾個重點:

  1. pwd 是Linux bash的指令,是Print Working Directory 的意思
  2. -v 後面接的指令由「:」區隔,前半部是host的路徑,後半部是Container的路徑,
    意思就是說,如果Container把檔案放到/home/inside,其實最後檔案的目的地是目前路徑,
    也就是說是幫host的pwd取了個別名叫/home/inside。
  3. 指令的最後接了一個/bin/bash,這是debian這個image當初在建立時有開放收的參數之一,
    代表開啟Container後我要執行/bin/bash,且搭配著-it這個選項,
    結果就會如畫面呈現的一樣,Container載入後直接進入其bash command。

接著我們建立一個叫hello.txt的文字檔,透過vi editor在裡面輸入Hello World
最後再將這個Container移除:

如上圖,各位可以發現下docker ps -a的時候debian已經狀態是Exited
這是因為我們透過exit指令離開debian bash的時候,Container就離開主程序,
此時這個Container就會被停止,這一點要特別留意思一下,
如果執行的Container因為某種原因開啟沒多久狀態就變成Exited,
也許就是有在某個地方發生什麼錯誤造成Container離開主程序了。

接著我們再檢查看看檔案是不是真的有被保留下來了:


如上圖,檔案確實被保留下來了,內容也是當初在Container內加入的Hello World
而且原本的debian Container也透過docker ps -a確認早已不存在!

雖然上例透過-v這個選項達到了效果,但其實官方並不建議這個解法,
因為畢竟是寫死了host的路徑,所以推薦的方式是建立docker volume,
如下幾張圖以同樣的例子示範:

首先利用docker volume create outside建立一個名稱為outside的volume,
接著透過docker volume ls列出目前docker engine內的volume清單檢查是否真已建立,
最後再用docker inspect outside觀看outside的相關資訊。

再來!

這裡跟前面的例子有一點小差異,就是把$(pwd)改成outside了,
所以這個debian會將檔案存到outside這個volume對應到的真實路徑,
而真實路徑在前面的步驟有看到,就是/var/lib/docker/volumes/outside/_data
實際檢查一下,裡面真的有這個檔案呢!

小結: 第一種方法叫bind mount,第二種叫named volume
官方建立使用第二種是因為這樣檔案會是統一由docker管理,
如果自己bind mount host的路徑的話,deploy到別台機器還要手動維持同樣的架構。

五、建立Image

前面講了這麼多,好像都在用別人建好的Image?
是時候該自己手動建一個來玩玩了!

自己的映像自己弄 -> docker build

假設今天我有一個hello.war,是要部署在tomcat上,
而且唯一的一個功能就是在畫面上輸出Hello World的這麼個簡單的小系統,
我該如何把它建置成一個image並放到docker hub上呢?

首先我們要建立的是Dockerfile,一個描述建置步驟的檔案,
目前切出的檔案架構如下圖:


其中/app/demo.war就是我們的小系統,
而Dockerfile的內容如下:

 

FROM tomcat:9.0-jre8-alphine
COPY ./app/demo.war /usr/local/tomcat/webapps/demo.war

雖然內容相當的簡單,但就目前的例子十分足夠了,有兩個重點:
  1. FROM -> 描述的我們的Image是基於什麼Image,以此例是tomact。
  2. COPY -> 描述要複製的檔案,通常是要執行的程式或是一些相關的設定檔,
    以此例來說是將/app/demo.war搬到產出的Container的/usr/local/tomcat/webapps/demo.war,這樣Container啟動時就會自動部署demo.war並啟動小系統。

接著就要建置映象了,指令:

 

docker build -t dreamfulfil/tomcat-demo .

 


如上圖,有幾個重點:

  1. -t -> 代表這個Image的標籤是什麼。
  2. tomcat-demo後面有一個小點.,給的是Dockerfile所在的位置,
    因為Dockerfile是目前的工作目錄,所以利用.就可以告知這個訊息。
  3. 執行的STEP如同我們在Dockerfile裡所定義的,它先去pull tomcat相關的所有image,
    接著將demo.war複製到指到的路徑,最後告知成功build的訊息。
  4. 因為我們的 -t沒有給版號,所以預設它會給的是latest。

建置完之後我們可以利用docker images在本機的Docker machine裡看到這個image:

而且還可以執行呢docker run呢!

如上圖,利用docker run啟動Container後,利用docker ps確定正常執行了,
接著我們就可以利用瀏覽器檢查這個Container是不是真的能提供小系統的服務:


事實證明是可行的!

最後就是docker push將image丟到docker hub:

如上圖,有兩個重點:

  1. 要登入,所以要先去docker hub註冊一個帳號,push的時候image name前半部要符合你註冊的帳號名稱(e.g.我的帳號是dreamfulfil)。
  2. docker push之後可以在docker hub上面看到結果。

取後我們試著把本機的image透過docker rmi移除後,
再重新利用docker run執行看看:

如上圖,docker run發現本機端沒有image了,
就會去docker hub上面把image拉下來(不管有沒有登入),
同時我們也利用curl確定了拉下來的東西是可以執行的。

參數深入說明

待補…

六、連接Container

有沒有想過Container間彼此是可以溝通的呢?
透過docker network就可以達到這樣的功能了,
預設它是透過bridge的方式互通有無,詳細請見這裡

情境: 原本的Hello World小系統,要改成從postgresql DB撈出資訊,
不再只是單純的輸出,而且兩個都要用Docker Container的方式啟動,該如何做呢?

首先我們利用以下語法建立一個network:

 

docker network create demo

 

這會建立一個network名稱叫demo,
只要執行docker run的時候有加上設定要使用這個network的Container,
都會被加到同一個network裡面,而預設該Container的domain就是它的–name屬性值:

docker run --net demo --name mydb -p 5432:5432 -e POSTGRES_PASSWORD=letmein -d postgres
docker run --net demo --name webapp -p 8080:8080 -d dreamfulfil/tomcat-demo

如上圖,可以正確的讀到所有的accounts。
特別留意程式的部份jdbc的url可以寫成如下:

final String url = "jdbc:postgresql://mydb:5432/postgres";

 

mydb的部份就是啟動postgresql Container時所給定的–name值。

七、結語

微服務是近幾年非常夯的話題,Docker也只是其中一個小環節而已,
希望本篇的介紹能稍微給各位帶來一些基本的概念。

微信訊息的 Reply 與 Push
ASP .NET部署方式比較,如何透過Web Deploy部署至IIS Server

相關文章