Enjoy Your Coding

使用 Docker 建立跨平台上開發環境

本篇文章將介紹如何使用Docker建立一個跨平台開發環境。使用Ruby on Rails開發環境作為範例!

為何使用Ruby on Rails (RoR)作為範例呢?筆者本身不是寫Rails的,但常常聽到開發Rails應用程式都是在Unix-like系統上做開發偏多,也比較方便,所以很多人會使用Mac或者Linux做開發,雖然Rails可以在Windows平台上做開發,但有些套件僅支援Unix-like的系統,安裝在Windows上的過程與運行過程中,又有很多繁雜的步驟要處理檢查,初學者很難一次就無痛架設好在Windows上的Rails開發環境。因此,本篇文章會透過Ruby on Rails作為範例,架設出一個可以在WindowsLinuxOS X上開發Rails應用程式的Docker環境。

什麼是 Unix-like 的系統呢?

常見的Unix-like的系統像是BSDLinuxOS X等Unix為基底的衍生系統或類似Unix的系統,都繼承了許多Unix系統的特性,並遵守POSIX規範。

什麼是Docker呢?

Docker是近年來相當熱門的技術應用之一,利用容器的技術來實現簡潔又輕量的VM (Virtual Machine),使用上較以往的VM還要方便。

今天如果我們需要安裝一個Service的服務在Ubuntu VM上面,通常的做法是去下載Ubuntu的ISO,並透過VM來安裝一個Ubuntu。除了安裝過程中,會透過網路去更新部分套件外,也會安裝一些在部署或開發中不需要的軟體及套件佔用了硬碟的空間。

若需要架設兩個以上的Ubuntu VM,則會花了更多空間裝重複不必要的軟體及套件,並且開啟兩個VM佔用系統的資源非常多。

透過Docker,若我們有兩個系統需要架設在相同版本的Ubuntu上面,兩個系統會使用Ubuntu 映像檔(Images)作為基底的映像檔來建立自己新的容器(Container)。在每個容器裡面,都是以同一個映像檔建立出的Ubuntu作業系統,工作目錄與檔案系統都是各自獨立不衝突!使用的資源比架設兩台VM來的較少。

部署環境

透過Docker這個技術,今天我們建立一個部署環境(Container)時,我們只需要在Dockerfile裡面寫一些腳本(Script),透過指令就會將我們部署所需要的套件等安裝好,並在Docker Engine裡運行。若需要架設第二個環境的話,則會在原本的Docker Engine上面隔離出新的環境(Container)出來。

開發環境

筆者的自身經驗:我在架設開發環境與部署環境時做使用的。我的論文搭配了Python、OpenCV實作,由於需用到OpenCV Contribution Library,所以需自行編譯環境,我分別在Windows、Ubuntu、OS X架設相關環境,雖然過程不難,但還是需要點耐心等待,為了之後開發不希望花太多時間在架設環境上,又不希望像VM不方便攜帶至不同電腦,所以使用Docker作為跨平台開發環境的解決方案。用了Docker後,在之後的應用程式開發與部署,個人專案都開始導入的Docker化。

在接下來的文中,筆者將帶大家分別介紹以下與Docker有關的內容:

常見名詞

Images(映像檔)

一個映像檔裡面包含了之後建立的Container要用的作業系統及相關設定。因為一個映像檔可能會被一個以上的Container做使用,所以僅可以讀取,不可寫入

Base Image(基底映像檔)

基底映像檔中,包含了該作業系統的Kernel(ex: 檔案系統)等架構,基本上就是一個最小化的作業系統做成的映像檔。也可以使用自己建立的Container或Image匯出成映像檔當做基底映像檔。

Container(容器)

是從Image中建立出來的獨立環境,Container跟VM一樣都是為了建立出獨立不衝突的環境而設計出來的。VM的虛擬化是使用Virtual Box或Hyper-V以作業系統建立出來的,而Docker的Container則是以應用程式為基底做出的Container,共用了架設Docker的作業系統(Host OS)上的資源。可進行讀寫。

Dockerfile

