用 Docker 部署 Rails,原來是這樣!?

圖片作者:Julius Silver, 圖片來源連結

如果你曾經看過 Docker 相關的文章,對 Docker 有點認識但又不是太熟,需要找個主題來實作看看,那這篇文章就是為你準備的!我們將透過 Docker 部署一個 Rails 應用到 DigitalOcean 平台上,本文會一步一步介紹部署的過程。


目錄

  • Docker 基本介紹
    • Docker 三大元素
    • 設定檔
    • 常用指令
  • 實戰演練
    • 步驟0: 簡單開一個 Rails 專案
    • 步驟1: 開始加入 Docker
    • 步驟2: Nginx 搭配 Puma
    • 步驟3: 部署到雲端上吧!
    • 步驟4: HTTPS 認證
    • 步驟5: Docker image 瘦身
  • 結論

Docker 基本介紹

在程式開發的過程中,常常會發生環境不同或是系統設定有誤的情況,這樣的情況如果在本機重現不了 bug 真的很讓人頭痛,如果有一種技術可以讓我們本機的環境跟測試站、正式站一樣或是只要更接近一點,一定可以省下不少時間。於是 Docker 出現了,Docker 是現在當紅的虛擬化技術,它非常的輕量,啟動、停止是可以在幾秒內完成的那種,同時 Docker 對系統資源需求很少,甚至一台主機可以同時啟動數百甚至數千個 Docker 容器。為了要學習和體驗一下 Docker 的威力,所以我試著用 Docker 來部署我的 Rails 應用程式,這是個很好的機會讓我對不管是 Docker 技術還是環境部署都有更深一層的認識!

礙於篇幅這邊只會簡要說明實作部分可能用到的名詞,更詳細的資料可能需要再 google 一下了

Docker 三大元素

  • Docker Image
Docker 映像檔,有點像虛擬機的映像檔,是一個能產生 Docker Container 的模板
之後文章中都簡寫成 Image
  • Docker Container
Docker 容器,就是用它來運行和隔離環境,它有點像簡易版的 Linux 環境
我們可以用 Docker Image 來建立 Docker Container,也能將它停止刪除等等
之後文章中都簡寫成 Container
  • Docker Registry
Docker 倉庫註冊伺服器,可以把我們的 Image 放上去
也能在其他機器把這些 Docker Image 下載下來
Docker Hub 就是 Docker 倉庫註冊伺服器的代表

設定檔

  • Dockerfile
可以設定自己客製化的 Docker Image
  • docker-compose.yml
docker-compose.yml 是個可以同時管理,啟動多個 Container 的工具

常用指令

  • docker images
列出目前機器上有的 Image
  • docker ps
列出目前機器上有運作中的 Container
  • docker exec -it <Container ID> bash
進入指定的 Container
-i 參數是本機電腦的鍵盤接到 Container 的 stdin
-t 參數是 Container 的 stdout 接到本機電腦的螢幕
  • docker run -it <Image ID> bash
用 Image 創建一個 Container 然後運行
  • docker rmi <Image ID>
刪除指定的 Image
  • docker rm <Container ID>
刪除指定的 Container
  • docker build .
依據 Dockerfile 的設定建立 Image
  • docker push NAME[:TAG]
上傳 Image 到 Docker Registry
  • docker pull NAME[:TAG]
從 Docker Registry 下載 Image 到本機
  • docker-compose up
啟動 docker-compose.yml 的設定,有 Container 就會啟動
沒 Container 也會從指定的 Image 生成

實戰演練

看完 Docker 基本介紹可能有種似懂非懂的感覺,所以我們就來操作一下吧!這單元除了會用到上述 Docker 相關的知識外,也會需要一些部署的觀念,可能可以參考這篇文章

步驟 0: 簡單開一個 Rails 專案

從終端機輸入指令建立專案吧!

rails new docker_study

接著新增 gem 'pg' 以及更新對應的 config/database.yml

完成後再用 scaffold 產生基本框架

rails generate scaffold post title:string body:text

如果想要畫面好看一點點可以加上 bootstrap 請參考: https://github.com/twbs/bootstrap-sass

