tags: docker

Docker Note

開始教學:
安裝好docker後,打開終端機跑下面這段code
docker run -d -p 80:80 docker/getting-started
針對flag做說明:

  • -d: detached mode將container在背景中執行
  • -p 80:80: 將主機的80port對照到container的80port,如果你的主機上有其他app運行在80port上你可以改為其他port例如3000:80,運行成功後你可以打開http://localhost:3000來看看結果。
  • docker/getting-started: 要使用的image檔案名稱

你也可以使用簡寫的方式來縮短code,例如:
docker run -dp 80:80 docker/getting-started

The Docker Dashboard

在進到下一階段前,你可以先打開Docker的dashboard,裡面可以讓你清楚的看到有哪些containers在你的主機上運行,也讓你快速查閱logs還有輕鬆地調整container的lifecycle。
打開dashboard你應該會看到,這個課程正在運行中,而名字會是隨機的。

What is a container?

簡單來說,container就是在你主機上運行的一個程式是與其他程式分別隔離開的,這類的隔離方式是利用了Linux長久以來有的特色kernel namespaces and cgroups,Docker就是讓此特色功能變得簡單易用。

Waht is a container image?

當運行一個container時會使用獨立的檔案系統,這獨立的檔案系統是由container image提供的,因為image包含了container的檔案系統,他必定會包含運作一個程式需要的所有檔案(all dependencies, configuration, scripts, binaries, etc.),image也會包含其他給container的configuration例如環境變數、運行程式的預設指令以及其他的metadata。

Containerize an application

接下來的課程會使用一個nodejs環境寫的簡易todo list來做示範,可以到github clone這個專案,並將裡面的app資料夾拉出來做練習。

Build this app's container image

首先建立一個Dockerfile的檔案,Dockerfile只有文字沒有副檔名。它包含了用來建立image的步驟指令。

  1. 在剛剛的app資料夾內建立一個dockerfile
touch Dockerfile
  1. 並將下面的內容加入到dockerfile內,如果你在之前就寫過dockerfile,你可能會覺得這內容有點缺陷,但別擔心,會續做再多做說明。
# syntax=docker/dockerfile:1 FROM node:12-alpine RUN apk add --no-cache python2 g++ make WORKDIR /app COPY . . RUN yarn install --production CMD ["node", "src/index.js"] EXPOSE 3000
  1. 打開終端機並進到app資料夾內,現在使用指令docker build來建立container image。
docker build -t getting-started .

你會注意到過程下載了許多"layers",這是因為我們指示builder要有node:12-alpine但因為自身主機上沒有這個image所以會需要先下載。

在下載完畢後會將其複製到程式內並再使用yarn來安裝程式的分支,CMD這個指令直接設定了一個在當從此image運行container時要運行的預設指令。

-t這個標籤會將image下標註,簡單來想就是命名一個人們易讀的image名稱,並可以用這個名稱來運作一個container。

還有一個最後的.告訴了docker build指令要去尋找當前資料夾的Dockerfile

Start an app container

現在有了image,就可以來運行我們的程式囉,我們使用docker run指令。

  1. 藉由docker run指令來將剛剛建立的image來運行起來:
docker run -dp 3000:3000 getting-started
  1. 幾秒後,打開你的瀏覽器輸入http://localhost:3000,就能看到app囉。
  2. 可以進去嘗試使用看看這個簡易的todo app,看是不是都正常。

接下來打開dashboard,你應該可以看到有兩個container運行在上面。

Update the application

Update the source code

  1. src/static/js/app.js第56行替換新的文字
- <p className="text-center">No items yet! Add one above!</p> + <p className="text-center">You have no todo items yet! Add one above!</p>
  1. 來更新image的版本,使用跟先前一項的指令
docker build -t getting-started .
  1. 啟動一個新的container
docker run -dp 3000:3000 getting-started

你可能會看到以下的錯誤訊息:

