在Docker中創建應用

ReynaldoDol 8年前發布 | 18K 次閱讀 Docker

【下面內容是在基于Docker,用node.js開發和部署網絡應用過程中獲得的經驗和教訓。】

本例中,將從頭開始開發一個基于Docker的socket.io聊天例子,一直到可以實用,因此希望可以從這些教訓中學到什么,例如:

使用Docker開始一個節點應用

不要做“root”敢死隊成員

用binds使得test-edit-reload流程更短

在容器內管理node_modules使得重建更快(有一個竅門)

使用npm shrinkwrap確保可重復使用

開發和生產團隊共享同一個Dockerfile

本文默認讀者已經對Docker和node.js有一定熟悉程度。如果需要了解Docker,可以參見這篇介紹文章

開始

我們會從頭開始,最終的代碼可以從github獲得, 其中從頭到尾都有每一步的標簽。這里是第一步的代碼,如果感興趣可以訪問。

如果沒有Docker,就需要在節點上安裝系統以及其他依賴包,并且運行npm init來創建新包,這種方法并非不好,但是如果我們使用Docker的話,可以學習更多東西,(當然,使用Docker最重要的原因在于不需要在節點上安裝所有東西)。我們會以生成“bootstrapping container”作為開始,然后為應用設置npm包。

需要生成兩個文件,Dockerfile和docker-compose.yml,后續我們會加入更多內容。bootstrapping Dockerfile內容如下:

FROM node:4.3.2

RUN useradd --user-group --create-home --shell /bin/false app &&\

npm install --global npm@3.7.5

ENV HOME=/home/app

USER app

WORKDIR $HOME/chat

這個文件很短,但是都很重要:

  1. 第一行,從最新long term support(LTS)官方發行版docker image開始。我傾向于指定一個特定版本,而不是一個浮動的標簽,例如node:argon 或者 node:latest,因此如果別人在一臺不同設備生成此image時,會得到相同版本,而不是因為版本升級帶來版本變化。
  2. 創建一個普通用戶,起名叫app,在容器中運行此app。如果不這樣,容器內進程將會以root運行,違反了安全最佳實踐。需要Docker指南為了簡單跳過這一步,雖然我們會做一些額外的工作,但是卻很重要。
  3. 安裝最新版本npm,最近npm變化很大,特別是npm shrinkwrap,以后shrinkwrap將會變化很大。同時,最好在Dockerfile中指定版本,以避免升級帶來的麻煩。
  4. 最后,我們在一個RUN內部嵌入了兩條命令,減少了生成映像的層數。本例中,并不很明顯,但是不使用太多層是一個好習慣,可以減少占用磁盤空間,節省下載時間;另外下載,解壓縮,創建,安裝以及清理時都可以一步完成,而不需要每一步都保存每層的增量部分。

下一步生成bootstrapping compose文件: docker-compose.yml:

chat:

build: .

command: echo 'ready'

volumes:

- .:/home/app/chat

文件定義了一個簡單服務,從Dockerfile創建。到這一步只是echo “ready”然后就退出。volume這一行:/home/app/chat,告訴docker掛載應用目錄,將主機上的 . 目錄掛載到容器內部/home/app/chat目錄,因此任何主機上變化會自動在容器內部出現,反之亦然。對于保證開發test-edit-reload周期盡量短非常重要。然而當npm安裝依賴包時候會出現一些問題,我們后面會談到。

現在,我們可以繼續了。運行docker-compose時,docker會生成如Dockerfile中指定的映像,用此映像啟動一個容器,運行echo命令,意味著所有配置工作一切正常。

$ docker-compose up

Building chat

Step 1 : FROM node:4.3.2

---> 3538b8c69182

...

lots of build output

...

Successfully built 1aaca0ac5d19

Creating dockerchatdemo_chat_1

Attaching to dockerchatdemo_chat_1

chat_1 | ready

dockerchatdemo_chat_1 exited with code 0

啟動此映像生成一個容器,在其中運行交互式shell,運行初始包文件:

$ docker-compose run --rm chat /bin/bash

app@e93024da77fb:~/chat$ npm init --yes

... writes package.json ...

app@e93024da77fb:~/chat$ npm shrinkwrap