就先這樣,完成一個最簡單只有 CRUD 網站

步驟 1: 開始加入 Docker

目標: 在本機使用 docker-compose up 啟動多個 Container 瀏覽器輸入 http://127.0.0.1:3000 可以開啟專案

我們先試著在本機的環境使用 Dokcer,新增 Dockerfile 如下

FROM ruby:2.5.1
MAINTAINER johnson <johnson@5xruby.tw>
RUN apt-get update && apt-get install -y build-essential libpq-dev nodejs vim postgis imagemagick
RUN mkdir /app
WORKDIR /app
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
ENV RAILS_ENV production
RUN bundle install
COPY . /app
CMD rake db:migrate assets:precompile && puma -C config/puma.rb

這一個 Dockerfile 產生的 Image 就是我們 Rails App 的 Image,它是從一個有 Ruby 的 Linux 環境做出來的 FROM ruby:2.5.1,同時它安裝了許多環境中需要的套件 RUN apt-get ...,最後用 puma 啟動它。

看到這邊可能會有個疑問,為什麼在第 6、7、10 行都要 COPY,而不是直接在第六行寫一個 COPY . /app 就好了?因為這樣子的話我們的 Rails 專案每次有修改,都會重新跑一次 bundle install 。這是什麼意思!? Dockerfile 在建立 Image 的時候是一層一層的包上去的,也就是 Dockerfile 中每一行設定都是會記錄下來的,如果這次專案沒有修改任何東西,當我們執行 docker build . 後,就不會重新產生 Image ,如果有做修改,就是從有修改的那個步驟再往下走,譬如第十行才修改,那就只會重新跑第十行後的設定。

目前操作的時候用 ruby 2.4.1 版會遇到問題,這篇文章先用 Ruby 2.5.1 版來操作,所以在Gemfile 要記得設定 ruby '2.5.1'

如果用 ruby 2.4.1 會遇到什麼問題呢?在用 Docker 跑 ruby 2.4.1 的時候用 apt-get update 指令會遇到下面這問題

W: Failed to fetch http://deb.debian.org/debian/dists/jessie-updates/InRelease  Unable to find expected entry 'main/binary-amd64/Packages' in Release file (Wrong sources.list entry or malformed file)

會有這問題是因為 /etc/apt/sources.list 檔案,也就是 APT 的套件來源清單沒更新,所以更新一下就能繼續用 ruby 2.4.1 囉,但在這範例我就直接用 ruby 2.5.1 給它操作下去!

docker-compose.yml

version: '3'

services:
  db:
    image: postgres:9.6
    environment:
      - "POSTGRES_USER=postgres"
      - "POSTGRES_PASSWORD=postgres"
    networks:
      - johnson
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://postgres:postgres@db/postgres
      - SECRET_KEY_BASE=8527f4cd4df56c3f41d8dc27251c09a55ed872eb299d230da758a05ee399b20ed655c33cf3039eccd59cf787cfb05ebe2b3e5a0b1811ad9d14163d0ee48f5f39
      - RAILS_SERVE_STATIC_FILES=true
    networks:
      - johnson
    depends_on:
      - db

networks:
  johnson:

現在我們同時啟動兩個 Container ,一個是我們的 Rails App,另一個則是 Postgres 資料庫,其中資料庫的部分我們是直接使用現成的 Image 來建立,也就是image: postgres:9.6,而 Rails App 則是從當前目錄 (備註1) 的 Dockerfile 產生的 Image 來產生也就是build: .

備註1 放 docker-compose.yml 的目錄

在 Rails App 的 Container 啟動的最後階段會執行 rake db:migrate (Dockerfile CMD 設定的)也就是說必須先讓資料庫的 Container 啟動後,才能啟動 Rails App 的 Container ,因此可以在 docker-compose.yml 中看到以下的設定。

app:
  depends_on:
     - db

在專案中新增完這兩個檔案後,就執行吧 docker-compose up 沒意外的話到瀏覽器輸入 http://127.0.0.1:3000 應該能看到專案跑起來了!希望你可以看到類似這樣的畫面XD

