首先,祝各位新年快樂,萬事如意,雞年大吉。
這次要來說說一個和前端并不太相關的東西——docker compose,一個整合發布應用的利器。
如果,你對 docker 有一些耳聞,那么,你可能知道它是什么。
不過,你不了解也沒有關系,在作者眼中,docker 就類似于一個沙箱,而你的應用起在這個沙箱里,不受服務器系統環境的影響,同時也不污染服務器,配置完成之后往服務器部署或移除應用都相當方便。
而 compose 就如同它的字面意思組合,它就好像是一個大箱子,可以把幾個不相關的沙箱給組合起來,變成一個整體,就如同小時候動畫片中變形金剛的合體變身。

理論知識就沒有什么比官方文檔更好的了,這里就不講了,主要來看看如何應用。本文主要包含以下幾個部分:
如果,你只對前端技術感興趣,那么,這篇文章可能不適合你。
常言道:一個不懂運維的設計,不是一個好前端。
<a name="Install"></a>
安裝
Windows 和 Mac 裝了 Docker 之后已經自帶 docker-compose,其他環境根據 Docker 官網介紹,簡單幾步也能完成安裝。
這里要提一下,在亞馬遜 aws 上安裝 docker-compose,由于沒有 root 權限會遇到官網上所提到的 Permission denied
錯誤,加了 sudo 也是無法直接下載到 /usr/local/bin 目錄下的。
硬來不行,還可以曲線救國嘛~
先將文件下載到 aws 服務器上,再將文件移動到 /usr/local/bin
目錄就可以了。
curl -L https://github.com/docker/compose/releases/download/1.9.0/docker-compose-`uname -s`-`uname -m` > docker-compose
sudo chown root docker-compose
sudo mv docker-compose /usr/local/bin
sudo chmod +x /usr/local/bin/docker-compose
驗證是否安裝成功,試試 docker-compose version
。如果有輸出版本信息,就說明 docker-compose 已經安裝好了。
docker-compose 雖然安裝好了,但并不一定能用,因為 docker 和 docker-compose 是分開安裝,即使它倆各自運行正常,在一起就不一定合拍了。
那怎么知道它倆合不合拍?答案很簡單,hello world~
<a name="HelloWorld"></a>
Hello world
在任意的目錄下,創建一個 docker-compose.yml 文件,并添加下面的內容。
version: '2'
services:
helloworld:
image: 'hello-world'
然后,在當前目錄下使用 docker-compose up
啟動 docker-compose。
啟動時,如遇到
client and server don't have same version (client : 1.22, server: 1.18)
類似這樣的錯誤,可以通過設置 docker-compose 的 api 版本來解決。
COMPOSE_API_VERSION=auto
不要嘗試通過一次次安裝不同的 docker-compose 版本來解決,你會 ?? 的。如果,還遇到
docker.errors.InvalidVersion: inspect_network is not available for version < 1.21
這是 Ubuntu 14.04 LTS 默認的 docker 版本太低引起的,需要升級 docker。然而,在 aws 的服務器上升級 docker 版本時,需要先創建 /etc/apt/sources.list.d/docker.list
文件,并添加
deb https://packages.docker.com/1.12/apt/repo ubuntu-trusty main
再運行
sudo apt-get update && sudo apt-get upgrade docker-engine
就能升級成功。看到??這樣的結果,就表示 docker 和 docker-compose 都安裝成功,而且它倆很搭。