docker: Error response from daemon: driver failed programming external connectivity on endpoint determined_wilson (a14b6b63f2b3d89ba8b3eb0e8301ee0bb59a27bb74740218a45ae58c0f76b0c0): Bind for 0.0.0.0:3000 failed: port is already allocated.

怎麼回事呢? 我們無法使用新的container因為舊的container依舊在運行中使用的是3000 port,且一個特定的port只能有一個特定的程序運行在上面,為解決此,我們可以移除舊的container。

Replace the old container

要移除container首先要把其暫停(終止),後在移除,以下有兩種方式可以做到,請自由選擇想要的方式進行。

Remove a container using the CLI

  1. 拿到container的id
docker ps
  1. 使用docker stop來停止container
docker stop <container-id>
  1. 一但暫停後你可以使用docker rm來將其移除
docker rm <container-id>

你可以同時暫停和移除container藉由加上 -f標籤

docker rm -f <the-container-id>

Remove a container using the Docker Dashboard

直接打開dashboard,點擊container右邊的垃圾桶按鈕並確認。

Start the updated app container

  1. 現在再一次啟動你更新過後的app
docker run -dp 3000:3000 getting-started
  1. 重新整理你的畫面就會看到新的字句被更新上去了

在更新後你可能會注意到下列兩件事:

  • 所有在原本存在todo list的資料都消失了,這並不好,後面會繼續說道。
  • 為了一個小更新需要經過這麼多步驟,你後續會學習到如何不用每次都重建以及重啟當你需要做檔案更動時。

Share the applcation

先前已建立了一個image,你需要使用Docker registry來建立,預設的registry是Docker Hub且是所有目前使用過的image的來源處。

Create a repo

  1. 建立並登入Docker Hub
  2. 點擊Create Repository
  3. repo名稱請使用getting-started.確保為公開Public
  4. 點擊Create

建立完畢後會看到頁面中如下圖有一組command,這是可以用來push到此repo的指令:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Push the image

  1. 先試著執行那條指令,請自行替換自己的命名
docker push louischen1995/getting-started The push refers to repository [docker.io/louischen1995/getting-started] An image does not exist locally with the tag: louischen1995/getting-started

為何失敗?這條指令會去尋找一個名稱為louischen1995/getting-started的image,可以使用docker image ls,你也不會看到有這個image。
為解決這個問題,我們需要將原有的image加上tag,來賦予其他名稱。

  1. 登入Docker Hub並使用以下指令docker login -u YOUR-USER-NAME
  2. 使用docker tag指令給予getting-startedimage一個新名字,例如:docker tag getting-started louischen1995/getting-started
  3. 再push一次,你可以下tagname,如果沒有設定就會是預設的latest

Persist the DB

之前的todo list app再重啟container後每次都會清空資料,為何會這樣?以下說明。

The container's filesystem

當一個container在運行時,他會使用來自image內不同層來作為其檔案系統,各個container也都有自己的暫存空間用來建立/更新/刪除檔案,任何更動都不會影響到其他的container甚至就算他們是使用同一個image。

See this in pracitce

To see this in action, we’re going to start two containers and create a file in each. What you’ll see is that the files created in one container aren’t available in another.

  1. Start an ubuntu container that will create a file named /data.txt with a random number between 1 and 10000.
 docker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null"

In case you’re curious about the command, we’re starting a bash shell and invoking two commands (why we have the &&). The first portion picks a single random number and writes it to /data.txt. The second command is simply watching a file to keep the container running.

  1. Validate that we can see the output by execing into the container. To do so, open the Dashboard and click the first action of the container that is running the ubuntu image.

Dashboard open CLI into ubuntu container

You will see a terminal that is running a shell in the ubuntu container. Run the following command to see the content of the /data.txt file. Close this terminal afterwards again.

cat /data.txt

If you prefer the command line you can use the docker exec command to do the same. You need to get the container’s ID (use docker ps to get it) and get the content with the following command.

docker exec <container-id> cat /data.txt

You should see a random number!

Now, let’s start another ubuntu container (the same image) and we’ll see we don’t have the same file.