同時也可以用 docker ps 來看看這些跑起來的 Container

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
09f91d4b32aa        docker_app_app      "/bin/sh -c 'rake db…"   41 seconds ago      Up 11 seconds       0.0.0.0:3000->3000/tcp   docker_app_app_1
6297180618b4        postgres:9.6        "docker-entrypoint.s…"   42 seconds ago      Up 12 seconds       5432/tcp                 docker_app_db_1

步驟 2: Nginx 搭配 Puma

目標: 在 docker-compose.yml 中加入 Nginx 客製化 Nginx 的相關設定,讓 Nginx 能搭配 Puma 處理流量

誒!?什麼是 Nginx 跟 Puma?為什麼需要這些東西?剛剛不是已經把專案跑起來了嗎?

其實一個完整的網站服務,除了有我們的 Rails App 之外,還會有專門處理 http request 的 reverse proxy server 像是 Nginx 或是 Apache ,還有在 reverse proxy server 跟 Rails App 中間的 App Server,例如 Puma 或 Passenger。

如果想了解更多 Nginx 或是部署相關的說明,可以參考這篇文章

由於 Puma 是 Rails 內建的 App Server,所以他會和我們的 Rails App 一起啟動,Puma 的設定也在 config/puma.rb 調整即可,而 Nginx 的部分則是我們自己透過 Dockerfile 來客製化的。

為了要讓 Puma 可以搭配 Nginx,我們需要讓 Puma bind 在 Unix domain socket 上,而非一般常見的 TCP/IP network socket,修改 config/puma.rb 如下

# port        ENV.fetch("PORT") { 3000 }
bind "unix:///app/tmp/sockets/puma.sock"

再來我們要用 Docker 的 Nginx 來作為我們的 reverse proxy server,在專案目錄中再開一個 Dockerfile,路徑可以是 docker/nginx/Dockerfile,這個 Dockerfile 就是官方的 Nginx 加上我們修改過的 default.conf 檔案,這檔案就是 Nginx 設定檔,我們要在裡面設定 Nginx 處理 request 的規則。新增 docker/nginx/Dockerfile 如下

FROM nginx
COPY default.conf /etc/nginx/conf.d/default.conf

我們要用下面這個設定方式,來執行我們的 Nginx,修改 docker/nginx/default.conf 如下

server {
    listen       80;
    server_name  localhost;

     location / {
        proxy_pass http://unix:/app/tmp/sockets/puma.sock;
        proxy_set_header X-Forwarded-Host localhost;
    }
}

上面的設定中,比較需要注意的是第六行的部分,要讓 Nginx 把接收到的 request 傳給 Puma

修改 docker-compose.yml 如下

app:
  volumes:
      - /tmp/sockets:/app/tmp/sockets
nginx:
  build: docker/nginx/.
  ports:
    - "80:80"
  networks:
    - johnson
  volumes:
    - /tmp/sockets:/app/tmp/sockets
  depends_on:
    - app

docker-compose.yml 也加上 Nginx,由於我們要讓 Nginx 跟 Web 這兩個 Container 可以共用 /app/tmp/sockets/puma.sock 這個檔案,所以我們用 volumes 來設定,讓 App 跟 Nginx 的 Container 的 /app/tmp/sockets 都會對應到本機的 /tmp/sockets。由於這次我們更新到 config/puma.rb 所以我們需要執行一次 docker-compose build 來讓 Dockerfile 重新檢查並產生新版本的 Image,更新完後再跑一次 docker-compose up

這一次 Nginx 可以搭配 Puma 一起運作!可以直接在瀏覽器輸入 127.0.0.1 來看我們的網站,由於 Nginx 是設定 port 80 ,因此這次不用在網址後面加上 :3000,然後再試著用 docker ps 看看我們啟動的 Container 吧!

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                NAMES
c54bb26356b1        docker_app_nginx    "nginx -g 'daemon of…"   12 minutes ago      Up 13 seconds       0.0.0.0:80->80/tcp   docker_app_nginx_1
a65a5c532bb2        docker_app_app      "/bin/sh -c 'rake db…"   12 minutes ago      Up 13 seconds                            docker_app_app_1
6297180618b4        postgres:9.6        "docker-entrypoint.s…"   About an hour ago   Up 14 seconds       5432/tcp             docker_app_db_1