<a name="Command"></a>
常用命令
docker-compose 的命令很簡單,它已經將一些 docker 常用關于 image, container & volume 的命令都整合在了一起,使發布變得極其簡單。比如,之前剛剛提到的 docker-compose up
,就類似于 docker build & run,用來創建并啟動 container。
其他常用的命令有:
-
build
:構建或重新構建 services -
config
:驗證 docker-compose 配置文件 -
create
:創建 services -
down
:與up
相對,停止并刪除 container, image, volumn 等 -
kill
:殺死某個 container -
logs
:查看 container 日志 -
ps
:查看 container 信息 -
restart
:重啟 services -
rm
:刪除已經停止的 container -
start
:啟動 services -
stop
:停止 service -
version
:顯示 docker-compose 版本
是不是發現有幾個命令和 docker 的命令一樣?的確,但就如同之前的安裝過程一樣,docker-compose 是依賴于 docker 的,docker 命令更底層。比如 docker-compose ps
這個命令,它只會顯示由 docker-compose 啟動的容器信息,但不包含 docker 啟動的容器信息,相反 docker ps
可以查看由 docker-compose 啟動的容器信息。
還剩幾個命令沒有列出來,有興趣的童鞋可以通過 docker-compose help
命令或上官網查看更多信息。
光說不練假把式。docker-compose 究竟好不好用,只有用了才知道。
<a name="RealWorld"></a>
Real world
之前,個人博客的靜態資源一直都是通過 node 提供服務。這的確可以,但這不是 node 的強項。
專業的事交給專業的人去做。 - by S(ome)B(ody)
這個專業的人就是 nginx。
除此之外,2017 年起水果和古哥都強推 https,升級 https 也是箭在弦上(雖然一直有這個打算,也拖到了現在??彡(-_-;)彡)。
于是,程序不再是原先單一的 node 服務,而是,變成了一系列密切相關的服務。如果,通過基礎的 docker 命令來一個個啟動、停止服務的話,那么,就需要額外添加一個復雜的腳本來控制。
docker-compose 就是用來處理類似的問題。它可以做到通過一條命令來控制一個應用相關的一系列服務的啟動、停止等,并且不依賴于機器環境,作到隨時可以將應用遷移至其他的機器上發布。
知道了準備做什么,先看看最終設計的應用結構和之前的對比。