... writes npm-shrinkwrap.json ...

app@e93024da77fb:~/chat$ exit

在主機上可以看到如下結構,可以commit此版本內容:

$ tree

.

├── Dockerfile

├── docker-compose.yml

├── npm-shrinkwrap.json

└── package.json

可以訪問Github上的代碼 。

安裝依賴包

下一步是安裝應用的依賴包。我們希望通過Dockerfile在容器內部安裝,因此當第一次運行docker-compose up時,應用就是可用狀態。

為實現此功能,需要在Dockerfile中運行npm install,在此之前,需要得到package.json和npm-shrinkwrap.json文件,他們會被讀入映像內。改變如下:

diff --git a/Dockerfile b/Dockerfile

index c2afee0..9cfe17c 100644

--- a/Dockerfile

+++ b/Dockerfile

@@ -5,5 +5,9 @@ RUN useradd --user-group --create-home --shell /bin/false app &&\

ENV HOME=/home/app

+COPY package.json npm-shrinkwrap.json $HOME/chat/

+RUN chown -R app:app $HOME/*

++RUN chown -R app:app $HOME/*

+

USER app

WORKDIR $HOME/chat

+RUN npm install

同樣,很小的改變,但要點如下:

不僅是打包文件,還可以COPY主機上所有應用目錄到$HOME/chat,后面我們將會看到此時只拷貝必要文件將會在docker build時節省時間,其它可以在運行完npm install之后copy,這其實是更好利用docker build的分層cache功能。

通過COPY命令進入容器內的文件在容器內屬主是root,也就是說普通用戶app不能讀寫他們,因此使用chown改變文件屬性(當然如果可以在USER app之后做copy操作,而使得文件屬主是app用戶是最佳方案,但是現在還不是時候。)

最后,在運行npm instsall之前多加了一步,使得以用戶app權限運行,安裝所有依賴包到容器的$HOME/chat/node_modules目錄下。(另外,可以添加npm cache clean移除安裝時下載的tar文件;然而在重建image時并沒有幫助,只是增加空間而已。)

最有一點,當開發使用此映像時會引起一些麻煩,因為綁定了在容器內部$HOME/chat到主機的應用目錄。不幸的是,node_modules目錄在主機上并不存在,這一綁定實際上“隱藏”了我們安裝的node modules。

node_modules Volume 竅門

有幾個方法可以解決此問題,但是最優雅的解決辦法應該是使用一個內置包含node_modules的卷。為了實現此目的,需要在docker compose文件末尾加一行如下:

diff --git a/docker-compose.yml b/docker-compose.yml

index 9e0b012..9ac21d6 100644

--- a/docker-compose.yml

+++ b/docker-compose.yml

@@ -3,3 +3,4 @@ chat:

command: echo 'ready' volumes:

- .:/home/app/chat

    • - /home/app/chat/node_modules盡管一點點變化,但是牽涉和后臺很大變化:

build過程中,npm install安裝依賴包(下一節中添加)到映像內的$HOME/chat/node_modules 目錄下,我們在影像中用藍色標識出來:

~/chat$ tree # in image

.

├── node_modules

│ ├── abbrev

...

│ └── xmlhttprequest

├── npm-shrinkwrap.json

└── package.json

當使用compose文件從映像啟動容器時,docker首先將從主機將應用目錄綁定到容器內的$HOME/chat目錄下,用紅色標識如下:

~/chat$ tree # in container without node_modules volume

.

├── Dockerfile

├── docker-compose.yml

├── node_modules

├── npm-shrinkwrap.json

└── package.json

但是映像內的node_modules被綁定隱藏了;容器內部,我們只能看到主機上空的node_modules目錄。

然而,通過上面的改變,Docker接下來會創建一個卷,包含$HOME/chat/node_modules在影像中,并被掛載到容器中,再次覆蓋了主機上綁定的node_modules.

~/chat$ tree # in container with node_modules volume

.

├── Dockerfile

├── docker-compose.yml

├── node_modules │ ├── abbrev

...

│ └── xmlhttprequest

├── npm-shrinkwrap.json └── package.json

我們所期望的都實現了:主機上的源文件都被綁定到容器,使得更快改變內容;容器內運行應用的依賴包也可用。(備注:這些卷中依賴包到底存放在哪里呢?簡短說,存放在主機上由docker管理的獨立目錄中,參見docker文檔中關于volume部分)

打包安裝和Shrinkwrap

重新生成映像,生成安裝包。

$ docker-compose build

... builds and runs npm install (with no packages yet)...

chat 應用需要特定4.10.2版本,因此需要npm install后用--save選項將依賴包保存到package.json,更新npm-shrinkwrap.json。

$ docker-compose run --rm chat /bin/bash

app@9d800b7e3f6f:~/chat$ npm install --save express@4.10.2

app@9d800b7e3f6f:~/chat$ exit

注意,一般并不需要聲明確切版本,只運行npm install --save express就可以使用最新版本,因為package.json和shrinkwrap中持有下次build運行時的依賴包名稱。

使用npm shrinkwrap的原因如下:盡管可以在package.json中更新直接依賴包的版本,但是并不能更新一些松散依賴包的版本,這就意味著未來生成映像時,(如果不使用shrinkwrap)并不能保證拉下來的是同一版本的依賴包,使得應用出錯。這種問題經常出現,因此提倡使用shrinkwrap,如果熟悉ruby的dependency manager,npm-shrinkwrap.json功能跟Gemfile.locl是類似的。

最后,并不耗費任何多余的東西,因為容器就是在最后docker-compose run才運行,安裝的模塊消失了。但是下次運行docker build時,docker會檢測出package.json和shrinkwrap發生變化,必須重新運行npm install,這非常關鍵。所需要的包將會被安裝到影像中:

$ docker-compose build

... lots of npm install output

$ docker-compose run --rm chat /bin/bashapp@912d123f3cea:~/chat$ ls node_modules/accepts cookie-signature depd ...

...

app@912d123f3cea:~/chat$ exit

可以訪問Github上的代碼

運行應用

最后終于可以安裝有應用了,生成index.js 和 index.html,如前所示,運行npm install --save安裝socket.io包:

在Dockerfile中,可以告訴docker當使用映像啟動容器時運行什么命令,例如node index.js,從docker compose文件中移除占位命令(dummy command),docker會從Dockerfile中運行這條命令。最后,告訴docker compose在容器中暴露3000端口給主機,以從瀏覽器中訪問:

diff --git a/Dockerfile b/Dockerfile

index 9cfe17c..e2abdfc 100644

--- a/Dockerfile

+++ b/Dockerfile

@@ -11,3 +11,5 @@ RUN chown -R app:app $HOME/*

USER app WORKDIR $HOME/chat

RUN npm install

+

+CMD ["node", "index.js"]diff --git a/docker-compose.yml b/docker-compose.yml

index 9ac21d6..e7bd11e 100644

--- a/docker-compose.yml

+++ b/docker-compose.yml@@ -1,6 +1,7 @@

chat:

build: .

  • command: echo 'ready'

+ ports:

+ - '3000:3000'volumes:

- .:/home/app/chat

- /home/app/chat/node_modules

最后需要重新build一次,就可以運行docker-compose了

$ docker-compose build

... lots of build output$ docker-compose up

Recreating dockerchatdemo_chat_1

Attaching to dockerchatdemo_chat_1

chat_1 | listening on *:3000

然后,(如果運行在Mac上,需要做一些端口轉發工作將端口3000數據從boot2docker虛機轉發到主機)可以通過瀏覽器訪問:

http://localhost:3000.

可以訪問Github上的代碼

DevProd環境下的Docker

現在,開發環境應用運行在docker compose下,很酷的事情,下面看看其他可行的步驟:

如果想將應用部署到生產環境,很明顯需要將應用源碼生成到映像中,實現此功能,需要在執行完npm install后將應用目錄拷貝到容器中,這樣只有當package.json或者npm-shrinkwrap.json發生改變,docker將只重新運行npm install,而改變源文件并不會重新運行。注意,我們還需要解決以root權限拷貝文件的問題:

diff --git a/Dockerfile b/Dockerfile

index e2abdfc..68d0ad2 100644--- a/Dockerfile+++ b/Dockerfile@@ -12,4 +12,9 @@ USER app WORKDIR $HOME/chat

RUN npm install+USER root

+COPY . $HOME/chat

+RUN chown -R app:app $HOME/*

+USER app

+ CMD ["node", "index.js"]

現在我們可以獨立運行此容器,不需要從主機掛載任何卷,docker compose可以從多個compose文件中編輯避免代碼重復,但是因為應用很簡單,我們可以加入第二個compose文件,docker-compose.prod.yml,將應用運行在生產環境。

chat:

build: .

environment:

NODE_ENV: production

ports:

- '3000:3000'

運行應用在生產模式:

$ docker-compose -f docker-compose.prod.yml up

Recreating dockerchatdemo_chat_1

Attaching to dockerchatdemo_chat_1

chat_1 | listening on *:3000

同樣可以指定開發容器,例如,應用運行在nodemon狀態,當源文件發生改變時,容器自動重裝(reload)(注意,如果運行在Mac上,可能不會工作的很好,因為virtualbox共享目錄和inotify集成的不太好)。容器內運行npm install --save-dev nodemon,重新生成(rebuilding),可以覆蓋默認生產命令:node index.js,在容器中可以更好調整開發配置:

diff --git a/docker-compose.yml b/docker-compose.yml

index e7bd11e..d031130 100644--- a/docker-compose.yml+++ b/docker-compose.yml@@ -1,5 +1,8 @@ chat:

build: .+ command: node_modules/.bin/nodemon index.js

+ environment:

+ NODE_ENV: development ports:

- '3000:3000'

volumes:

注意,必須給nodemon全路徑,因為nodemon作為npm依賴包被安裝;可以配置npm腳本運行nodemon,但是我運行時有一些問題。容器運行npm腳本會花大約10秒鐘shutdown(默認超時),因為npm并不從docker轉發TERM信號給實際進程。所以最好直接運行命令行(這個問題應該在npm3.8.1+,因此很快就可以使用npm腳本了)。

$ docker-compose up

Removing dockerchatdemo_chat_1

Recreating 3aec328ebc_dockerchatdemo_chat_1

Attaching to dockerchatdemo_chat_1

chat_1 | [nodemon] 1.9.1

chat_1 | [nodemon] to restart at any time, enter rs

chat_1 | [nodemon] watching: .

chat_1 | [nodemon] starting node index.js

chat_1 | listening on *:3000

指定docker compose文件可以使同一個Dockerfile和映像用在不同環境中,盡管不是最節省空間的,因為我們可能在生產環境中安裝開發依賴包,但是我想這是dev-prod開發模式中性價比最好的方法。就如哲人所說“‘test as you fly, fly as you test.’

$ docker-compose run --rm chat /bin/bash -c 'npm test'npm info it worked if it ends with ok

npm info using npm@3.7.5

npm info using node@v4.3.2

npm info lifecycle chat@1.0.0~pretest: chat@1.0.0

npm info lifecycle chat@1.0.0~test: chat@1.0.0> chat@1.0.0 test /home/app/chat> echo "Error: no test specified" && exit 1

Error: no test specified

npm info lifecycle chat@1.0.0~test: Failed to exec test script

npm ERR! Test failed. See above for more details.

(提示:運行npm 時帶--silent參數可以不輸出多余部分)

可以訪問Github上的代碼

結論

本文中我們使一個應用運行在基于docker的開發和生產系統中,真酷!

我們跳過了主機安裝的一些步驟這樣直接進入節點環境配置,希望不會造成困難,因為這只需要做一次。

npm將依賴包安裝到掛載卷子目錄下使得我們的方案實現相對麻煩一些,(其它解決方案,例如ruby的bundler,將依賴包安裝到其它路徑下)但是我們可以通過內置卷的技巧解決這個問題。

這只是一個很簡單的應用,后續會有很多基于此應用的討論,其思路將涵蓋:

架構項目與多服務基礎之上,例如一個API,一個worker和一個靜態前端。一個大repo看起來比管理每個服務各自的repo更容易,但是卻引入了其它的復雜性。

使用 npm link

減少服務間共享代碼包

使用docker代替其它生產環境下日志管理和進程監控

狀態和配置管理,包括數據庫遷移

來自: http://dockone.io/article/1263

 本文由用戶 ReynaldoDol 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!