寫給前端的Docker實戰(zhàn)教程
本篇文章詳細(xì)而又簡短的介紹了:一名完全不了解 Docker 前端程序員,將全站 Docker 化的過程。內(nèi)容主要包含:
- Docker 基本概念
- 真實站點遷移過程:
- 靜態(tài)站點
- Nodejs 站點(Express)
- WordPress(PHP)
- 一些必備技巧:開機啟動、常用Shell
文章會講解使用 Docker 過程中用到的全部技術(shù)棧(Github CI、Nginx 反向代理、docker-compose),絕不會出現(xiàn)“詳見:http://xxx”甩鏈接的情況。
無需再查閱其他文檔,看著一篇就夠了!
當(dāng)前有哪些問題
手動部署成本太高
筆者維護了諸多網(wǎng)站,其中包含:
- 我的簡歷:pea3nut.info,使用 Vuejs 構(gòu)建的SPA單頁應(yīng)用,純靜態(tài)。
- 我的博客:pea3nut.blog,使用著名的 WordPress 搭建(PHP+Apache+MySQL)。
- 一個開源項目——Pxer:pxer.pea3nut.org,官網(wǎng)使用 Nodejs + Express SSR 搭建。
而每次我想修改某個網(wǎng)站內(nèi)容是十分麻煩的。拿大家熟悉的純靜態(tài)站點來說,修改過程如下:
- 下載:從Github下載代碼,然后本地npm install
- 開發(fā):npm run dev本地修改代碼,測試
- 編譯:npm run build使用 Webpack 進行編譯,產(chǎn)出靜態(tài)資源
- 上傳:打開FTP軟件,上傳替換文件
- 測試:看看網(wǎng)站是否在線上工作正常
- 提交:將代碼提交到 Github
哪怕我只是修改個錯別字,都要十幾分鐘。
網(wǎng)站太多,改動太頻繁,而每次不管多小的改動都很麻煩。簡直讓我感覺自己在維護一個萬級QPS的大型項目。
某個服務(wù)掛了,我不懂 Linux 無法排查
最近我發(fā)現(xiàn)我的 MySQL 進程總是掛掉,導(dǎo)致所有依賴于 MySQL 的站點都掛了。
我也不知道為什么,之前還是好好的。
我嘗試了重啟進程、重啟服務(wù)器、撈報錯日志百度,均未奏效。
好吧,其實我不太懂 Linux,也不太懂 MySQL,我只是想用下他們搭建 WordPress 站點。而最近總出問題,讓我意識到:
我不僅要維護站點,我還要維護環(huán)境
這個對于一名前端來說太難了,裝個 nvm 就已經(jīng)是我的極限了。MySQL無緣無故掛掉,我根本沒有能力查出個一二三四,然后解決它。
我不僅要保證站點本地能跑通,還必須要部署在遠程VPS穩(wěn)定運行。。。
重啟不行。。。那就只能重裝系統(tǒng)了
可是,由于搭建了許多站點,VPS服務(wù)器環(huán)境相當(dāng)復(fù)雜(或許這就是 MySQL 掛掉的原因),光 Apache 配置文件都幾百行了。重做系統(tǒng)的遷移成本,光是想一想就耗光了我所有的勇氣。
新的技術(shù)方案——Docker
總結(jié)一下有如下問題:
- 手動部署成本太高,改錯別字都很麻煩
- 一臺服務(wù)器由于時間累積導(dǎo)致環(huán)境變得“臟亂差”
- 重裝系統(tǒng)成本太高,難以遷移
而 Docker,正是我解決所有問題SCP-500萬能藥!
那么 Docker 是如何做的呢?
鏡像與容器
Docker 中有兩個重要概念。
一個是容器(Container):容器特別像一個虛擬機,容器中運行著一個完整的操作系統(tǒng)??梢栽谌萜髦醒b Nodejs,可以執(zhí)行npm install,可以做一切你當(dāng)前操作系統(tǒng)能做的事情。
另一個是鏡像(Image):鏡像是一個文件,它是用來創(chuàng)建容器的。如果你有裝過 Windows 操作系統(tǒng),那么 Docker 鏡像特別像“Win7純凈版.rar”文件。
上邊就是你所需要了解的 Docker 全部基礎(chǔ)知識。就這么簡單。
順便一提,在 Docker 中,我們通常稱你當(dāng)前使用的真實操作系統(tǒng)為“宿主機”(Host)。
安裝 Docker
安裝 Docker 在你的電腦上就像安裝 VS Code 一樣簡單。
如果你使用的是Windows電腦,需要購買支持虛擬化的版本。如Win10專業(yè)版,Win10家庭版是不行的。
- Mac:https://download.docker.com/mac/stable/Docker.dmg
- Windows:https://download.docker.com/win/stable/Docker%20for%20Windows%20Installer.exe
- Linux:https://get.docker.com/
安裝完Docker后,你可能會發(fā)現(xiàn)自己可以打開一個漂亮的 Docker 窗口。其實這個窗口沒什么用處,通常我們都是通過CLI命令行的方式操作 Docker的,就像 Git 一樣。
運行 Docker
接下來我們搭建一個能夠托管靜態(tài)文件的 Nginx 服務(wù)器
容器運行程序,而容器哪來的呢?容器是鏡像創(chuàng)建出來的。那鏡像又是哪來的呢?
鏡像是通過一個 Dockerfile 打包來的,它非常像我們前端的package.json文件
所以創(chuàng)建關(guān)系為:
- Dockerfile: 類似于“package.json”
- |
- V
- Image: 類似于“Win7純凈版.rar”
- |
- V
- Container: 一個完整操作系統(tǒng)
創(chuàng)建文件
我們創(chuàng)建一個目錄hello-docker,在目錄中創(chuàng)建一個index.html文件,內(nèi)容為:
- <h1>Hello docker</h1>
然后再在目錄中創(chuàng)建一個Dockerfile文件,內(nèi)容為:
- FROM nginx
- COPY ./index.html /usr/share/nginx/html/index.html
- EXPOSE 80
此時,你的文件結(jié)構(gòu)應(yīng)該是:
- hello-docker
- |____index.html
- |____Dockerfile
打包鏡像
文件創(chuàng)建好了,現(xiàn)在我們就可以根據(jù)Dockerfile創(chuàng)建鏡像了!
在命令行中(Windows優(yōu)先使用PowerShell)鍵入:
- cd hello-docker/ # 進入剛剛的目錄
- docker image build ./ -t hello-docker:1.0.0 # 打包鏡像
- 注意!Docker 中的選項(Options)放的位置非常有講究,docker —help image和docker image —help是完全不同的命令
docker image build ./ -t hello-docker:1.0.0的意思是:基于路徑./(當(dāng)前路徑)打包一個鏡像,鏡像的名字是hello-docker,版本號是1.0.0。該命令會自動尋找Dockerfile來打包出一個鏡像
- Tips: 你可以使用docker images來查看本機已有的鏡像
不出意外,你應(yīng)該能得到如下輸出:
- Sending build context to Docker daemon 3.072kB
- Step 1/3 : FROM nginx
- ---> 5a3221f0137b
- Step 2/3 : COPY ./index.html /usr/share/nginx/html/index.html
- ---> 1c433edd5891
- Step 3/3 : EXPOSE 80
- ---> Running in c2ff9ec2e945
- Removing intermediate container c2ff9ec2e945
- ---> f6a472c1b0a0
- Successfully built f6a472c1b0a0
- Successfully tagged hello-docker:1.0.0
可以看到其運行了 Dockerfile 中的內(nèi)容,現(xiàn)在我們簡單拆解下:
- FROM nginx:基于哪個鏡像
- COPY ./index.html /usr/share/nginx/html/index.html:將宿主機中的./index.html文件復(fù)制進容器里的/usr/share/nginx/html/index.html
- EXPOSE 80:容器對外暴露80端口
運行容器
我們剛剛使用 Dockerfile 創(chuàng)建了一個鏡像?,F(xiàn)在有鏡像了,接下來要根據(jù)鏡像創(chuàng)建容器:
- docker container create -p 2333:80 hello-docker:1.0.0
- docker container start xxx # xxx 為上一條命令運行得到的結(jié)果
然后在瀏覽器打開127.0.0.1:2333,你應(yīng)該能看到剛剛自己寫的index.html內(nèi)容
在上邊第一個命令中,我們使用docker container create來創(chuàng)建基于hello-docker:1.0.0鏡像的一個容器,使用-p來指定端口綁定——將容器中的80端口綁定在宿主機的2333端口。執(zhí)行完該命令,會返回一個容器ID
而第二個命令,則是啟動這個容器
啟動后,就能通過訪問本機的2333端口來達到訪問容器內(nèi)80端口的效果了
- Tips: 你可以使用docker containers ls來查看當(dāng)前運行的容器
當(dāng)容器運行后,可以通過如下命令進入容器內(nèi)部:
- docker container exec -it xxx /bin/bash # xxx 為容器ID
原理實際上是啟動了容器內(nèi)的/bin/bash,此時你就可以通過bash shell與容器內(nèi)交互了。就像遠程連接了SSH一樣
發(fā)生了什么
我們總結(jié)下都發(fā)生了什么:
- 寫一個 Dockerfile
- 使用docker image build來將Dockerfile打包成鏡像
- 使用docker container create來根據(jù)鏡像創(chuàng)建一個容器
- 使用docker container start來啟動一個創(chuàng)建好的容器
雖然很簡單,但是也沒有感覺到“廣闊天地,大有可為,為所欲為”呢?
遷移靜態(tài)站點
接下來我們實戰(zhàn)遷移一個由 Vuejs 寫的純靜態(tài) SPA 單頁站點:
- 網(wǎng)址:pea3nut.info
- 源碼:github/pea3nut-info
我打算怎么做
在沒遷移 Docker 之前,若我想更新線上網(wǎng)站中內(nèi)容時,需要:
- 本地npm run build打包產(chǎn)出靜態(tài)文件
- 手動通過 FTP 上傳到服務(wù)器
- git push更新 Github 源碼
稍微有點麻煩,因此我打算這樣改:
- 執(zhí)行g(shù)it push
- 自動檢測到 github 有代碼更新,自動打包出一個 Docker 鏡像
- CI 編譯完成后,SSH 登錄 VPS,刪掉現(xiàn)有容器,用新鏡像創(chuàng)建一個新容器
而這樣做的好處是:
- 不必再手動 FTP 上傳文件
- 當(dāng)我進行修改錯別字這樣的簡單操作時,可以免測。改完直接git push,而不必本地npm run build
Github中的CI
首先是讓 Github 在我每次更新代碼時打包出一個鏡像
在 Github,可以有免費的 CI 資源用,它就是 Travis CI
在項目中根目錄中添加.travis.yml文件,內(nèi)容如下:
- language: node_js
- node_js:
- - "12"
- services:
- - docker
- before_install:
- - npm install
- script:
- - npm run build
- - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
- - docker build -t pea3nut/pea3nut-info:latest .
- - docker push pea3nut/pea3nut-info:latest
文件內(nèi)容非常簡單,就是使用npm run build編譯靜態(tài)產(chǎn)出后,打包一個鏡像并且 push 到遠程。有幾點需要詳細(xì)說一下:
- 為了能夠讓鏡像上傳到服務(wù)器,你需要在hub.docker.com中注冊一個賬號,然后替換代碼中的pea3nut/pea3nut-info:latest為用戶名/包名:latest即可
- 使用 Github 登錄 Travis CI 后,在左邊點擊+加號添加自己的 Github 倉庫后,需要移步到 Setting 為項目添加DOCKER_USERNAME和DOCKER_PASSWORD環(huán)境變量。這樣保證我們可以秘密的登錄 Docker Hub 而不被其他人看到自己的密碼。如下圖:
然后需要添加 Dockerfile 文件來描述如何打包 Docker 鏡像。
按照.travis.yml的命令次序,在打包鏡像時,npm run build已經(jīng)執(zhí)行過了,項目產(chǎn)出已經(jīng)有了。不必在 Docker 容器中運行npm install和npm run build之類的,直接復(fù)制文件即可:
- FROM nginx
- COPY ./dist/ /usr/share/nginx/html/
- EXPOSE 80
- Note: 過程雖然簡單但是線條很長,建議本地多測試測試再進行g(shù)it push
若你編譯出的靜態(tài)站點也是一個 SPA 單頁應(yīng)用,需要增加額外的 Nginx 配置來保證請求都能打到index.html。下邊是我寫的vhost.nginx.conf Nginx 配置文件,將不訪問文件的請求全部重定向到/index.html:
- server {
- listen 80;
- server_name localhost;
- location / {
- root /usr/share/nginx/html;
- index index.html index.htm;
- proxy_set_header Host $host;
- if (!-f $request_filename) {
- rewrite ^.*$ /index.html break;
- }
- }
- error_page 500 502 503 504 /50x.html;
- location = /50x.html {
- root /usr/share/nginx/html;
- }
- }
然后在 Dockerfile 中新加一行,將本機的vhost.nginx.conf文件復(fù)制到容器的/etc/nginx/conf.d/pea3nut-info.conf,讓 Nginx 能夠讀取該配置文件:
- FROM nginx
- COPY ./dist/ /usr/share/nginx/html/
- + COPY ./vhost.nginx.conf /etc/nginx/conf.d/pea3nut-info.conf
- EXPOSE 80
然后執(zhí)行g(shù)it push后,你可以在 Travis CI 看到 CI 的編譯結(jié)果。如果編譯沒問題,遠程實際上就有了pea3nut/pea3nut-info:latest這個鏡像。本地可以試試看該鏡像工作是否正常:
- docker image pull pea3nut/pea3nut-info:latest
- docker container create -p 8082:80 pea3nut/pea3nut-info:latest
- docker container start xxx # xxx 為上一條命令執(zhí)行的返回值
運行完成后,瀏覽器訪問127.0.0.1:8082應(yīng)該就能看到效果了!
然后你可以登錄遠程 VPS 服務(wù)器,安裝 Docker,執(zhí)行同樣的命令。然后訪問遠程 VPS 服務(wù)器的公網(wǎng) IP + 8082 端口號,應(yīng)該能看到和本地相同的效果。
Tips: 忘了如何在 VPS 上安裝 Docker?在上文“安裝 Docker”一節(jié),你可能需要的是 Linux 的安裝方式。
- curl https://get.docker.com/ > install-docker.sh # 下載安裝腳本
- sh install-docker.sh # 執(zhí)行安裝腳本
Nginx 反向代理
Note: 接下來的操作都是在你的遠程 VPS 服務(wù)器上操作,并非本地電腦,或者容器中。
目前我們將容器掛到了 8082 端口,但是線上不可能讓用戶手動輸入 8082 端口進行訪問。而如果將容器直接掛到 80 端口,雖然這樣用戶可以直接不加端口直接訪問,但是如果有第二個容器,或者更多容器呢?
這時候就需要在宿主機跑一個 Nginx,由它來獨占 80 端口,然后根據(jù)域名來講請求分發(fā)給響應(yīng)的容器。如下圖:
這種方案叫做“反向代理”
登錄VPS服務(wù)器,安裝 Nginx。因為我是 Ubuntu,所以可以用apt安裝。其他 Linux 發(fā)行版可以百度下安裝方法,通常2行內(nèi)可以搞定:
- apt update # 更新軟件包
- apt-get install nginx # 安裝 Nginx
- systemctl status nginx # 查看 Nginx 狀態(tài)
此時本地通過瀏覽器訪問 VPS 的公網(wǎng) IP 可用看到 Nginx 的歡迎頁面。
然后在 VPS 服務(wù)器的/etc/nginx/conf.d/中建立一個vhost.conf文件,配置如下內(nèi)容:
- server {
- listen 80;
- server_name pea3nut.info;
- location / {
- proxy_pass http://127.0.0.1:8082;
- }
- }
配置的意思是,監(jiān)聽來自 80 端口的流量,若訪問域名是pea3nut.info(替換為你自己的域名),則全部轉(zhuǎn)發(fā)到http://127.0.0.1:8082中。
配置完成后,重啟 Nginx 服務(wù)器。若是 Ubuntu 可以使用systemctl restart nginx命令,不同 Linux 發(fā)行版稍有不同。
配置成功后,訪問pea3nut.info會看到和VPS公網(wǎng)IP:8082相同的效果。
更新站點
而遷移完成 Docker 后,我想改一個錯別字的流程變?yōu)椋?/p>
- 本地修改完成,執(zhí)行g(shù)it push
- 等待 CI 編譯完成
- 登錄 VPS 服務(wù)器,執(zhí)行:
- docker image pull pea3nut/pea3nut-info:latest
- docker container create -p 8082:80 pea3nut/pea3nut-info:latest # 得到 yyy
- docker container stop xxx # xxx 為當(dāng)前運行的容器ID,可用 docker container ls 查看
- docker container start yyy # yyy 第二條命令返回值
命令還是有些長?我們在下面會進一步優(yōu)化它
遷移 Nodejs 站點(Express)
接下來我們實戰(zhàn)遷移一個由 Nodejs 寫的 Express SSR 站點
- 網(wǎng)址:pxer.pea3nut.org
- 源碼:github/pxer-homepage
我打算怎么做
網(wǎng)站使用 Ejs 模板渲染頁面。在沒遷移 Docker 之前,若我想更新線上網(wǎng)站中內(nèi)容時,需要:
- 本地修改好 Ejs 或者其他文件
- 手動通過 FTP 上傳到服務(wù)器
- 在服務(wù)器端重啟 Nodejs 進程。若有 npm 包依賴改動,需要在VPS服務(wù)器上手動執(zhí)行npm install
- git push更新 Github 源碼
稍微有點麻煩,因此我打算這樣改:
- 執(zhí)行g(shù)it push
- 自動檢測到 github 有代碼更新,自動打包出一個 Docker 鏡像
- CI 編譯完成后,SSH 登錄 VPS,刪掉現(xiàn)有容器,用新鏡像創(chuàng)建一個新容器
而這樣做的好處是:
- 不必再手動 FTP 上傳文件
- 不必手動維護服務(wù)器的 Nodejs 運行環(huán)境
實施
具體的過程和處理靜態(tài)站點沒有什么特別的區(qū)別,無非是:
- 編寫 Dockerfile 文件
- 在 CI 時自動打包鏡像
- 在VPS增加一個 Nginx 反向代理
這次就不重復(fù)講了,具體的配置可以參考項目中的相關(guān)文件
Tips: 你可能發(fā)現(xiàn)了 Dockerfile 中的ENTRYPOINT命令必須指定一個前臺進程。若你的 Nodejs 應(yīng)用是使用 PM2 進行保活的,你需要替換pm2 start app.js為pm2-docker app.js
docker-compose
當(dāng)將 Nodejs 站點遷移完成,我們的 VPS 服務(wù)器上已經(jīng)運行了2個容器。每次鏡像更新都要手動的docker container create帶一堆參數(shù)是比較麻煩的,尤其是當(dāng)日后容器日益增多的時候。而這時,就輪到docker-compose登場了~
docker-compose 是 Docker 官方提供的一個 Docker 管理工具。若你是通過桌面端的 Docker 安裝包安裝的 Docker,它是會默認(rèn)為你安裝 docker-compose 的。可以試試如下命令:
- docker-compose --help
如果是在 Linux,可以通過如下命令安裝 docker-compose:
- curl -L "https://github.com/docker/compose/releases/download/1.23.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
- chmode +x /usr/local/bin/docker-compose
docker-compose 和 Docker 差不多,也是只要一份文件就能跑起來。docker-compose 主要的作用就是能夠讓你不必手敲那么多 Docker 命令
建立一個目錄,然后在目錄中建立docker-compose.yml,內(nèi)容如下:
- version: "3.7" # 這個是配置文件的版本,不同的版本號聲明方式會有細(xì)微的不同
- services:
- info:
- container_name: pea3nut-info
- image: pea3nut/pea3nut-info:latest
- ports:
- - "8082:80"
- restart: on-failure
然后在目錄中鍵入如下命令就能將服務(wù)跑起來:
- docker-compose up info
docker-compose 會幫我們自動去拉鏡像,創(chuàng)建容器,將容器中的80端口映射為宿主機的8082端口。restart字段還要求 docker-compose 當(dāng)發(fā)現(xiàn)容器意外掛掉時重新啟動容器,類似于 pm2,所以你不必再在容器內(nèi)使用 pm2。
如果想要更新一個鏡像創(chuàng)建新容器,只需要:
- docker-compose pull info
- docker-compose stop info
- docker-compose rm info
- docker-compose up -d info # -d 代表后臺運行
筆者已將自己網(wǎng)站部署方式開源,可參考github/pea3nut-hub
遷移 WordPress 站點(Apache + PHP + MySQL)
接下來我們實戰(zhàn)遷移一個 WordPress 站點
- 網(wǎng)址:pea3nut.blog
- 源碼:非公開
可能你也發(fā)現(xiàn)了這個站點和其他站點的一個非常大的不同——他的源碼和數(shù)據(jù)是不能公開的。
之前我們打包鏡像時,都是直接將代碼打進鏡像內(nèi)的。這條方案用在這里顯然是不行的,有兩個問題:
- 我不想公開 MySQL 數(shù)據(jù)文件和網(wǎng)站內(nèi)容(如圖片)。若將這些打包進鏡像,任何人都能docker image pull下載到鏡像,然后取得鏡像內(nèi)的文件。
- 當(dāng)容器被刪掉,存儲的 MySQL 數(shù)據(jù)都將丟失。
Volume
Docker 提供了一個叫做 Volume 的東西,可以將容器內(nèi)和宿主機的某個文件夾進行”綁定“,任何文件改動都會得到同步。所以,我可以將整個站點目錄和 MySQL 目錄都掛載為 Volume。這樣,當(dāng)容器刪除時,所有數(shù)據(jù)文件和源碼都會保留。
在本地建立./blog/mysql-data目錄存儲 MySQL 數(shù)據(jù),建立./blog/wordpress目錄存儲 WordPress 源碼。然后修改docker-compose.yml如下:
- version: "3.7"
- services:
- info:
- container_name: pea3nut-info
- image: pea3nut/pea3nut-info:latest
- ports:
- - "8082:80"
- restart: on-failure
- + blog:
- + container_name: pea3nut-blog
- + image: tutum/lamp:latest
- + ports:
- + - "8081:80"
- + volumes:
- + - ./blog/mysql-data:/var/lib/mysql
- + - ./blog/wordpress:/app
- + restart: on-failure
可以看到這次根本沒有打包鏡像,而是直接使用tutum/lamp鏡像提供的 LAMP 環(huán)境(Linux + Apache + MySQL + PHP),然后將 MySQL 數(shù)據(jù)目錄/var/lib/mysql和源碼目錄/app都掛載出來就可以了。
Tips: 通過 Volume 我們只是解決了部署問題,而如何本地開發(fā)然后將源碼同步到服務(wù)器呢?用 FTP 當(dāng)然是可以的,但是稍微有點麻煩。其實你可以自建一個 Git 服務(wù)器!詳見:pea3nut.blog/e127
坑和其他技巧
- 設(shè)置開機啟動:Ubuntu 18.04 啟用 rc.local 設(shè)置開機啟動 - digdeep - 博客園
- 遷移后中文文件亂碼:解決linux下中文文件名顯示亂碼問題