摘要: 本文以構建Haskell應用程序為例,演示了兩種不同的方法來實現多階段構建。
在這篇文章中,Deni Bertovic將向我們展示如何使用Docker來快速構建Haskell應用程序並生成Docker鏡像。
備註: Haskell 是一種標準化的,通用的純函數編程語言,有非限定性語義和強靜態類型。作為一門函數編程語言,主要控制結構是函數。Haskell具有「證明即程序、命題為類型」的特徵。(摘自維基百科)
接下來,我們將從兩個案例入手,通過對比分析來幫助您理解。第一個案例,使用相同的Linux發行版(ubuntu:16.04)進行開發。第二個案例,使用不同的操作系統和發行版來進行開發。
關於Docker多階段構建功能和「Stack Images Container」指令,會在該文章的最後有更多介紹。
案例一:在相同的操作系統或發行版上構建和部署
如果我們在相同的Linux發行版上構建Haskell應用程序(在本例中是一個Docker鏡像),那麼構建它的過程就會十分精簡。
在這種情況下,我們就能夠在本地構建我們的Haskell應用程序了。使用Stack,然後將編譯後的二進位文件嵌入到Docker鏡像中。
讓我們看一下將要使用的Makefile,特別是構建目標:
構建二進位文件和docker 鏡像
build:
@stack build
@BINARY_PATH=${BINARY_PATH_RELATIVE} docker-compose build
正如我們所看到的,我們先在本地使用Stack來構建二進位文件,然後通過調用docker-compose和提供的Dockerfile構建出最終的docker鏡像。
docker-compose文件如下所示:
version: “2”services:
myapp:
build:
context: .
args:
– BINARY_PATH
image: fpco/myapp
command: /opt/myapp/myapp
正如我們從docker-compose文件中看到的,我們在Docker構建時將一個「構建參數」傳遞進去。具體而言,我們傳遞的就是位於.stack-work/path/to/myapp中的二進位文件的相對位置。接下來,讓我們觀察一下Dockerfile,看看如何使用這個構建參數。
FROM ubuntu:16.04RUN mkdir -p /opt/myapp/
ARG BINARY_PATH
WORKDIR /opt/myapp
RUN apt-get update && apt-get install -y
ca-certificates
libgmp-dev
COPY “$BINARY_PATH” /opt/myapp
COPY static /opt/myapp/staticCOPY config /opt/myapp/config
CMD [“/opt/myapp/myapp”]
在Dockerfile中,我們只需要簡單的將.stack-work/path/to/myapp複製到Dockerfile中的預定位置即可。
案例二:在不同的操作系統或發行版中構建
如果在一個與我們部署的操作系統或發行版不同的機器上進行開發,那麼事情就會變得複雜一些,因為我們要將動態鏈接庫嵌入到Docker鏡像中。具體來說,我們現在必須在Docker容器中編譯二進位文件,而不是在本地主機上編譯它。
以前,可以通過下面兩種方式來完成:
-
將所有構建時相關的文件打包到最終的Docker鏡像中
-
將整個構建過程分解為Dockerfile.build(包含所有構建時相關的文件)和Dockerfile(包含運行時相關的文件和最終的二進位文件)兩部分。這是一個相當繁瑣的過程,需要一個shell腳本來輔助。首先構建第一個Docker鏡像,然後利用這個docker鏡像啟動一個容器,獲取已編譯的二進位文件,之後再啟動第二個容器,將這個已編譯二進位文件嵌入其中,最後將其生成為新的docker鏡像。您可以在Docker文檔中了解到更多相關信息。
現在,Docker已經採用了一個名為多階段構建的功能,它使整個構建過程變得簡單化、自動化。它所帶來的好處就是,我們不再使用構建工具和構建需求來擴大生產鏡像,從而保證我們原本的鏡像大小。
Docker多階段構建
在這種情況下,我們的Dockerfile看起來像以下這樣:
FROM fpco/stack-build:lts-9.9 as build
RUN mkdir /opt/build
COPY . /opt/build
RUN cd /opt/build && stack build –system-ghc
FROM ubuntu:16.04RUN mkdir -p /opt/myapp
ARG BINARY_PATH
WORKDIR /opt/myapp
RUN apt-get update && apt-get install -y
ca-certificates
libgmp-dev
# NOTICE THIS LINE
COPY –from=build /opt/build/.stack-work/install/x86_64-linux/lts-9.9/8.0.2/bin .
COPY static /opt/myapp/staticCOPY config /opt/myapp/config
CMD [“/opt/myapp/myapp”]
在Dockerfile中,我們首先使用fpco / stack-build:lts-9.9鏡像來構建我們的應用程序。在我們構建完鏡像之後,我們有另一個FROM 塊使用相同的fpco/stack-build基礎鏡像,在這裡我們複製了前一個版本的二進位文件。這使我們只能交付最終的二進位文件,而不需要任何相關的構建文件。
使用「Stack Images Container」
Stack通過一種將應用程序的可執行文件放入其中的方式來構建Docker鏡像。雖然在Docker引入多階段構建之前,這種支持是可用的,但與上述方法相比,它缺乏靈活性。然而,它需要對docker的熟悉程度要低得多,因此對於大多數人來說,應該更容易上手。
首先讓我們看看要將stack.yaml複製到stack-native.yaml所需的更改:
docker:Docker Carrying Haskell.jpg
enable: trueimage:
container:
base: “fpco/myapp-base”
name: “fpco/myapp”
add: static/: /opt/app/static
config/: /opt/app/config
正如我們所看到的,我們正在讓Stack使用docker構建我們的可執行文件(如果您正在構建像OSX或Windows這樣的非Linux平台,則需要該文件),然後指定一些我們想要Stack為我們構建容器的元數據。
我們需要將生成鏡像的名稱和本地目錄添加到鏡像中。Stack將自動為我們添加可執行文件。
讓我們看一下我們的Dockerfile.base,我們將用它來構建我們的基礎鏡像。
FROM ubuntu:16:04RUN mkdir -p /opt/app
WORKDIR /opt/app
RUN apt-get update && apt-get install -y
ca-certificates
libgmp-dev
正如我們所看到的,除了少了複製生成的二進位文件的部分,它與我們之前的Dockerfile沒有什麼不同,因為Stack將會為我們完成這一點。
我們可以使用make build-base來構建鏡像。然後建立新的鏡像來運行make build-stack-native。
上面的目標是這樣的:
構建用於stack image container的基礎鏡像
build-base:
@docker build -t fpco/myapp-base -f Dockerfile.base .
使用stack-native.yaml構建應用程序
build-stack-native: build-base
@stack –stack-yaml stack-native.yaml build
@stack –stack-yaml stack-native.yaml image container
現在來測試我們構建的鏡像吧,讓我們運行make run-stack-native試試看吧,如下所示:
**運行由stack image container構建的容器
run-stack-native:
@docker run -p 3000:3000 -it -w /opt/app
${IMAGE_NAME} myapp
可以在Stack文檔中閱讀關於stack image container的更多信息。
這篇文章演示了如何使用Docker多階段構建來構建Haskell應用程序並生成Docker鏡像,同時保持了鏡像原有的大小。此外,還展示了一種使用「Stack Images Container」的替代方法,同樣它也可以生成類似的docker鏡像,但與前者相比,它缺乏了一些靈活性。
根據您的項目需要,選擇適合您的方法吧!
註:上面的示例代碼可以在Github上找到,並且包含關於進程管理和許可權處理的一些內容,為簡潔起見,這些內容在本文中被省略了。