Docker Compose:搭建開發(fā)環(huán)境的好方式
最近,我考慮了很多關(guān)于這種個(gè)人開發(fā)環(huán)境的搭建方式,原因是,我現(xiàn)在把所有的計(jì)算工作都搬到了一個(gè)私有云上,大概 20 美元/月的樣子。這樣一來(lái),我就不用在工作的時(shí)候花時(shí)間去思考應(yīng)該如何管理幾千臺(tái) AWS 服務(wù)器了。
在此之前,我曾花了兩天的時(shí)間,嘗試使用其他的工具來(lái)嘗試搭建一個(gè)開發(fā)環(huán)境,搭到后面,我實(shí)在是心累了。相比起來(lái),Docker Compose 就簡(jiǎn)單易用多了,我非常滿意。于是,我和妹妹分享了我的 ??docker-compose?
? 使用經(jīng)歷,她略顯驚訝:“是吧!你也覺(jué)得 Docker Compose 真棒對(duì)吧!” 嗯,我覺(jué)得我應(yīng)該寫一篇博文把過(guò)程記錄下來(lái),于是就有了你們看到的這篇文章。
我們的目標(biāo)是:搭建一個(gè)開發(fā)環(huán)境
目前,我正在編寫一個(gè) Ruby on Rails 服務(wù)(它是一個(gè)計(jì)算機(jī)“調(diào)試”游戲的后端)。在我的生產(chǎn)服務(wù)器上,我安裝了:
- 一個(gè) Nginx 服務(wù)器
- 一個(gè) Rails 服務(wù)
- 一個(gè) Go 服務(wù)(使用了??gotty?? 來(lái)代理一些 SSH 連接)
- 一個(gè) Postgres 數(shù)據(jù)庫(kù)
在本地搭建 Rails 服務(wù)非常簡(jiǎn)單,用不著容器(我只需要安裝 Postgres 和 Ruby 就行了,小菜一碟)。但是,我還想要把匹配 ??/proxy/*?
? 的請(qǐng)求的發(fā)送到 Go 服務(wù),其他所有請(qǐng)求都發(fā)送到 Rails 服務(wù),所以需要借助 Nginx。問(wèn)題來(lái)了,在筆記本電腦上安裝 Nginx 對(duì)我來(lái)說(shuō)太麻煩了。
是時(shí)候使用 ??docker-compose?
? 了!
docker-compose 允許你運(yùn)行一組 Docker 容器
基本上,Docker Compose 的作用就是允許你運(yùn)行一組可以互相通信 Docker 容器。
你可以在一個(gè)叫做 ??docker-compose.yml?
?? 的文件中,配置你所有的容器。我在下方將貼上我為這個(gè)服務(wù)編寫的 ??docker-compose.yml?
? 文件(完整內(nèi)容),因?yàn)槲矣X(jué)得它真的很簡(jiǎn)潔、直接!
version: "3.3"
services:
db:
image: postgres
volumes:
- ./tmp/db:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: password # yes I set the password to 'password'
go_server:
# todo: use a smaller image at some point, we don't need all of ubuntu to run a static go binary
image: ubuntu
command: /app/go_proxy/server
volumes:
- .:/app
rails_server:
build: docker/rails
command: bash -c "rm -f tmp/pids/server.pid && source secrets.sh && bundle exec rails s -p 3000 -b '0.0.0.0'"
volumes:
- .:/app
web:
build: docker/nginx
ports:
- "8777:80" # this exposes port 8777 on my laptop
這個(gè)配置包含了兩種容器。對(duì)于前面兩個(gè)容器,我直接使用了現(xiàn)有的鏡像(??image: postgres?
?? 和 ??image: ubuntu?
??)。對(duì)于后面兩個(gè)容器,我不得不構(gòu)建一個(gè)自定義容器鏡像,其中, ??build: docker/rails?
?? 的作用就是告訴 Docker Compose,它應(yīng)該使用 ??docker/rails/Dockerfile?
? 來(lái)構(gòu)建一個(gè)自定義容器。
我需要允許我的 Rails 服務(wù)訪問(wèn)一些 API 密鑰和其他東西,因此,我使用了 ??source secrets.sh?
?,它的作用就是在環(huán)境變量中預(yù)設(shè)一組密鑰。
如何啟動(dòng)所有服務(wù):先 “build” 后 “up”
我一直都是先運(yùn)行 ??docker-compose build?
?? 來(lái)構(gòu)建容器,然后再運(yùn)行 ??docker-compose up?
? 把所有服務(wù)啟動(dòng)起來(lái)。
你可以在 yaml 文件中設(shè)置 ??depends_on?
?,從而進(jìn)行更多啟動(dòng)容器的控制。不過(guò),對(duì)于我的這些服務(wù)而言,啟動(dòng)順序并不重要,所以我沒(méi)有設(shè)置它。
網(wǎng)絡(luò)互通也非常簡(jiǎn)單
容器之間的互通也是一件很重要的事情。Docker Compose 讓這件事變得超級(jí)簡(jiǎn)單!假設(shè)我有一個(gè) Rails 服務(wù)正在名為 ??rails_server?
?? 的容器中運(yùn)行,端口是 3000,那么我就可以通過(guò) ??http://rails_server:3000?
? 來(lái)訪問(wèn)該服務(wù)。就是這么簡(jiǎn)單!
以下代碼片段截取自我的 Nginx 配置文件,它是根據(jù)我的使用需求配置的(我刪除了許多 ??proxy_set_headers?
? 行,讓它看起來(lái)更清楚):
location ~ /proxy.* {
proxy_pass http://go_server:8080;
}
location @app {
proxy_pass http://rails_server:3000;
}
或者,你可以參考如下代碼片段,它截取自我的 Rails 項(xiàng)目的數(shù)據(jù)庫(kù)配置,我在其中使用了數(shù)據(jù)庫(kù)容器的名稱(??db?
?):
development:
<<: *default
database: myproject_development
host: db # <-------- 它會(huì)被“神奇地”解析為數(shù)據(jù)庫(kù)容器的 IP 地址
username: postgres
password: password
至于 ??rails_server?
? 究竟是如何被解析成一個(gè) IP 地址的,我還真有點(diǎn)兒好奇。貌似是 Docker 在我的計(jì)算機(jī)上運(yùn)行了一個(gè) DNS 服務(wù)來(lái)解析這些名字。下面是一些 DNS 查詢記錄,我們可以看到,每個(gè)容器都有它自己的 IP 地址:
$ dig +short @127.0.0.11 rails_server
172.18.0.2
$ dig +short @127.0.0.11 db
172.18.0.3
$ dig +short @127.0.0.11 web
172.18.0.4
$ dig +short @127.0.0.11 go_server
172.18.0.5
是誰(shuí)在運(yùn)行這個(gè) DNS 服務(wù)?
我(稍微)研究了一下這個(gè) DNS 服務(wù)是怎么搭建起來(lái)的。
以下所有命令都是在容器外執(zhí)行的,因?yàn)槲覜](méi)有在容器里安裝很多網(wǎng)絡(luò)工具。
第一步::使用 ??ps aux | grep puma?
?,獲取 Rails 服務(wù)的進(jìn)程 ID。
找到了,它是 ??1837916?
?!簡(jiǎn)單~
第二步::找到和 ??1837916?
? 運(yùn)行在同一個(gè)網(wǎng)絡(luò)命名空間的 UDP 服務(wù)。
我使用了 ??nsenter?
?? 來(lái)在 ??puma?
?? 進(jìn)程的網(wǎng)絡(luò)命令空間內(nèi)運(yùn)行 ??netstat?
??(理論上,我猜想你也可以使用 ??netstat -tupn?
?? 來(lái)只顯示 UDP 服務(wù),但此時(shí),我的手指頭只習(xí)慣于打出 ??netstat -tulpn?
?)。
$ sudo nsenter -n -t 1837916 netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.11:32847 0.0.0.0:* LISTEN 1333/dockerd
tcp 0 0 0.0.0.0:3000 0.0.0.0:* LISTEN 1837916/puma 4.3.7
udp 0 0 127.0.0.11:59426 0.0.0.0:* 1333/dockerd
我們可以看到,此時(shí)有一個(gè)運(yùn)行在 ??59426?
?? 端口的 UDP 服務(wù),它是由 ??dockerd?
? 運(yùn)行的!或許它就是我們要找的 DNS 服務(wù)?
第三步:確定它是不是我們要找的 DNS 服務(wù)
我們可以使用 ??dig?
? 工具來(lái)向它發(fā)送一個(gè) DNS 查詢:
$ sudo nsenter -n -t 1837916 dig +short @127.0.0.11 59426 rails_server
172.18.0.2
奇怪,我們之前運(yùn)行 ??dig?
?? 的時(shí)候,DNS 查詢?cè)趺礇](méi)有發(fā)送到 ??59426?
?? 端口,而是發(fā)送到了 ??53?
? 端口呢?這到底是怎么回事呀?
第四步:iptables
對(duì)于類似“這個(gè)服務(wù)似乎正運(yùn)行在 X 端口上,但我卻在 Y 端口上訪問(wèn)到了它,這是什么回事呢?”的問(wèn)題,我的第一念頭都是“一定是 iptables 在作怪”。
于是,我在運(yùn)行了容器的網(wǎng)絡(luò)命令空間內(nèi)執(zhí)行 ??iptables-save?
?,果不其然,真相大白:
$ sudo nsenter -n -t 1837916 iptables-save
.... redacted a bunch of output ....
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p udp -m udp --sport 59426 -j SNAT --to-source :53
COMMIT
在輸出中有一條 iptables 規(guī)則,它將 ??53?
?? 端口的流量發(fā)送到了 ??59426?
? 上。哈哈,真有意思!
數(shù)據(jù)庫(kù)文件儲(chǔ)存在一個(gè)臨時(shí)目錄中
這樣做有一個(gè)好處:我可以直接掛載 Postgres 容器的數(shù)據(jù)目錄 ??./tmp/db?
?,而無(wú)需在我的筆記本電腦上管理 Postgres 環(huán)境。
我很喜歡這種方式,因?yàn)槲艺娴牟幌朐诠P記本電腦上獨(dú)自管理一個(gè) Postgres 環(huán)境(我也真的不知道該如何配置 Postgres)。另外,出于習(xí)慣,我更喜歡讓開發(fā)環(huán)境的數(shù)據(jù)庫(kù)和代碼放在同一個(gè)目錄下。
僅需一行命令,我就可以訪問(wèn) Rails 控制臺(tái)
管理 Ruby 的版本總是有點(diǎn)棘手,并且,即使我暫時(shí)搞定了它,我也總是有點(diǎn)擔(dān)心自己會(huì)把 Ruby 環(huán)境搞壞,然后就要修它個(gè)十年(夸張)。
(使用 Docker Compose)搭建好這個(gè)開發(fā)環(huán)境后,如果我需要訪問(wèn) Rails 控制臺(tái)console(一個(gè)交互式環(huán)境,加載了所有我的 Rails 代碼),我只需要運(yùn)行一行代碼即可:
$ docker-compose exec rails_server rails console
Running via Spring preloader in process 597
Loading development environment (Rails 6.0.3.4)
irb(main):001:0>
好耶!
小問(wèn)題:Rails 控制臺(tái)的歷史記錄丟失了
我碰到了一個(gè)問(wèn)題:Rails 控制臺(tái)的歷史記錄丟失了,因?yàn)槲乙恢痹诓粩嗟刂貑⑺?/p>
不過(guò),我也找到了一個(gè)相當(dāng)簡(jiǎn)單的解決方案(嘿嘿):我往容器中添加了一個(gè) ??/root/.irbrc?
? 文件,它能夠把 IRB 歷史記錄文件的保存位置指向一個(gè)不受容器重啟影響的地方。只需要一行代碼就夠啦:
IRB.conf[:HISTORY_FILE] = "/app/tmp/irb_history"
我還是不知道它在生產(chǎn)環(huán)境的表現(xiàn)如何
到目前為止,這個(gè)項(xiàng)目的生產(chǎn)環(huán)境搭建進(jìn)度,還停留在“我制作了一個(gè) DigitalOcean droplet(LCCT 譯注:一種 Linux 虛擬機(jī)服務(wù)),并手工編輯了很多文件”的階段。
嗯……我相信以后會(huì)在生產(chǎn)環(huán)境中使用 docker-compose 來(lái)運(yùn)行一下它的。我猜它能夠正常工作,因?yàn)檫@個(gè)服務(wù)很可能最多只有兩個(gè)用戶在使用,并且,如果我愿意,我可以容忍它在部署過(guò)程中有 60 秒的不可用時(shí)間。不過(guò)話又說(shuō)回來(lái),出錯(cuò)的往往是我想不到的地方。
推特網(wǎng)友提供了一些在生產(chǎn)中使用 docker-compose 的注意事項(xiàng):
- ?
?docker-compose up?
? 只會(huì)重啟那些需要重啟的容器,這會(huì)讓重啟速度更快。 - 有一個(gè) Bash 小腳本??wait-for-it??,你可以用它來(lái)保持等待一個(gè)容器,直到另一個(gè)容器的服務(wù)可用。
- 你可以準(zhǔn)備兩份?
?docker-compose.yaml?
?? 文件:用于開發(fā)環(huán)境的??docker-compose.yaml?
?? 和用于生產(chǎn)環(huán)境的??docker-compose-prod.yaml?
??。我想我會(huì)在分別為 Nginx 指定不同的端口:開發(fā)時(shí)使用??8999?
??,生產(chǎn)中使用??80?
?。 - 人們似乎一致認(rèn)為,如果你的項(xiàng)目是一臺(tái)計(jì)算機(jī)上運(yùn)行的小網(wǎng)站,那么 docker-compose 在生產(chǎn)中不會(huì)有問(wèn)題。
- 有個(gè)人建議說(shuō),如果愿意在生產(chǎn)環(huán)境搭建復(fù)雜那么一丟丟,Docker Swarm 就或許會(huì)是更好的選擇,不過(guò)我還沒(méi)試過(guò)(當(dāng)然,如果要這么說(shuō)的話,干嘛不用 Kubernetes 呢?Docker Compose 的意義就是它超級(jí)簡(jiǎn)單,而 Kubernetes 肯定不簡(jiǎn)單 : ))。
Docker 似乎還有一個(gè)特性,它能夠 ??把你用 docker-compose 搭建的環(huán)境,自動(dòng)推送到彈性容器服務(wù)(ESC)上??,聽上去好酷的樣子,但是我還沒(méi)有試過(guò)。
docker-compose 會(huì)有不適用的場(chǎng)景嗎
我聽說(shuō) docker-compose 在以下場(chǎng)景的表現(xiàn)較差:
- 當(dāng)你有很多微服務(wù)的時(shí)候(還是自己搭建比較好)
- 當(dāng)你嘗試從一個(gè)很大的數(shù)據(jù)庫(kù)中導(dǎo)入數(shù)據(jù)時(shí)(就像把幾百 G 的數(shù)據(jù)存到每個(gè)人的筆記本電腦里一樣)
- 當(dāng)你在 Mac 電腦上運(yùn)行 Docker 時(shí)。我聽說(shuō) Docker 在 macOS 上比在 Linux 上要慢很多(我猜想是因?yàn)樗枰鲱~外的虛擬化)。我沒(méi)有 Mac 電腦,所以我還沒(méi)有碰到這個(gè)問(wèn)題。
以上就是全部?jī)?nèi)容啦!
在此之前,我曾花了一整天時(shí)間,嘗試使用 Puppet 來(lái)配置 Vagrant 虛擬機(jī),然后在這個(gè)虛擬機(jī)里配置開發(fā)環(huán)境。結(jié)果,我發(fā)現(xiàn)虛擬機(jī)啟動(dòng)起來(lái)實(shí)在是有點(diǎn)慢啊,還有就是,我也不喜歡編寫 Puppet 配置(哈哈,沒(méi)想到吧)。
幸好,我嘗試了 Docker Compose,它真好簡(jiǎn)單,馬上就可以開始工作啦!