步驟 3: 部署到雲端上吧!

目標: 將我們的 Docker 應用部署到 DigitalOcean 平台上 並用申請好的 Domain Name 連線成功

首先到 DigitalOcean 上申請一台機器吧!DigitalOcean 有提供已經裝好 Docker 的機器選擇,其實這就是一台裝好 Docker 的 Ubuntu 機器,我自己是選每月 10 美元的方案!

DigitalOcean 的機器申請完後,可以在 Droplets 列表找到一組代表這台機器的 IP,有 IP 後就要來申請個 Domain Name,這樣才有部署網站的樣子嘛!可以網路上找個免費的服務來申請,像是 freenom 就是個不錯的選擇。這練習中我註冊的 Domain Name 是 dockerapp.ga

所以要記得把 docker/nginx/default.conf 修改如下

server {
    listen       80;
    server_name  dockerapp.ga;

    location / {
        proxy_pass http://unix:/app/tmp/sockets/puma.sock;
        proxy_set_header X-Forwarded-Host localhost;
    }
}

從這步驟起,我們要把所有的 Image 都放到 Docker Hub 上面,這樣就可以直接從 Docker Hub 把 Image pull 到機器上跑,這樣讓我們的環境更乾淨了一些。

首先把 docker-compose.yml 這兩個地方修改一下,這樣我們的 docker-compose up 用的 Image 就會是來自 Docker Hub ,而非當下目錄 build 出來的

  app:
    image: johnsonzhan121/docker_app:latest

  nginx:
    image: johnsonzhan121/docker_nginx:latest

johnsonzhan121 是我在 Docker Hub 上的帳號,我的 Image 都會放在這個帳號之下

修改完後,可以執行 docker-compose up 看看會發生什麼事?

Pulling app (johnsonzhan121/docker_app:latest)...
ERROR: manifest for johnsonzhan121/docker_app:latest not found

在 Docker Hub 並沒有我的 Image,我現在需要把 Image 一個一個在本地 build 完,然後 psuh 到 Docker Hub 上面,也就是執行以下指令

docker build . -t johnsonzhan121/docker_app
docker build docker/nginx/. -t johnsonzhan121/docker_nginx
docker push johnsonzhan121/docker_app
docker push johnsonzhan121/docker_nginx

事情還沒完成,我們要在 DigitalOcean 平台上執行 docker-compose up,還需要把 Docker Hub 上的 Image pull 到 DigitalOcean 上等等,要做的事太多,直接寫成腳本自動執行吧!以後只要執行 ruby auto_deploy.rb 就能把部署的事情做完啦!自己新增一個 auto_deploy.rb 檔案吧

#!/usr/bin/env ruby

p 'build docker_app and docker_nginx'
`docker build . -t johnsonzhan121/docker_app`
`docker build docker/nginx/. -t johnsonzhan121/docker_nginx`

p 'push docker_app and docker_nginx'
`docker push johnsonzhan121/docker_app`
`docker push johnsonzhan121/docker_nginx`

p 'update docker-compose.yml'
`scp docker-compose.yml root@178.128.214.6:/home/docker`

p 'pull Image from Docker Hub'
`ssh root@178.128.214.6  'docker pull johnsonzhan121/docker_app'`
`ssh root@178.128.214.6  'docker pull johnsonzhan121/docker_nginx'`

p 'remote docker-compose up'
`ssh root@178.128.214.6  'cd /home/docker && docker-compose up'`

178.128.214.6 是我 DigitalOcean 的 IP,你們的腳本要換上自己的 IP 喔,當然也可以用你申請到的 Domain Name

在執行 ruby auto_deploy.rb 之前記得要到伺服器上新增對應的目錄喔,像是這個 /home/docker 正常執行後終端機會長這樣

"build docker_app and docker_nginx"
"push docker_app and docker_nginx"
"update docker-compose.yml"
"pull Image from Docker Hub"
"remote docker-compose up"
Starting docker_db_1 ... done
Recreating docker_app_1 ... done
Recreating docker_nginx_1 ... done