docker run -it ubuntu ls /
And look! There’s no data.txt file there! That’s because it was written to the scratch space for only the first container.

Go ahead and remove the first container using the docker rm -f <container-id> command

Container volumes

根據前面的測試,我們知道各個container間的資料是不會互相影響的且重建重啟後資料也不會被保存,但有了volume,就可以改變這一切。
Volumes提供了container一個特定存儲到主機的檔案系統路徑,如果如果container內的路徑被更動過後,在主機上也會看到更動的情況,如果在container重啟時給於相同的volumes 路徑,也會看到相同的資料。

有兩種主要使用volume的方式,兩種方式都會說,以下先使用named volumes,

Persist the todo data

預設中todo app會將資料存在SQLite Database在/etc/todos/todo.db這是在container的檔案系統中,如果對於SQLite不熟悉也不用擔心,他只是個關聯式資料庫,而container會將所有資料存在單一一個檔案中,而這對大規模的app並不是個好選項,待會會提到如何些換不同的資料庫引擎。

隨著資料庫為單一檔案時,如果我們可以保存這個檔案在主機上並讓下一個container所使用,它也應該可以接續先從執行過的動作,藉由建立一組volume並將其安裝在檔案儲存的路徑,我們就可以永久儲存資料。

如前面所說,我們會使用一組named volume,簡單想像named volume就是一個籃子裝著所有資料,docker會將其存在主機硬碟上而你只需要記住此volume的名稱。

  1. 藉由docker volume create指令來建立一組volume
docker volume create todo-db
  1. 停止並刪除先前的container docker rm -f <id>
  2. 啟用todo app的container,但加上-v這標籤來指定安裝volume,使用named volume(todo-db),並將其放到/etc/todos,這會抓取路徑上所有建立的檔案。
docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started
  1. 啟用後打開todo app並隨意加上幾筆資料。
  2. 再一次停止並刪除先前的container docker rm -f <id>
  3. 在啟用container,和先前指令一樣。
  4. 打開todo app後會看到你剛剛新增的資料,確認docker已經有了保存資料的功能。
  5. 再一次停止並刪除。

Use bind mounts

在前一章節中,我們講到使用name volumes來保存資料,如果只有單純存入資料和不用管存在哪個位置的話Name volumes是個好選擇。

有了bind mounts,我們可以準確控制主機上的存入位置,這可以被用來存資料,但比較常被用來存提供給container的額外資料,當在開發一個程式時,我們可以使用bind mount方式,來將code存入給container讓其觀察有何發生改變。

Quick volume type comparisons

Bind mounts 和 named volumes是Docker engine中兩種主要的volume,然而也有其餘的volume drive供其他使用模式(SFTP, Ceph, NetApp, S3, and more)

Named Volumes Bind Mounts
Host Location Docker chooses You control
Populates new volume with container contents my-volume:/usr/local/data /path/to/data:/usr/local/data
Mount Example (using -v) Yes No
Supports Volume Drivers Yes No

Start a dev-mode container

