如果你正準備著手學習 Docker,別再觀望,動起手來吧!
在這篇文章中,我將告訴你 Docker 是如何工作的?使用中會遇到什麼問題?如何通過 Docker 完成一個基本的開發任務——構建一個微伺服器。
我們將以一臺配有 Node.js 服務和 MySQL 後臺的伺服器為例,從在本地執行程式碼開始,完成一個執行著微服務和資料庫的容器。
什麼是 Docker ?
從本質上來說,Docker 是一種軟體,讓使用者建立映象檔案(就像虛擬機器中的模板),然後在容器中執行這個映象的例項。
Docker 維護著有著大量映象的儲存庫,名字叫 Docker Hub ,你可以將它作為嘗試映象的起始點,或者用來免費儲存你的映象。你可以安裝 Docker ,選擇你喜歡的映象,然後在容器中執行它的例項。
本文我們將介紹建立映象、從映象建立容器等一系列內容。
安裝 Docker
如果你想跟上本文的節奏,那麼你需要安裝 Docker 。
點選 docs.docker.com/engine/installation 這個連結,在上面檢視適合你的系統的安裝嚮導。
如果你是 Mac 或者 Windows 作業系統,那麼你需要使用虛擬機器。我在 Mac OS X 上使用 Parallels 安裝 Ubuntu 虛擬機器來應付大多數的開發任務。因為它支援快照功能,當你做實驗的時候,他可以方便的將破壞了的環境恢復回去。
試試看
輸入以下命令:
1 |
docker run -it ubuntu |
一段時間後,你將會看到如下提示:
1 |
root@719059da250d:/# |
試試如下的命令,然後退出容器:
1 2 3 4 5 6 7 |
root@719059da250d:/# lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 14.04.4 LTS Release: 14.04 Codename: trusty root@719059da250d:/# exit |
這看起來沒什麼,但是其實在後臺發生了很多事情。
你看到的是在你的機器上執行著的 Ubuntu 的隔離容器環境裡的 bash shell。這個環境完全歸你所有——可以在上面安裝軟體,執行軟體,可以做任何你想做的事情。
下圖表明瞭剛剛發生了什麼(圖來自於《 理解 Docker 架構 》一文):
1. 列出如下的 Docker 指令:
- docker : 執行 docker 客戶端
- run : 執行一個新的容器
- -it :讓容器帶有“互動終端”的一個引數
- ubuntu : 容器所依賴的基礎映象
2. 在主機(我們的機器)上執行的 docker 服務檢查本地是否有所請求的映象拷貝——這裡發現沒有。
3. docker 服務檢查公有儲存庫(the docker hub),看是否有可用的名為 ubuntu
的映象——這裡發現有。
4. docker 服務下載映象,將其儲存到本地快取裡(為了下一次直接使用)。
5. docker 服務基於 ubuntu 映象建立新的容器。
Try any of these:
試試下面這些命令:
1 2 3 |
docker run -it haskell docker run -it java docker run -it python |
我們沒準備使用 Haskell ,但是你可以看到,搭建一個環境是多麼容易。
構建自己的映象也很輕鬆,可以在這上面安裝應用程式或者服務,可以是資料庫,或者是其他你需要的。隨後就可以在任意安裝了 Docker 的機器上執行它們——要保證映象是相同的、可預測的方式在每臺機器上執行。我們可以將軟體及其執行所需的環境整體構建成程式碼,並且輕鬆部署。
讓我們以一個簡單微伺服器為例。
概述
我們將要用 Node.js 和 MySQL 建立一個讓我們管理郵件地址到電話號碼目錄的微服務。
開始
要完成本地開發,需要安裝MySQL,並且建立一個測試資料庫…
…搖頭。
建立本地資料庫,並且上面執行指令碼,這很容易,但是可能會帶來一些問題。很多不受控制的事情開始了。它可能工作,我們甚至可以通過提交進程式碼庫的 shell 指令碼來控制這些步驟,但是如果其他開發人員已經安裝了 MySQL 了呢?如果他們的資料庫已經使用了我們想要建立的名稱 ‘users’ 了呢?
第一步:在 Docker 中建立一個資料庫測試伺服器
這是很好的 Docker 應用場景。我們可能不想在 Docker 裡執行生產環境資料庫(比如可能會使用 Amazon RDS),但是可以使用 Docker 容器建立一個乾淨的 MySQL 資料庫做開發——讓我們的開發及其保持乾淨,並且保證所有東西都在控制中,並且可重複使用。
執行下面的命令:
1 |
docker run --name db -d -e MYSQL_ROOT_PASSWORD=123 -p 3306:3306 mysql:latest |
該命令啟動一個執行著的 MySQL 例項,通過 3306 埠訪問,root 密碼為 123 。
docker run
告訴引擎,使用者想要執行一個映象(在最後傳入的是映象,mysql:latest )- –name db 將整個容器命名為 db 。
- -d detach,在後臺執行容器。
- -e MYSQL_ROOT_PASSWORD=123(或者是 –env)環境變數 – 引數告訴 docker 所提供的環境變數。這之後跟著的變數正是 MySQL 映象檢查且用來設定的預設 root 密碼。
- -p
3306:3306(或者 --publish)
告訴引擎使用者想要將容器內的3306埠對映到外部的3306埠上。
最後一部分很重要——即使這是 MySQL 的預設埠,如果使用者不顯式告訴 docker 想要對映的埠,docker 就會阻塞該埠的訪問(因為容器預設是隔離的,直到使用者告訴 docker 想要訪問它們)。
該命令返回值是容器 id,這是容器的指標,使用者可以用它來停止容器,向容器傳送命令等等。讓我們看看正在執行的是哪些容器:
1 2 3 |
$ docker ps CONTAINER ID IMAGE ... NAMES 36e68b966fd0 mysql:latest ... db |
關鍵的資訊是容器 ID,映象和名稱。連線到這個映象看看裡面有什麼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ docker exec -it db /bin/bash root@36e68b966fd0:/# mysql -uroot -p123 mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | +--------------------+ 1 rows in set (0.01 sec) mysql> exit Bye root@36e68b966fd0:/# exit |
下面這麼做也很有意思:
1. docker exec -it db
:告訴 docker 使用者想要在名為 db
的容器裡執行一個命令(我們也可以使用 id,或者 id 的前幾個字母)。 -it
確保使用者有互動型終端。
2. mysql -uroot -p123
:我們實際在容器裡作為程式執行的命令,這裡是 mysql 客戶端。
我們可以建立資料庫,表,使用者,其他你需要的等等。
打包測試資料庫
在容器內執行 MySQL 需要一些 Docker 技巧,但是讓我們先打住,看看服務。現在,使用指令碼建立一個 test-database
目錄來啟動資料庫,停止資料庫以及搭建測試資料:
1 2 3 |
test-databasesetup.sql test-databasestart.sh test-databasestop.sh |
啟動指令碼很簡單:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#!/bin/sh # Run the MySQL container, with a database named 'users' and credentials # for a users-service user which can access it. echo "Starting DB..." docker run --name db -d -e MYSQL_ROOT_PASSWORD=123 -e MYSQL_DATABASE=users -e MYSQL_USER=users_service -e MYSQL_PASSWORD=123 -p 3306:3306 mysql:latest # Wait for the database service to start up. echo "Waiting for DB to start up..." docker exec db mysqladmin --silent --wait=30 -uusers_service -p123 ping || exit 1 # Run the setup script. echo "Setting up initial data..." docker exec -i db mysql -uusers_service -p123 users < setup.sql |
該指令碼在一個分離容器鍾執行資料庫映象(比如,在後臺執行),建立了一個使用者來訪問 users
資料庫,然後等待資料庫伺服器啟動,隨後執行 setup.sql
指令碼來設定初始資料。
setup.sql 的內容是:
1 2 3 4 5 6 |
create table directory (user_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, email TEXT, phone_number TEXT); insert into directory (email, phone_number) values ('homer@thesimpsons.com', '+1 888 123 1111'); insert into directory (email, phone_number) values ('marge@thesimpsons.com', '+1 888 123 1112'); insert into directory (email, phone_number) values ('maggie@thesimpsons.com', '+1 888 123 1113'); insert into directory (email, phone_number) values ('lisa@thesimpsons.com', '+1 888 123 1114'); insert into directory (email, phone_number) values ('bart@thesimpsons.com', '+1 888 123 1115'); |
stop.sh
指令碼會停止容器並且刪除容器(docker 預設會保留容器,這樣能夠快速重啟,本示例中並不需要這樣):
1 2 3 4 |
#!/bin/sh # Stop the db and remove the container. docker stop db && docker rm db |
之後會進一步簡化這個過程,讓它更加順暢。在 repo 裡的 step1 分支裡檢視這一階段的程式碼。
第二步:用 Node.js 建立一個微服務
本文的主題是 Docker 的學習,因此並不會花太多篇幅講解 Node.js 的微服務。只是強調一些重點。
1 2 3 4 5 6 7 8 |
test-database/ # contains the code seen in Step 1 users-service/ # root of our node.js microservice - package.json # dependencies, metadata - index.js # main entrypoint of the app - api/ # our apis and api tests - config/ # config for the app - repository/ # abstraction over our db - server/ # server setup code |
讓我們仔細看看這一部分。首先看看這個程式碼庫。最好將你的資料庫訪問封裝和抽象成一些類,允許模擬它來實現測試目的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
// repository.js // // Exposes a single function - 'connect', which returns // a connected repository. Call 'disconnect' on this object when you're done. 'use strict'; var mysql = require('mysql'); // Class which holds an open connection to a repository // and exposes some simple functions for accessing data. class Repository { constructor(connection) { this.connection = connection; } getUsers() { return new Promise((resolve, reject) => { this.connection.query('SELECT email, phone_number FROM directory', (err, results) => { if(err) { return reject(new Error("An error occured getting the users: " + err)); } resolve((results || []).map((user) => { return { email: user.email, phone_number: user.phone_number }; })); }); }); } getUserByEmail(email) { return new Promise((resolve, reject) => { // Fetch the customer. this.connection.query('SELECT email, phone_number FROM directory WHERE email = ?', [email], (err, results) => { if(err) { return reject(new Error("An error occured getting the user: " + err)); } if(results.length === 0) { resolve(undefined); } else { resolve({ email: results[0].email, phone_number: results[0].phone_number }); } }); }); } disconnect() { this.connection.end(); } } // One and only exported function, returns a connected repo. module.exports.connect = (connectionSettings) => { return new Promise((resolve, reject) => { if(!connectionSettings.host) throw new Error("A host must be specified."); if(!connectionSettings.user) throw new Error("A user must be specified."); if(!connectionSettings.password) throw new Error("A password must be specified."); if(!connectionSettings.port) throw new Error("A port must be specified."); resolve(new Repository(mysql.createConnection(connectionSettings))); }); }; |
其實有很多種其他實現方式!但是我們可以像下面這樣建立 Repository
物件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
repository.connect({ host: "127.0.0.1", database: "users", user: "users_service", password: "123", port: 3306 }).then((repo) => { repo.getUsers().then(users) => { console.log(users); }); repo.getUserByEmail('homer@thesimpsons.com').then((user) => { console.log(user); }) // ...when you are done... repo.disconnect(); }); |
在 repository/repository.spec.js
檔案裡也有一系列的單元測試。得到 repo 後,就可以建立伺服器了。server/server.js
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// server.js var express = require('express'); var morgan = require('morgan'); module.exports.start = (options) => { return new Promise((resolve, reject) => { // Make sure we have a repository and port provided. if(!options.repository) throw new Error("A server must be started with a connected repository."); if(!options.port) throw new Error("A server must be started with a port."); // Create the app, add some logging. var app = express(); app.use(morgan('dev')); // Add the APIs to the app. require('../api/users')(app, options); // Start the app, creating a running server which we return. var server = app.listen(options.port, () => { resolve(server); }); }); }; |
該模組暴露了一個 start
函式,可以像下面這樣使用:
1 2 3 4 |
var server = require('./server/server); server.start({port: 8080, repo: repository}).then((svr) => { // we've got a running http server :) }); |
注意到 server.js 中
使用了 api/users/js 吧
?程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
// users.js // // Defines the users api. Add to a server by calling: // require('./users') 'use strict'; // Only export - adds the API to the app with the given options. module.exports = (app, options) => { app.get('/users', (req, res, next) => { options.repository.getUsers().then((users) => { res.status(200).send(users.map((user) => { return { email: user.email, phoneNumber: user.phone_number }; })); }) .catch(next); }); app.get('/search', (req, res) => { // Get the email. var email = req.query.email; if (!email) { throw new Error("When searching for a user, the email must be specified, e.g: '/search?email=homer@thesimpsons.com'."); } // Get the user from the repo. options.repository.getUserByEmail(email).then((user) => { if(!user) { res.status(404).send('User not found.'); } else { res.status(200).send({ email: user.email, phoneNumber: user.phone_number }); } }) .catch(next); }); }; |
這些檔案都有和原始碼匹配的單元測試。
我們還需要配置。與其使用特定的庫函式,不如使用一個簡單的檔案 – config/config.js :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// config.js // // Simple application configuration. Extend as needed. module.exports = { port: process.env.PORT || 8123, db: { host: process.env.DATABASE_HOST || '127.0.0.1', database: 'users', user: 'users_service', password: '123', port: 3306 } }; |
我們可以按需進行配置。目前,大部分配置是硬編碼的,但是從埠的配置中
可以看出,我們可以很容易的通過新增環境變數的方式來改變它。
最後一步 – 將它和包含所有東西的 index.js
檔案連線到一起:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// index.js // // Entrypoint to the application. Opens a repository to the MySQL // server and starts the server. var server = require('./server/server'); var repository = require('./repository/repository'); var config = require('./config/config'); // Lots of verbose logging when we're starting up... console.log("--- Customer Service---"); console.log("Connecting to customer repository..."); // Log unhandled exceptions. process.on('uncaughtException', function(err) { console.error('Unhandled Exception', err); }); process.on('unhandledRejection', function(err, promise){ console.error('Unhandled Rejection', err); }); repository.connect({ host: config.db.host, database: config.db.database, user: config.db.user, password: config.db.password, port: config.db.port }).then((repo) => { console.log("Connected. Starting server..."); return server.start({ port: config.port, repository: repo }); }).then((app) => { console.log("Server started successfully, running on port " + config.port + "."); app.on('close', () => { repository.disconnect(); }); }); |
我們做了一點錯誤處理,在此之上僅僅載入了配置,建立了 repo 並且啟動了伺服器。
這就是微服務,它讓使用者能夠得到所有使用者,或者搜尋某個使用者:
1 2 |
HTTP GET /users # gets all users HTTP GET /search?email=homer@thesimpons.com # searches by email |
如果下載了相關程式碼,可以發現有一些可用的命令:
1 2 3 4 5 6 |
cd ./users-service npm install # setup everything npm test # unit test - no need for a test database running npm start # run the server - you must have a test database running npm run debug # run the server in debug mode, opens a browser with the inspector npm run lint # check to see if the code is beautiful |
除了程式碼之外,我們完成了:
1. 用於除錯的 Node 皮膚
2. 用於單元測試的 Mocha/shoud/supertest
3. 用於 linting 的 ESlint
大功告成!
使用如下命令執行資料庫測試:
1 2 |
cd test-database/ ./start.sh |
然後啟動服務:
1 2 |
cd ../users-service/ npm start |
可以用瀏覽器開啟 localhost:8123/users,就可以看到資料庫已經可以使用了。如果使用的是 Docker Machine(假設,在 Mac 或者 Windows 上),那麼 localhost
無法訪問,你需要使用 docker 的 IP。可以通過 docker-machine ip
得到 IP 地址。
可以看到,我們很快完成了服務的建立。繼續下一步之前如果想檢視程式碼,見 step2 分支。
第三步:微服務 Docker 化
現在開始變得有趣啦!
我們已經有了一個可以執行在開發環境裡的微服務,只要它和安裝的 Node.js 版本相容即可。這一步想做的是搭建起我們的服務,這樣可以從其中建立一個 Docker Image,從而可以將服務部署到任何支援 docker 的地方。
要達到這一目的,需要建立一個 Dockerfile。Dockerfile 告訴 Docker 引擎如何構建映象。我們會在 users-service
目錄下建立一個簡單的 Dockerfile,並且研究如何通過修改它來適應需求。
建立 Dockerfile
在 users-service/
目錄下建立名為 Dockerfile
的文字檔案,內容如下:
1 2 3 4 5 |
# Use Node v4 as the base image. FROM node:4 # Run node CMD ["node"] |
執行如下命令列構建映象,並在映象上執行容器:
1 2 |
docker build -t node4 . # Builds a new image docker run -it node4 # Run a container with this image, interactive |
首先看看構建命令。
1. docker build
告訴引擎使用者需要建立一個新的映象
2. -t node4
使用標籤 node4
標記該映象。之後就可以使用這個標籤來指代該映象。
3. .
在當前目錄裡查詢 Dockerfile
.
控制檯打出一些輸出之後,就可以看到新的映象建立好了。使用 docker images
命令可以在系統裡看到所有映象。下面的命令和之前的很類似:
1. docker run
從某個映象裡執行新容器
2. -it
使用互動式終端
3. node4
是想要在容器裡使用的映象的標籤。
當執行該映象時,會得到 node repl,執行如下命令檢查當前版本:
1 2 3 |
> process.version 'v4.4.0' > process.exit(0) |
這很可能和你當前機器上的 node 版本不同。
檢查 Dockerfile
從 Dockerfile 裡可以很容易看出發生了什麼:
1. FROM node:4
在 Dockerfile 裡指定的第一件事就是基礎映象。docker hub上的 Node 官方頁面可以搜尋列出所有可用映象。這裡用的是安裝了 node 的 ubuntu。
2. CMD ["node"]
裡的 CMD
告訴 docker 該映象需要執行 node 程式。當 node 程式終止時,容器會關閉。
使用額外的幾個命令,可以更新 Dockerfile,從而執行服務:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# Use Node v4 as the base image. FROM node:4 # Add everything in the current directory to our image, in the 'app' folder. ADD . /app # Install dependencies RUN cd /app; npm install --production # Expose our server port. EXPOSE 8123 # Run our app. CMD ["node", "/app/index.js"] |
唯一的改變是使用了 ADD
命令將當前目錄下的所有東西拷貝到名為 app/
的容器目錄裡。隨後使用 RUN
在映象裡執行命令,該命令安裝了模組。最後,EXPOSE
了伺服器埠,告訴 docker 想要支援 8123
埠的連線,然後執行伺服器程式碼。
確保 test-database 服務已經執行著,然後再次構建並且執行映象:
1 2 |
docker build -t users-service . docker run -it -p 8123:8123 users-service |
如果在瀏覽器裡檢視 localhost:8123/users
,會看到一個錯誤,檢查控制檯,提示容器報告了一些問題:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
--- Customer Service--- Connecting to customer repository... Connected. Starting server... Server started successfully, running on port 8123. GET /users 500 23.958 ms - 582 Error: An error occured getting the users: Error: connect ECONNREFUSED 127.0.0.1:3306 at Query._callback (/app/repository/repository.js:21:25) at Query.Sequence.end (/app/node_modules/mysql/lib/protocol/sequences/Sequence.js:96:24) at /app/node_modules/mysql/lib/protocol/Protocol.js:399:18 at Array.forEach (native) at /app/node_modules/mysql/lib/protocol/Protocol.js:398:13 at nextTickCallbackWith0Args (node.js:420:9) at process._tickCallback (node.js:349:13) |
我勒個去!從 users-service
容器到 test-database
容器的連線被拒絕了。執行 docker ps
檢視所有執行著的容器:
1 2 3 |
CONTAINER ID IMAGE PORTS NAMES a97958850c66 users-service 0.0.0.0:8123->8123/tcp kickass_perlman 47f91343db01 mysql:latest 0.0.0.0:3306->3306/tcp db |
這兩個容器都執行著呢,到底怎麼回事呢?
連線容器
我們看到的問題實際上是可以預期的。Docker 容器應該是互相隔離的,因此如果不顯式地允許容器間連線的話就容器間就無法互聯。
是的,使用者可以從自己的機器(宿主機)連線到容器裡,因為我們為這樣的連線開啟了埠(比如,使用了-p 8123:8123
)。如果以同樣的方式允許容器間互聯,那麼執行在同一臺機器上的兩個容器之間就應該能夠通訊,即使開發人員不想這麼做。並且這是災難性的,尤其是我們在叢集的機器上利用容器執行不同的應用程式的時候。
如果想要從某個容器連線到另一個容器,需要連線這兩個容器,告訴 docker,使用者顯式想要允許這兩個容器間通訊。有兩種方式可以完成這一目標,第一種是“不流行的舊方式”但是非常簡單,第二種之後會介紹。
使用 link 引數連線容器
當執行容器時,可以使用 link
引數告訴 docker 我們想要連線到另外的容器上。本文示例中,可以通過如下命令正確執行服務:
1 |
docker run -it -p 8123:8123 --link db:db -e DATABASE_HOST=DB users-service |
1. docker run -it
在容器裡執行 docker 映象,使用互動式終端。
2. -p 8123:8123
將宿主機的 8123 埠對映到容器的 8123 埠上
3. link db:db
連線到名為 db
的 容器上,並且用 db
指代該容器
4. -e DATABASE_HOST=db
將環境變數 DATABASE_HOST
設定為 db
5. users-service
是在容器內執行的映象名稱
現在當我們訪問 localhost:8123/users
時一切工作正常。
它是如何工作的呢?
還記得服務的配置檔案麼?它讓使用者能夠使用環境變數指定資料庫的主機名:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// config.js // // Simple application configuration. Extend as needed. module.exports = { port: process.env.PORT || 8123, db: { host: process.env.DATABASE_HOST || '127.0.0.1', database: 'users', user: 'users_service', password: '123', port: 3306 } }; |
執行容器時,將環境變數設定為 DB
,這意味著要連線到一個名字為 DB
的主機上。當連線到容器上時,docker 引擎會自動為我們設定好一切。
嘗試執行 docker ps
列出所有執行著的容器。查詢執行 users-service
的容器名稱,這是個隨機名稱,例如 trusting_jang
:
1 2 3 4 |
docker ps CONTAINER ID IMAGE ... NAMES ac9449d3d552 users-service ... trusting_jang 47f91343db01 mysql:latest ... db |
現在可以看到容器可用的主機:
1 2 3 4 5 6 7 8 9 |
docker exec trusting_jang cat /etc/hosts 127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters 172.17.0.2 db 47f91343db01 # linking magic!! 172.17.0.3 ac9449d3d552 |
還記得 docker exec
是怎麼工作的嗎?選擇一個容器名,之後跟著想在容器上執行的命令,在本例中是 cat /etc/hosts
。
好了,主機檔案之前可沒有 # linking magic!!
註釋,可以看到 -docker 將 db
新增到了主機檔案裡,因此可以通過主機名連線到容器上。這是連線資訊:
1 2 3 4 5 6 7 |
docker exec trusting_jang printenv | grep DB DB_PORT=tcp://172.17.0.2:3306 DB_PORT_3306_TCP=tcp://172.17.0.2:3306 DB_PORT_3306_TCP_ADDR=172.17.0.2 DB_PORT_3306_TCP_PORT=3306 DB_PORT_3306_TCP_PROTO=tcp DB_NAME=/trusting_jang/db |
從該命令還可以看到當 docker 連線容器時,它還提供了一系列包含有用資訊的環境變數,比如,主機名,tcp 埠和容器名。
第3步完成了 —— MySQL 資料庫正常執行在容器裡,還可以在本地或者在容器裡執行 node.js 微服務,並且已經知道了如何連線這兩者。
如果你想了解更多,可以在 step3 的分支裡檢視這一階段的程式碼。
第4步:環境的整合測試
現在可以編寫整合測試,呼叫實際伺服器,作為 docker 容器執行,呼叫容器化的測試資料庫。
可以用任何語言,或者在任何平臺上完成整合測試,但是為了保持簡潔,這裡使用的是 Node.js,因為專案裡已經使用了Mocha 和 Supertest。
在名為 integration-tests
的新目錄下,建立一個 index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var supertest = require('supertest'); var should = require('should'); describe('users-service', () => { var api = supertest('http://localhost:8123'); it('returns a 200 for a known user', (done) => { api.get('/search?email=homer@thesimpsons.com') .expect(200, done); }); }); |
它會檢查 API 呼叫,並且顯示測試結果。
只要 users-services
和 test-database
正在執行,測試就能夠通過。但是,這時候服務開始變得有點難處理:
1. 需要使用 shell 指令碼來啟動和停止資料庫
2. 需要記住一系列命令來基於資料庫啟動使用者服務
3. 需要使用 node 直接執行整合測試
既然我們已經很熟悉 Docker 了,應該能夠解決這些問題。
簡化 Test 資料庫
目前測試資料庫有如下檔案:
1 2 3 |
/test-database/start.sh /test-database/stop.sh /test-database/setup.sql |
既然已經很熟悉 Docker 了,讓我們嘗試改進它們。
在 Docker Hub 上檢視 mysql 映象文件,有一處註釋告訴使用者任何新增到映象的 /docker-entrypoint-initdb.d
目錄的 .sql
或者 .sh
檔案會在搭建 DB 的時候執行。
這意味著可以使用 Dockerfile
代替 start.sh
和 stop.sh
。
1 2 3 4 5 6 7 8 |
FROM mysql:5 ENV MYSQL_ROOT_PASSWORD 123 ENV MYSQL_DATABASE users ENV MYSQL_USER users_service ENV MYSQL_PASSWORD 123 ADD setup.sql /docker-entrypoint-initdb.d |
現在執行測試資料庫只需要:
1 2 |
docker build -t test-database . docker run --name db test-database |
組合
構建並且執行每個容器仍然有些費時。可以使用 Docker Compose 工具進一步簡化。
Docker Composer 允許使用者建立一個檔案,在其中定義系統裡的每個容器,容器間的關係,並且構建或者執行它們。
首先,安裝 Docker Compose。在專案根目錄下建立一個新檔案,稱為 docker-compose.yml
:
1 2 3 4 5 6 7 8 9 10 11 12 |
version: '2' services: users-service: build: ./users-service ports: - "8123:8123" depends_on: - db environment: - DATABASE_HOST=db db: build: ./test-database |
現在就可以試一下啦:
1 2 |
docker-compose build docker-compose up |
Docker Compose 會構建出應用程式所需要的所有映象,從其上建立出容器,並且以正確順序執行容器,從而啟動整個應用程式!
docker-compose build
命令構建 docker-compose.yml
檔案裡列出的每個映象:
1 2 3 4 5 6 7 8 9 10 11 12 |
version: '2' services: users-service: build: ./users-service ports: - "8123:8123" depends_on: - db environment: - DATABASE_HOST=db db: build: ./test-database |
每個服務的 build
值告訴 docker 到哪裡找到 Dockerfile
。當使用者執行 docker-compose up
時,docker 會啟動所有服務。注意在 Dockerfile
裡,使用者可以指定埠和依賴關係。實際上,在這個檔案裡,使用者可以更改所有配置。
在另一個終端裡,執行 docker compose down
,可以正常關閉容器。
總結
本文裡介紹了很多 docker 知識,不過這裡還有一些。我希望你能夠從本文找到一些有趣有用的東西,能夠幫助你在工作中使用 docker。
和平常一樣,歡迎提問和建議!同時強烈推薦文件《理解 Docker 》,可以幫助大家更深入地理解 docker 的工作機制。
在 github.com/dwmkerr/node-docker-microservice 處可以看到本文所構建的專案的最終原始碼。
注意
1. 將所有東西完全拷貝不是什麼好主意,因為那樣也會拷貝 node_modules 資料夾。通常來說顯式指定想要拷貝的檔案或資料夾會更好,或者使用 .dockerignore 檔案,它和 .gitignore 檔案類似。
2. 如果伺服器不正常工作,顯示一個很討厭的 exception,這是由一個bug造成的,詳見 github.com/visionmedia/supertest/issues/314。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式