直接看這張圖可能有點蒙圈,沒事,一點點來看。
<a name="Transform"></a>
docker 到 docker-compose 的轉換
本文一開始就有提到,docker 可以看做是一個小箱子,而 docker-compose 是一個大箱子用來裝這些小箱子。
那么,如何將小箱子放入這個大箱子里哪?
非常簡單!只需告訴 docker-compose 如何啟動你的應用就可以了,那就先看看原先的啟動命令。
docker run -d -p 80:8080 --name blog
啟動命令中,主要配置了一個端口的映射 -p
,以及命名了容器名,用于方便地啟動、停止應用。清楚了這些,那么改成 docker-compose 的文件也就輕而易舉了。
version: '2'
services:
node:
build: .
container_name: node
ports:
- "80:8080"
docker 到 docker-compose 的轉換就這樣完成了,這些更新都不需要修改任何的業務邏輯或者打包配置。
試著使用 docker-compose up -d
啟動服務驗證看看。
啟動正常之后,還是一步步來,先引入 nginx。
<a name="Nginx"></a>
引入 Nginx
Nginx 是一個高性能的 Web 服務器,它具有配置簡單、運行穩定和負載均衡等特點,常被作為靜態資源服務器。(詳細的 Nginx 信息,請自行查詢資料,這方面本人也不是行家)
Nginx 在 docker hub 上有現成的官方鏡像,直接拿來用就可以了。
version: '2'
services:
# ...
nginx:
image: nginx:stable
container_name: nginx
ports:
- "80:80"
restart: always
此時,啟動服務會失敗并報錯,因為 nginx 和原有的 node 容器都綁定到了 80 端口。docker-comopse 各個容器之間是相互獨立的,容器內部的接口相互之間不影響,但對外暴露的接口不能相同,不然就會引起沖突。
從之前的結構圖可以看到,請求全部由 nginx 接受并轉發到 node 服務,也就是說,node 不直接對外提供服務。那么,docker-compose 中也就可以移除 ports 部分(這里便于測試 node 服務依舊暴露 8080 端口)。
其次,靜態文件是由 node 打包后生成的,也就是說需要將 node 服務中的數據共享給 nginx 服務,這就需要用到 volume(數據卷)。數據卷可以將數據在宿主機和容器之間、容器和容器之間共享,即使容器被刪除了,數據卷依舊存在。
這里就需要將服務器上的 nginx 配置文件和 node 構建之后的靜態文件共享給 nginx。
version: '2'
services:
node:
build: .
container_name: node
# node service port export for test
ports:
- "8080:8080"
volumes:
- ./log/node:/var/log/node
nginx:
image: nginx:stable
container_name: nginx
depends_on:
- node
volumes:
- ./config/nginx:/etc/nginx/conf.d:ro
- ./log/nginx:/var/log/nginx
volumes_from:
- node:ro
ports:
- "80:80"
restart: always
volume 是 docker 中相當重要及常用的一部分,理解它對使用 docker 解決問題有巨大的幫助。推薦一篇關于 docker volume 的文章,有助于理解 volume。
負載均衡
docker-compose 配置完了,再來看看 nginx 配置。本章一開始有提到 nginx 可以做負載均衡,那該如何配置哪?
在 nginx 中配置負載均衡相當簡單,只需在 upstream
里配置一下目標服務器。
然而,這里就會遇到一個問題。由于,容器之間是相互獨立的,于是,localhost 便無法在容器之間相互訪問。不過,由同一 docker-compose 所起的容器之間可以通過容器名相互訪問,這里就是
upstream node_server {
server node:8080 max_fails=2 fail_timeout=30s;
}
如果要額外再起一個服務,只需在 docker-compose 文件中再啟動一個容器(可以依賴同一套代碼),并將之前所配的 upstream
中額外多添加一條 server 信息,比如:
upstream node_server {
server node:8080 max_fails=2 fail_timeout=30s;
server node-backup:8080 max_fails=2 fail_timeout=30s;
}
這樣即使一個服務掛了,只要另一個服務還運行正常,nginx 會將請求轉發給運行正常的服務。一個最簡單的復雜均衡就做好了,所有這些都不需要修改任何功能性的代碼。
知道了 nginx 可以提供負載均衡,但也不要忘了老朋友 pm2。
pm2 通過命令行參數 -i,或配置文件通過起多個實例來做負載均衡(本人的小博客也是用的這個方式)。
引入 nginx 之后,將全站升級成 https 就輕而易舉了,只需在配置文件中標明證書及秘鑰文件的位置就可以了。接下去,就看看如何生成證書和秘鑰。
<a name="Letsencrypt"></a>
使用 Letsencrypt 生成 SSL 證書
獲取 ssl 證書的方式有許多種,有的買域名就送證書,這里介紹一下用 letsencrypt(現已更名為 certbot
)獲取免費 ssl 證書。
常言道:前人栽樹,后人乘涼。
同樣的,letsencrypt 在 docker hub 上也有現成的鏡像。鏡像有了,剩下的就只需根據不同的場景來生成證書。
certbot
支持 5 種生成證書的模式,分別是:apache
, nginx
, webroot
, standalone
和 manual
,分別用于不同的場景。這里 nginx 和 certbot 使用的是不同的鏡像,所以選用的模式是 webroot
。
選定了鏡像和模式,那么參照 certbot 的文檔就能夠簡單地生成證書了。
docker run -it --rm --name certbot \
-v /letsencrypt/etc/letsencrypt:/etc/letsencrypt \
-v /letsencrypt/lib/letsencrypt:/var/lib/letsencrypt \
-v /letsencrypt/challenge:/usr/share/nginx/html \
-v /var/log/letsencrypt:/var/log/letsencrypt \
deliverous/certbot \
certonly --webroot -w /usr/share/nginx/html
需要注意的是,在 webroot
模式下申請證書,需要向 certbot 證明服務器能被訪問。certbot 驗證程序會訪問 web root 目錄(這里是 /usr/share/nginx/html)來驗證。這里又要用到之前提到的 volume 將目錄共享給 nginx,讓 nginx 能夠訪問到目錄內部的文件。
server {
listen 80;
listen [::]:80;
server_name discipled.me;
# ...
# letsencrypt challenge file location
location /.well-known {
root /usr/share/nginx/html;
access_log /var/log/nginx/challenge-access.log main;
allow all;
}
...
}
修改 nginx 配置之后,別忘重啟 nginx 服務。
docker-compose restart nginx
重啟 nginx 之后,然后再運行上面生成證書的命令就能生成證書了。