要使container支援開發者工作模式,需要做以下幾項設定

  • 將程式碼掛載到container
  • 安裝所有的dependencies包含"dev" dependencies
  • 開啟nodeman 並監看所有檔案變動
  1. 確保沒有getting-startedcontainer在運行

  2. 執行下面的命令並看是哪種機器
    如果是x86-64 Mac or Linux

    ​​​​ docker run -dp 3000:3000 \ ​​​​ -w /app -v "$(pwd):/app" \ ​​​​ node:12-alpine \ ​​​​ sh -c "yarn install && yarn run dev"

    如果是Windows

    ​​​​ docker run -dp 3000:3000 ` ​​​​ -w /app -v "$(pwd):/app" ` ​​​​ node:12-alpine ` ​​​​ sh -c "yarn install && yarn run dev"

    如果是Apple晶片或是其他ARM64架構的

    ​​​​ docker run -dp 3000:3000 \ ​​​​ -w /app -v "$(pwd):/app" \ ​​​​ node:12-alpine \ ​​​​ sh -c "apk add --no-cache python2 g++ make && yarn install && yarn run dev"
    • -dp 3000:3000: 跟先前相同,detached mode(背景執行),以及建立port mapping
    • -w /app: 設定當前路徑的工作資料夾
    • -v "$(pwd):/app: 綁定掛載目前container的路徑到/app這資料內
    • node:12-alpine: 使用的基礎image
    • sh -c "yarn install && yarn run dev":apline 沒有bash並執行yarn install安裝所有的dependencies然後執行yarn run dev
  3. 你可以使用docker logs -f <container-id>來追蹤logs

  4. 在檔案內做變動

- {submitting ? 'Adding...' : 'Add Item'} + {submitting ? 'Adding...' : 'Add'}
  1. 過幾秒後重整頁面就會看到更新後的資料
  2. 當結束開發後,暫停此container並建立新的image
docker build -t getting-started .

Multi container apps

到目前,我們已經與單一container互動過了,但接下來要加入MySQL到開發層內,問題是,MySQL要在哪裡運作呢?放在同個container或是分開呢?基本上個別的container應該就只做一件事並將其做好而其原因有:

  • 有極大的可能你會將API的開發與前端的開發分開來放相對於資料庫來說。
  • 獨立container可以讓你更好掌握版本以及版本的更迭。
  • 雖然開發時會在本地container放資料庫,但在產品上現階段通常都是把資料庫放在其他服務上(GCP,AWS,AZURE),你不會想將你的資料庫隨著app到處移動。
  • 運行多個程序需要有程序管理,會相對增加container啟動/關閉的複雜性。
    還有其他不同理由所以我們會將app像下圖那樣進行開發。

Container networking

預設上container會獨立運作且不會知道同個機器上的其他程序、container如何運作,那這樣如何允許container與另外一個container連線呢?答案就是netwokring,你不用是個專業的IT網路工程師,你只需要記得簡單的一點

如果兩個container是在同一個network環境,他們就可以與對方溝通連線,如果不是同環境則就不行。

Start MySQL

有兩種方式將兩個container放在同一network,1) 在設定一開始就指定 2)與現有的container連線。
現在先建立一個network,且將MySQL連接上去。

  1. 建立network
    ​​​​docker network create todo-app
  2. 建立一個MySQL container並將其連接上network。 我們也會定義一些環境變數提供給資料庫初始化使用
    下面為ARM架構晶片使用
docker run -d \ --network todo-app --network-alias mysql \ --platform "linux/amd64" \ -v todo-mysql-data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=secret \ -e MYSQL_DATABASE=todos \ mysql:5.7

裡面的--network-alias待會會再說明。

你會注意到這邊使用了volume namedtodo-mysql-data並將其安裝在了/var/lib/mysql,這裡就是MySQL存資料的地方,然而我們從未執行docker volume create指令,Docker會自動識別並知道我們想要使用named volume並自動建立。

  1. 確認已經建立了資料庫且運行中,連線到資料庫確認。
    ​​​​docker exec -it <mysql-container-id> mysql -u root -p
    接下來輸入密碼secret,接下來輸入:
    ​​​​SHOW DATABASES;
    會看到:
    ​​​​ +--------------------+
    ​​​​ | Database           |
    ​​​​ +--------------------+
    ​​​​ | information_schema |
    ​​​​ | mysql              |
    ​​​​ | performance_schema |
    ​​​​ | sys                |
    ​​​​ | todos              |
    ​​​​ +--------------------+
    ​​​​ 5 rows in set (0.00 sec)
    
    然後離開 exit

Connect to MySQL

現在MySQL已經建立並且運行,但問題是如何使用呢?如果我們將另外一個container運行在同一network,它們要找到對方?(記住雙方都有自己的IP位址)
為了解決這問題,我們要使用nicolaka/netshoot container,這提供了許多工具用來有效解決且debug network的問題。

  1. 建立一個新的container基於 nicolaka/netshoot image,且確保連線到相同的network
    ​​​​docker run -it --network todo-app nicolaka/netshoot
  2. 在container內,我們要使用dig指令,這是個有用的DNS工具,我們要找到mysql的IP。
    ​​​​dig mysql
    你會得到像以下output
    ​​​​ ; <<>> DiG 9.14.1 <<>> mysql ​​​​ ;; global options: +cmd ​​​​ ;; Got answer: ​​​​ ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32162 ​​​​ ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 ​​​​ ;; QUESTION SECTION: ​​​​ ;mysql. IN A ​​​​ ;; ANSWER SECTION: ​​​​ mysql. 600 IN A 172.23.0.2 ​​​​ ;; Query time: 0 msec ​​​​ ;; SERVER: 127.0.0.11#53(127.0.0.11) ​​​​ ;; WHEN: Tue Oct 01 23:47:24 UTC 2019 ​​​​ ;; MSG SIZE rcvd: 44
    ANSWER SECTION中你會看到A紀錄顯示MySQL為172.23.0.2(請觀察自己的terminal上顯示的IP),雖然mysql不是一個有效的主機名稱,Docker 還是能夠將其解析為具有該網絡別名的容器的 IP 地址。
    意思就是我們的app只要連到主機名為mysql就可以跟資料庫互動了。

Run your app with MySQL

todo app支援設置環境變數的功能,來為MySQL提供連線設定,他們分別是:

  • MYSQL_HOST:hostname for the running MySQL server
  • MYSQL_USER - the username to use for the connection
  • MYSQL_PASSWORD - the password to use for the connection
  • MYSQL_DB - the database to use once connected
  1. 如果是MySQL 8.0或以上請確保下面指令有被加入到mysql
ALTER USER 'root' IDENTIFIED WITH mysql_native_password BY 'secret'; flush privileges;
  1. ARM架構晶片使用以下
docker run -dp 3000:3000 \ -w /app -v "$(pwd):/app" \ --network todo-app \ -e MYSQL_HOST=mysql \ -e MYSQL_USER=root \ -e MYSQL_PASSWORD=secret \ -e MYSQL_DB=todos \ node:12-alpine \ sh -c "apk add --no-cache python2 g++ make && yarn install && yarn run dev"
  1. 看一下logdocker logs <container-id>應該會出現下列
nodemon src/index.js [nodemon] 1.19.2 [nodemon] to restart at any time, enter `rs` [nodemon] watching dir(s): *.* [nodemon] starting `node src/index.js` Connected to mysql db at host mysql Listening on port 3000
  1. 在瀏覽器打開app並添加一些項目
  2. 連線到資料庫
docker exec -it <mysql-container-id> mysql -p todos
  1. 並運行下列
mysql> select * from todo_items; +--------------------------------------+------+-----------+ | id | name | completed | +--------------------------------------+------+-----------+ | 50907f37-1704-4935-84cd-15f2c0d2be14 | aaa | 0 | | 9c5fb951-2a99-42eb-8dba-3d3804dc81bd | aaa | 0 | | b72e77b8-18c3-48b9-9c19-b9978837e1e8 | ddd | 0 | | d1a81946-d848-462b-89be-6fbbd59c7f23 | ccc | 0 | +--------------------------------------+------+-----------+ 4 rows in set (0.00 sec)

再來看Docker dashboard,你會看到兩個container正在運行,但沒有明確地表明他們是同一組的,後續會講如何讓其更好。

Use Docker Compose

Docker Compose的開發是用來協助定義及分享有多個container的應用程式。有了Compose,我們可以建立一個YAML檔案且只需要一個簡單指令來定義服務,可以讓其運作或是停止。
使用Compose的其中一個好處就是你可以在一個檔案中來定義你的應用程式棧,存在project的根目錄,其他人只要clone你的repo並啟用compose app就可以對你的檔案做出貢獻,實際上,你可以看到許多在github上的專案就是這樣執行。

Install Docker Compose

如果你有Docker Desktop的話,就已經有Docker Compose了,但如果你是使用Linus的話你還要安裝Docker Compose
在安裝後你可以看一下是什麼版本

docker compose version

Create the Compose file

  1. 在app根目錄建立一個file名為docker-compose.yml
  2. 在compose檔案內我們先定義架構版本,在多數使用情況,最好是使用最新的版本
version: "3.7"
  1. 接下來定義要跑在應用程式內的服務(containers)列表
version: "3.7" services:

Define the app service

以下這是我們定義app container時的指令

docker run -dp 3000:3000 \ -w /app -v "$(pwd):/app" \ --network todo-app \ -e MYSQL_HOST=mysql \ -e MYSQL_USER=root \ -e MYSQL_PASSWORD=secret \ -e MYSQL_DB=todos \ node:12-alpine \ sh -c "yarn install && yarn run dev"
  1. 首先,定義container的服務進入點(service entry)和image,可以為service取任何名稱,這名稱會自動成為network別名,在定義MySQL服務時會使用到。
version: "3.7" services: app: image: node:12-alpine
  1. 雖然在內容順序上沒有特別規定,但通常來說你會看到command在靠近image的地方,把這個也加入到file內吧
version: "3.7" services: app: image: node:12-alpine command: sh -c "yarn install && yarn run dev"
  1. 將port定義加入到service中,這裡使用短語法但也可以使用長語法來定義不同的動作
version: "3.7" services: app: image: node:12-alpine command: sh -c "yarn install && yarn run dev" ports: - 3000:3000
  1. 下一步,把working directory(-w /app)和volume mapping(-v "$(pwd):/app")藉由working_dirvolumes加入定義。
    一個使用Docker Compose volume的好處就是可以使用相對路徑根據當前的路徑。
version: "3.7" services: app: image: node:12-alpine command: sh -c "yarn install && yarn run dev" ports: - 3000:3000 working_dir: /app volumes: - ./:/app
  1. 最後,再把環境變數藉由enviroment key加入定義。
version: "3.7" services: app: image: node:12-alpine command: sh -c "yarn install && yarn run dev" ports: - 3000:3000 working_dir: /app volumes: - ./:/app environment: MYSQL_HOST: mysql MYSQL_USER: root MYSQL_PASSWORD: secret MYSQL_DB: todos

Define the MySQL service

接下來換定義MySQL service, 下面是先前使用過指令。

docker run -d \ --network todo-app --network-alias mysql \ -v todo-mysql-data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=secret \ -e MYSQL_DATABASE=todos \ mysql:5.7
  1. 先定義一個新的serice並將其命名成mysql,他也會自動辨別為network別名,再來定義要使用的image。
version: "3.7" services: app: # The app service definition mysql: image: mysql:5.7
  1. 接下來我們要定義volume mapping,先前用docker run時,name volume會自動建立,但Compose並不會這樣做,我們需要將volume定義在top-level。
version: "3.7" services: app: # The app service definition mysql: image: mysql:5.7 volumes: - todo-mysql-data:/var/lib/mysql volumes: todo-mysql-data:
  1. 最後,將環境變數加上去
version: "3.7" services: app: # The app service definition mysql: image: mysql:5.7 volumes: - todo-mysql-data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: secret MYSQL_DATABASE: todos volumes: todo-mysql-data:

到目前,docker-compose.yml看起來應該是這樣:

version: "3.7" services: app: image: node:12-alpine command: sh -c "yarn install && yarn run dev" ports: - 3000:3000 working_dir: /app volumes: - ./:/app environment: MYSQL_HOST: mysql MYSQL_USER: root MYSQL_PASSWORD: secret MYSQL_DB: todos mysql: image: mysql:5.7 volumes: - todo-mysql-data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: secret MYSQL_DATABASE: todos volumes: todo-mysql-data:

Run the application stack

.
.
.
.
.
.
.
.
.
. TBC