然後就能用剛剛申請的 Domain Name 來試試囉!也可以用 ssh 登入 DigitalOcean 然後輸入指令 docker imagesdocker ps 查看,我的網站已經可以用 http://dockerapp.ga/ 連線上囉!雖然目前只能做 get 的操作還不能用 post 的操作。

步驟 4: HTTPS 認證

目標: 透過 certbot 來申請 SSL 憑證 可以用在瀏覽器輸入 https://dockerapp.ga 然後連線成功

一般正式的網站都會採用 HTTPS,來讓網站更安全更專業,我們要透過 Cerbot 自動化工具來申請免費的 Let’s Encrypt 的 SSL 憑證

docker-compose.yml 加入 certbot 以及調整 Nginx 如下

  nginx:
    image: johnsonzhan121/docker_nginx:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /tmp/sockets:/app/tmp/sockets
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot

  certbot:
    image: certbot/certbot
    volumes:
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot

我們要讓 Nginx 跟 certbot 可以共用 /etc/letsencrypt/var/www/certbot 這兩個目錄,前者是放我們 SSL 認證的資料,後者是拿來申請 SSL 憑證用的。再來就是修改 Nginx 設定檔的部分,我們要用兩個 Sever 的 block 來控制不同 port 進來的 request,第一個 server block 是接收 80 port 的 request,接收到後把 request 轉到第二個 server block 也就是 443 port 的。這邊可以注意到我們新增了 proxy_set_header X-Forwarded-Ssl on;,有了它後我們就能對網站做 post request 囉,不然原本會遇到 Rails 的 ActionController::InvalidAuthenticityToken 問題

server {
    listen       80;
    server_name  dockerapp.ga;
    return 301 https://dockerapp.ga$request_uri;
}

server {
    listen       443 ssl;
    server_name dockerapp.ga;

    ssl on;
    # 剛剛說的 SSL 認證資料
    ssl_certificate      /etc/letsencrypt/live/dockerapp.ga/fullchain.pem;
    ssl_certificate_key  /etc/letsencrypt/live/dockerapp.ga/privkey.pem;

    # 跟 Puma 搭配用
    location / {
        proxy_pass http://unix:/app/tmp/sockets/puma.sock;
        proxy_set_header X-Forwarded-Ssl on;
        proxy_set_header X-Forwarded-Host dockerapp.ga;
    }

    # 申請 SSL 憑證用的
    location ~ /.well-known {
        root /var/www/certbot;
    }
}

完成囉!執行看看 ruby auto_deploy.rb 執行完後好像開始不能連上網站了XD,我們需要登入到機器裡面看看,登入機器後用 docker ps

CONTAINER ID        IMAGE                              COMMAND                  CREATED              STATUS              PORTS               NAMES
8dac143fc834        johnsonzhan121/docker_app:latest   "/bin/sh -c 'rake db…"   About a minute ago   Up About a minute                       docker_app_1
2887c373e865        postgres:9.6                       "docker-entrypoint.s…"   18 minutes ago       Up 13 minutes       5432/tcp            docker_db_1

看來 Nginx 沒有跑起來,所以到 /home/docker 跑跑看 docker-compose up ,跑完後大概看到這樣的東西

docker_nginx_1 exited with code 1
certbot_1  | Saving debug log to /var/log/letsencrypt/letsencrypt.log
certbot_1  | Certbot doesn't know how to automatically configure the web server on this system. However, it can still get a certificate for you. Please run "certbot certonly" to do so. You'll need to manually configure your web server to use the resulting certificate.
docker_certbot_1 exited with code 1

我們遇到一個問題,我們要用 Nginx 來申請 SSL 憑證,但是 Nginx 沒有申請 SSL 憑證的情況下無法啟動 Container,要處理這問題很麻煩,我們需要先做出一個假的憑證讓 Nginx 可以正常啟動,然後刪除掉這假憑證,接著正式申請 Let’s Encrypt 的 SSL 憑證。看到這真是麻煩到讓人想放棄,好險有人已經針對這塊寫好了腳本讓大家一鍵完成以上工作,請參考這篇文章的教學 看完教學後,我們需要做的事情就是下載腳本,修改權限