看到 Congratulations!
,證書就生成成功了。
再一次修改 nginx 配置,添加 ssl 證書信息,并監聽 443 端口。
# redirect host http://domain to https://domain
server {
listen 80;
listen [::]:80;
server_name discipled.me;
# letsencrypt challenge file location
location /.well-known {
root /usr/share/nginx/html;
access_log /var/log/nginx/challenge-access.log main;
allow all;
}
location / {
return 301 https://discipled.me$request_uri;
}
}
# https://domain server
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name discipled.me;
charset utf-8;
gzip on;
gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css;
root /usr/app/build/client/;
ssl_certificate /etc/letsencrypt/live/discipled.me/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/discipled.me/privkey.pem;
location / {
try_files $uri @node;
}
location @node {
proxy_pass http://node_server;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
重啟 nginx 服務后,訪問網站就可以看到

小鎖加上,大功告成。
七牛的圖床用 https 還要實名認證,為了保護(pa)個(cha)人(shui)隱(biao)私,就暫時用 Github 來救一下急。(誰知道有啥好用的圖床麻煩推薦一下,像七牛一樣支持 qrsync 用腳本批量上傳的就最好了~先謝過...)
證書更新
letsencrypt 生成的證書有效期是 3 個月,所以,至少 3 個月內需要更新一次證書。
certbot 提供了 renew 命令可以方便地更新證書,使用 --dry-run
參數可以驗證證書更新命令是否正確。
docker run -it --rm --name certbot \
-v /letsencrypt/etc/letsencrypt:/etc/letsencrypt \
-v /letsencrypt/lib/letsencrypt:/var/lib/letsencrypt \
-v /letsencrypt/challenge:/usr/share/nginx/html \
-v /var/log/letsencrypt:/var/log/letsencrypt \
deliverous/certbot \
renew --dry-run
同樣,看到 Congratulations
說明證書更新成功了。
由于,本人每月都會發布文章并重啟服務,就可以把證書更新一起交由 docker-compose 管理。(這里偷了個懶,增加了證書同應用之間的耦合關系,還是建議大家證書是通過系統定時任務來更新,省得哪天忘更新證書,證書就過期了)。
<a name="Conclusion"></a>
最后
看一下最終的 docker-compose 配置文件和發布腳本。
# docker-compose.yml
version: '2'
services:
node:
build: .
image: "blog:${TAG_NAME}"
container_name: node
# node service port export for test
ports:
- "8080:8080"
volumes:
- ./log/node:/var/log/node
nginx:
image: nginx:stable
container_name: nginx
depends_on:
- node
- letsencrypt
volumes:
- ./config/nginx:/etc/nginx/conf.d:ro
- ./letsencrypt/etc/letsencrypt:/etc/letsencrypt
- ./letsencrypt/lib/letsencrypt:/var/lib/letsencrypt
- ./letsencrypt/challenge:/usr/share/nginx/html
- ./log/nginx:/var/log/nginx
volumes_from:
- node:ro
ports:
- "80:80"
- "443:443"
restart: always
letsencrypt:
image: deliverous/certbot
container_name: certbot
volumes:
- ./letsencrypt/etc/letsencrypt:/etc/letsencrypt
- ./letsencrypt/lib/letsencrypt:/var/lib/letsencrypt
- ./letsencrypt/challenge:/usr/share/nginx/html
- ./log/letsencrypt:/var/log/letsencrypt
command: renew
發布腳本主要用來更新代碼,以及獲取應用版本號。
# deploy.sh
# git operation
git reset HEAD --hard
git fetch
git pull
# TAG_NAME used to set docker image tag
export TAG_NAME=`git tag -l | sort -r | head -n 1`
# docker operation
docker-compose down --volumes
docker-compose up --build -d
其他配置可以上 github 查看。
一扯似乎又扯遠了,歡迎提意見和建議,順便再問一下有啥好的圖床推薦。