Dockerfile是一個文字檔,裡面可以撰寫我們自定義映像檔的腳本。

常用指令

在本章節「常用指令」,需開啟電腦上的Terminal做操作!

使用現有映像檔(Images)
# 下載Ubuntu 17.04的映像檔(Images)
docker pull ubuntu:17.04

# 下載Ubuntu Latest的映像檔案(Images)
# docker pull ubuntu:latest
docker pull ubuntu
查看電腦上現有的映像檔(Images)
docker images

結果如下:

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              17.04               8694892af3c5        2 weeks ago         92.7MB
ubuntu              latest              d355ed3537e9        2 weeks ago         119MB
刪除映像檔(Images)

若今天我們想刪除Ubuntu Latest的映像檔,預設沒有加TAG則代表是latest,不會任意抓一個,就是latest。

docker rmi ubuntu
# 或 docker rmi ubuntu:latest
建立容器(Container)

每個容器(Container)的建立都必須指定來自映像檔(Images),若本地沒有該映像檔,則會到Docker Hub中尋找。不存在...就看有沒有打錯字了或忘記匯入自己建立的囉。建立容器的指令如下:

docker run --restart=always -d --name ubuntu_1 -i -t ubuntu:latest

其中,-t代表--tty的簡寫,容器中加上了終端機的功能,-i代表--interactive的簡寫,容器有標準輸入(STDIN)的功能,--restart=always設定後,只要沒有主動刪除容器,系統重開機Docker啟動時,容器就會在背景啟動,-d代表了容器建立後在背景執行。

有了-t-i的設定,我們就能透過bash與容器進行互動。預設建立容器後,會啟動容器喔。還有更多的參數設定會在後面內容提到。

檢視容器(Container)清單
docker ps #列出目前在運行的容器
docker ps --all #列出所有的容器
開啟、暫停容器(Container)
docker start ubuntu_1 # ubuntu_1為容器名稱,也可以用容器id
docker stop ubuntu_1
對容器(Container)進行操作
docker exec -i -t ubuntu_1 bash #進入bash下指令進行操作,輸入exit後中斷。
docker exec -i -t ubuntu_1 <any_command> #該<any_command>執行結束後就中斷。
刪除容器(Container)

刪除容器前,記得先關閉容器

docker rm ubuntu_1 # docker run <container_id or name>
docker rm ubuntu_1 -f # 強制關閉 不管有沒有停止

撰寫自己的Dockerfile

在自己現有的Rails專案下,建立一個檔案Dockerfile,依序設定相關事項。

選擇基底的映像檔(Images)
FROM ubuntu:17.04
設定環境變數

在Container中,我們希望能事先定義好Linux Shell中會使用的變數(Variable),可以透過以下的指令進行設定。

ENV TZ 'Asia/Taipei'
更新及安裝相依性套件
RUN apt update && apt upgrade -y
RUN apt install wget git-core curl zlib1g-dev build-essential libssl-dev libreadline-dev libyaml-dev libxml2-dev libxslt1-dev libcurl4-openssl-dev libffi-dev -y
設定時區