curl -L https://raw.githubusercontent.com/wmnnd/nginx-certbot/master/init-letsencrypt.sh > init-letsencrypt.sh

chmod +x init-letsencrypt.sh

然後修改檔案裡面的 domains 等參數,然後再執行 sudo ./init-letsencrypt.sh 即可完成,試試看用 HTTPS 來拜訪網站吧!

步驟 5: Docker image 瘦身

目標: 用 alpine 的 Ruby Image 來幫我們的 web image 瘦身 從原本的 1.72GB 簡化到 503MB

Docker Image 的大小是衡量我們容器技術的重要指標,小的 Docker Image 可以讓部署更有效率而且也更安全!用 docker iamges 來看看我們的 web Image 到底多大吧,居然有 1.72GB,我們有很大的空間來幫它瘦身。

REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
johnsonzhan121/docker_app     latest              fab76a5630cb        45 minutes ago      1.72GB
ruby                          2.5.1               3c8181e703d2        5 months ago        869MB

要幫 Image 瘦身有很多方法,包含使用小的 Base Image 、用 Multi-Stage 的方式寫 Dockerfile、移除不用進入 Image 的檔案等等,在這次實作我們將要用縮小 Base Image 來瘦身,因為在 1.72 GB 的 Image 中 Base Image 就佔了 896MB 呀!

其實 Ruby 有更小 Image 可以使用,那就是 Alpine 版本的 Ruby Image 啦!什麼是 Alpine? Alpine 是一個輕型的 Linux 發行版本,目前大部分 Docker 官方映像檔都已經有支援 Alpine 作為基礎的映像檔,當然 Ruby 也不例外,下面是 Alpine 版本的 Ruby 比原版小的多,只有 45.4MB

REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
ruby                          2.5.1-alpine        d29267791323        6 months ago        45.3MB

來改寫我們的 Dockerfile 吧!

把原本的

FROM ruby:2.5.1
RUN apt-get update && apt-get install -y build-essential libpq-dev nodejs vim postgis imagemagick

改成

FROM ruby:2.5.1-alpine
RUN apk update && apk upgrade && apk add --update --no-cache build-base nodejs imagemagick postgresql-dev

第一行好理解,就是改用 Alpine 版本的 Ruby。第二行不是用 Ubuntu 常見的 apt-get,而是 apk add, apk 就是 Alpine 的套件管理工具,那我要怎麼知道每個原本用 apt-get 套件在 apk 有沒有支援呢?只能試試看了?或是到 apk 提供的文件去看囉。完成後再部署一次吧,執行 ruby auto_deploy.rb

執行後會看到類似這樣的錯誤訊息

web_1      | rake aborted!
web_1      | TZInfo::DataSourceNotFound: tzinfo-data is not present. Please add gem 'tzinfo-data' to your Gemfile and run bundle install

看起來是少裝了 tzinfo-data,這個 gem 到底是什麼呢?稍微看一下我們這個預設的 Gemfile 裡面其實有寫到

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

看上面的意思就是說,如果在某些環境執行 Rails 就會需要裝這個 gem,剛好我們從原來的 ruby:2.5.1 Debian 系統,換成現在 ruby:2.5.1-alpine 就遇到了這問題,沒意外的話,只要加上這個 gem 就真的能順利運作了吧!?於是我們加上這個 gem 重跑一次試試看!沒錯成功啦 ><

再一次用 docker iamges 指令來確認,我們成功瘦身了多少? 我們從原本的 1.72GB 瘦身成 503MB 啦!還有一些進步空間,那就之後再說吧~

REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
johnsonzhan121/docker_app     latest              6a7f0235885f        About a minute ago   503MB

結論

終於成功用 Docker 部署一個 Rails 應用到 DigitalOcean 平台上了,雖然很陽春但還是很開心可以完成。範例程式碼都放到 GitHub 上了,需要就拿去吧,每個步驟都有用 git commit 分出來。目前手邊的專案還沒有使用到 Docker 來部署,但在未來還是會想再撥一些時間研究 Docker,目前為止感覺它是相對有趣的技術,希望未來可以對 Docker 越來越熟悉,甚至在工作中使用到。