Docker Compose:搭建開發(fā)環(huán)境的好方式
最近,我考慮了很多關(guān)于這種個人開發(fā)環(huán)境的搭建方式,原因是,我現(xiàn)在把所有的計算工作都搬到了一個私有云上,大概 20 美元/月的樣子。這樣一來,我就不用在工作的時候花時間去思考應(yīng)該如何管理幾千臺 AWS 服務(wù)器了。
在此之前,我曾花了兩天的時間,嘗試使用其他的工具來嘗試搭建一個開發(fā)環(huán)境,搭到后面,我實在是心累了。相比起來,Docker Compose 就簡單易用多了,我非常滿意。于是,我和妹妹分享了我的 ??docker-compose?
? 使用經(jīng)歷,她略顯驚訝:“是吧!你也覺得 Docker Compose 真棒對吧!” 嗯,我覺得我應(yīng)該寫一篇博文把過程記錄下來,于是就有了你們看到的這篇文章。
我們的目標(biāo)是:搭建一個開發(fā)環(huán)境
目前,我正在編寫一個 Ruby on Rails 服務(wù)(它是一個計算機“調(diào)試”游戲的后端)。在我的生產(chǎn)服務(wù)器上,我安裝了:
- 一個 Nginx 服務(wù)器
- 一個 Rails 服務(wù)
- 一個 Go 服務(wù)(使用了??gotty?? 來代理一些 SSH 連接)
- 一個 Postgres 數(shù)據(jù)庫
在本地搭建 Rails 服務(wù)非常簡單,用不著容器(我只需要安裝 Postgres 和 Ruby 就行了,小菜一碟)。但是,我還想要把匹配 ??/proxy/*?
? 的請求的發(fā)送到 Go 服務(wù),其他所有請求都發(fā)送到 Rails 服務(wù),所以需要借助 Nginx。問題來了,在筆記本電腦上安裝 Nginx 對我來說太麻煩了。
是時候使用 ??docker-compose?
? 了!
docker-compose 允許你運行一組 Docker 容器
基本上,Docker Compose 的作用就是允許你運行一組可以互相通信 Docker 容器。
你可以在一個叫做 ??docker-compose.yml?
?? 的文件中,配置你所有的容器。我在下方將貼上我為這個服務(wù)編寫的 ??docker-compose.yml?
? 文件(完整內(nèi)容),因為我覺得它真的很簡潔、直接!
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
這個配置包含了兩種容器。對于前面兩個容器,我直接使用了現(xiàn)有的鏡像(??image: postgres?
?? 和 ??image: ubuntu?
??)。對于后面兩個容器,我不得不構(gòu)建一個自定義容器鏡像,其中, ??build: docker/rails?
?? 的作用就是告訴 Docker Compose,它應(yīng)該使用 ??docker/rails/Dockerfile?
? 來構(gòu)建一個自定義容器。
我需要允許我的 Rails 服務(wù)訪問一些 API 密鑰和其他東西,因此,我使用了 ??source secrets.sh?
?,它的作用就是在環(huán)境變量中預(yù)設(shè)一組密鑰。
如何啟動所有服務(wù):先 “build” 后 “up”
我一直都是先運行 ??docker-compose build?
?? 來構(gòu)建容器,然后再運行 ??docker-compose up?
? 把所有服務(wù)啟動起來。
你可以在 yaml 文件中設(shè)置 ??depends_on?
?,從而進行更多啟動容器的控制。不過,對于我的這些服務(wù)而言,啟動順序并不重要,所以我沒有設(shè)置它。
網(wǎng)絡(luò)互通也非常簡單
容器之間的互通也是一件很重要的事情。Docker Compose 讓這件事變得超級簡單!假設(shè)我有一個 Rails 服務(wù)正在名為 ??rails_server?
?? 的容器中運行,端口是 3000,那么我就可以通過 ??http://rails_server:3000?
? 來訪問該服務(wù)。就是這么簡單!
以下代碼片段截取自我的 Nginx 配置文件,它是根據(jù)我的使用需求配置的(我刪除了許多 ??proxy_set_headers?
? 行,讓它看起來更清楚):
location ~ /proxy.* {
proxy_pass http://go_server:8080;
}
location @app {
proxy_pass http://rails_server:3000;
}
或者,你可以參考如下代碼片段,它截取自我的 Rails 項目的數(shù)據(jù)庫配置,我在其中使用了數(shù)據(jù)庫容器的名稱(??db?
?):
development:
<<: *default
database: myproject_development
host: db # <-------- 它會被“神奇地”解析為數(shù)據(jù)庫容器的 IP 地址
username: postgres
password: password
至于 ??rails_server?
? 究竟是如何被解析成一個 IP 地址的,我還真有點兒好奇。貌似是 Docker 在我的計算機上運行了一個 DNS 服務(wù)來解析這些名字。下面是一些 DNS 查詢記錄,我們可以看到,每個容器都有它自己的 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
是誰在運行這個 DNS 服務(wù)?
我(稍微)研究了一下這個 DNS 服務(wù)是怎么搭建起來的。
以下所有命令都是在容器外執(zhí)行的,因為我沒有在容器里安裝很多網(wǎng)絡(luò)工具。
第一步::使用 ??ps aux | grep puma?
?,獲取 Rails 服務(wù)的進程 ID。
找到了,它是 ??1837916?
?!簡單~
第二步::找到和 ??1837916?
? 運行在同一個網(wǎng)絡(luò)命名空間的 UDP 服務(wù)。
我使用了 ??nsenter?
?? 來在 ??puma?
?? 進程的網(wǎng)絡(luò)命令空間內(nèi)運行 ??netstat?
??(理論上,我猜想你也可以使用 ??netstat -tupn?
?? 來只顯示 UDP 服務(wù),但此時,我的手指頭只習(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
我們可以看到,此時有一個運行在 ??59426?
?? 端口的 UDP 服務(wù),它是由 ??dockerd?
? 運行的!或許它就是我們要找的 DNS 服務(wù)?
第三步:確定它是不是我們要找的 DNS 服務(wù)
我們可以使用 ??dig?
? 工具來向它發(fā)送一個 DNS 查詢:
$ sudo nsenter -n -t 1837916 dig +short @127.0.0.11 59426 rails_server
172.18.0.2
奇怪,我們之前運行 ??dig?
?? 的時候,DNS 查詢怎么沒有發(fā)送到 ??59426?
?? 端口,而是發(fā)送到了 ??53?
? 端口呢?這到底是怎么回事呀?
第四步:iptables
對于類似“這個服務(wù)似乎正運行在 X 端口上,但我卻在 Y 端口上訪問到了它,這是什么回事呢?”的問題,我的第一念頭都是“一定是 iptables 在作怪”。
于是,我在運行了容器的網(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ù)庫文件儲存在一個臨時目錄中
這樣做有一個好處:我可以直接掛載 Postgres 容器的數(shù)據(jù)目錄 ??./tmp/db?
?,而無需在我的筆記本電腦上管理 Postgres 環(huán)境。
我很喜歡這種方式,因為我真的不想在筆記本電腦上獨自管理一個 Postgres 環(huán)境(我也真的不知道該如何配置 Postgres)。另外,出于習(xí)慣,我更喜歡讓開發(fā)環(huán)境的數(shù)據(jù)庫和代碼放在同一個目錄下。
僅需一行命令,我就可以訪問 Rails 控制臺
管理 Ruby 的版本總是有點棘手,并且,即使我暫時搞定了它,我也總是有點擔(dān)心自己會把 Ruby 環(huán)境搞壞,然后就要修它個十年(夸張)。
(使用 Docker Compose)搭建好這個開發(fā)環(huán)境后,如果我需要訪問 Rails 控制臺console(一個交互式環(huán)境,加載了所有我的 Rails 代碼),我只需要運行一行代碼即可:
$ 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>
好耶!
小問題:Rails 控制臺的歷史記錄丟失了
我碰到了一個問題:Rails 控制臺的歷史記錄丟失了,因為我一直在不斷地重啟它。
不過,我也找到了一個相當(dāng)簡單的解決方案(嘿嘿):我往容器中添加了一個 ??/root/.irbrc?
? 文件,它能夠把 IRB 歷史記錄文件的保存位置指向一個不受容器重啟影響的地方。只需要一行代碼就夠啦:
IRB.conf[:HISTORY_FILE] = "/app/tmp/irb_history"
我還是不知道它在生產(chǎn)環(huán)境的表現(xiàn)如何
到目前為止,這個項目的生產(chǎn)環(huán)境搭建進度,還停留在“我制作了一個 DigitalOcean droplet(LCCT 譯注:一種 Linux 虛擬機服務(wù)),并手工編輯了很多文件”的階段。
嗯……我相信以后會在生產(chǎn)環(huán)境中使用 docker-compose 來運行一下它的。我猜它能夠正常工作,因為這個服務(wù)很可能最多只有兩個用戶在使用,并且,如果我愿意,我可以容忍它在部署過程中有 60 秒的不可用時間。不過話又說回來,出錯的往往是我想不到的地方。
推特網(wǎng)友提供了一些在生產(chǎn)中使用 docker-compose 的注意事項:
- ?
?docker-compose up?
? 只會重啟那些需要重啟的容器,這會讓重啟速度更快。 - 有一個 Bash 小腳本??wait-for-it??,你可以用它來保持等待一個容器,直到另一個容器的服務(wù)可用。
- 你可以準(zhǔn)備兩份?
?docker-compose.yaml?
?? 文件:用于開發(fā)環(huán)境的??docker-compose.yaml?
?? 和用于生產(chǎn)環(huán)境的??docker-compose-prod.yaml?
??。我想我會在分別為 Nginx 指定不同的端口:開發(fā)時使用??8999?
??,生產(chǎn)中使用??80?
?。 - 人們似乎一致認(rèn)為,如果你的項目是一臺計算機上運行的小網(wǎng)站,那么 docker-compose 在生產(chǎn)中不會有問題。
- 有個人建議說,如果愿意在生產(chǎn)環(huán)境搭建復(fù)雜那么一丟丟,Docker Swarm 就或許會是更好的選擇,不過我還沒試過(當(dāng)然,如果要這么說的話,干嘛不用 Kubernetes 呢?Docker Compose 的意義就是它超級簡單,而 Kubernetes 肯定不簡單 : ))。
Docker 似乎還有一個特性,它能夠 ??把你用 docker-compose 搭建的環(huán)境,自動推送到彈性容器服務(wù)(ESC)上??,聽上去好酷的樣子,但是我還沒有試過。
docker-compose 會有不適用的場景嗎
我聽說 docker-compose 在以下場景的表現(xiàn)較差:
- 當(dāng)你有很多微服務(wù)的時候(還是自己搭建比較好)
- 當(dāng)你嘗試從一個很大的數(shù)據(jù)庫中導(dǎo)入數(shù)據(jù)時(就像把幾百 G 的數(shù)據(jù)存到每個人的筆記本電腦里一樣)
- 當(dāng)你在 Mac 電腦上運行 Docker 時。我聽說 Docker 在 macOS 上比在 Linux 上要慢很多(我猜想是因為它需要做額外的虛擬化)。我沒有 Mac 電腦,所以我還沒有碰到這個問題。
以上就是全部內(nèi)容啦!
在此之前,我曾花了一整天時間,嘗試使用 Puppet 來配置 Vagrant 虛擬機,然后在這個虛擬機里配置開發(fā)環(huán)境。結(jié)果,我發(fā)現(xiàn)虛擬機啟動起來實在是有點慢啊,還有就是,我也不喜歡編寫 Puppet 配置(哈哈,沒想到吧)。
幸好,我嘗試了 Docker Compose,它真好簡單,馬上就可以開始工作啦!