通常我們安裝Ubuntu在VM或主機上時,安裝過程會設定時區等設定,前面內容有提到基底映像檔包含了最小化的作業系統,預設不會設定目前的時區,因為時區會因使用者而異,故若有需要的話,則需手動設定。(P.S. 在後面的Rails應用程式需有設定時區才能運行,不然會噴錯。

RUN apt install tzdata -y
# $TZ是在前面內容設定的環境變數
RUN echo $TZ > /etc/timezone && \
    rm /etc/localtime && \
    ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \
    dpkg-reconfigure -f noninteractive tzdata
編譯及安裝Ruby

在以下的內容中,安裝了Ruby 2.4.0。比較需要特別注意的地方是WORKDIR這個設定。在Unix-like的系統中,通常都是使用cd作為目錄的切換,但在Docker中比較不一樣的是,需使用WORKDIR來做目錄切換。若真的需要用cd的話,則必須將在該目錄要做的事情,全部寫在同一行的指令裡,用&&做串接。

# Dockerfile
# Download the Ruby 2.4.0 source code
RUN wget http://ftp.ruby-lang.org/pub/ruby/2.4/ruby-2.4.0.tar.gz
# Uncompress it
RUN tar -xzvf ruby-2.4.0.tar.gz
# switching directory
WORKDIR "ruby-2.4.0/"
# Compiling source code
RUN ./configure
RUN make install
# Checking Ruby is installed
RUN ruby -v
# Switching directory
WORKDIR "/"

# Removing Ruby source code
RUN rm -rf "ruby-2.4.0/"
安裝Node.js

在這邊讀者可能會覺得很困惑,為何我們需要安裝Node.js在我們的Rails環境裡?

由於Rails有些gem的函式庫需要JavaScript Runtime (Node.js)支援才能使用,所以我們需要安裝Node.js。

RUN apt install -y nodejs
安裝 Bundler 及 Rails

相信本來就有在寫Rails的朋友,應該已經很了解了。這篇文章不是針對Rails的朋友寫的,沒寫Rails的當作基本安裝設定就可以了。

RUN gem install bundler
RUN gem install rails

完整的Dockerfile如下:

# Dockerfile
FROM ubuntu:17.04
# Set timezone because tzinfo-data in Rails will use it
ENV TZ 'Asia/Taipei'
RUN apt update && apt upgrade -y
RUN apt install wget git-core curl zlib1g-dev build-essential libssl-dev libreadline-dev libyaml-dev libxml2-dev libxslt1-dev libcurl4-openssl-dev libffi-dev -y
RUN apt install tzdata
RUN echo $TZ > /etc/timezone && \
    rm /etc/localtime && \
    ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \
    dpkg-reconfigure -f noninteractive tzdata
# Using sqlite3 as default database
RUN apt install libsqlite3-dev sqlite3 -y
# Downloading ruby 2.4.0...
RUN wget http://ftp.ruby-lang.org/pub/ruby/2.4/ruby-2.4.0.tar.gz
# Uncompress it
RUN tar -xzvf ruby-2.4.0.tar.gz
# switching directory
WORKDIR "ruby-2.4.0/"
# Compiling source code
RUN ./configure
RUN make install
# Checking Ruby is installed
RUN ruby -v
# Switching directory
WORKDIR "/"
# Removing Ruby source code
RUN rm -rf "ruby-2.4.0/"
# Installing Node.js because Rails will use it to do stuffs on JavaScript
RUN apt install -y nodejs
# Installing Bunlder & Rails
RUN gem install bundler
RUN gem install rails
RUN rails -v
WORKDIR "/app"

從Dockerfile建立開發環境

建立映像檔(Images)

開啟Terminal進入剛剛新增Dockerfile的目錄裡,執行

docker build -t rails_dev:1.0 .

等待一段時間後,則會建立一個映像檔rails_dev:1.0在本地

匯出建立好的映像檔

根據前一步驟,我們建立了rails_dev:1.0的映像檔(Images),接著透過以下指令匯出成rails_dev_1.0.tar後,即可分享給其他開發人員匯入使用。

docker save rails_dev:1.0 > rails_dev_1.0.tar
匯入映像檔

接著透過以下的指令即可在另一台電腦上匯入剛剛的映像檔。

docker load -i rails_dev_1.0.tar
建立容器(Containers)

其中,$(pwd)是將目前Terminal執行的目錄帶入到命令列裡面,若使用Windows請改成%CD%-v是用來將目前OS上的某個目錄與Container裡的目錄做連結,不論在自己的OS上做變更,或者在Container裡面的該目錄做變更,都會同步。-p則是將某個port指定給自己電腦的某個port,在下列範例中是將自己電腦的3001分配給Container裡的3000。

docker run -v $(pwd):/app -p 3001:3000 --restart=always --name rails_dev_1 -i -t rails_dev:1.0

 輸入完後,會進入bash的介面,接著透過bundle還原套件,然後執行rails s就會開啟內部的port 3000,在自己電腦上的瀏覽器輸入http:/localhost:3001則會連到裡面的3000,即可進行開發。若沒有Rails的專案,請先下rails new .後,再下rails s