聊聊 Python 應用容器化部署流程
1. 簡介
Docker 是目前主流IT公司廣泛接受和使用的,用于構建、管理和保護它們應用程序的工具。
容器,例如 Docker 允許開發人員在單個操作系統上隔離和運行多個應用程序,而不是為服務器上的每個應用程序專用一個虛擬機。使用容器更輕量級,可以降低成本、更好地使用資源和發揮更高的性能。
本文將使用 Flask 開發一個簡單的 Python web 應用程序,并為 “容器化” 做好準備。然后創建一個 Docker 映像,并將其部署到測試和生產環境中。
注意: 請確保機器上已安裝 Docker,如果沒有請參考 Docker 官方安裝教程。
2. Docker介紹
Docker 是一種工具,它使開發人員能夠交付他們的應用程序(以及庫或其他依賴項),確保他們可以使用正確的配置運行,而不受部署環境影響。
這是通過將應用程序隔離在單獨的容器中來實現的,這些應用程序雖然被容器分隔開,但是卻可以共享操作系統和其他資源。
Docker 包含兩部分:
- Docker Engine — 應用打包工具,用于封裝應用程序。
- Docker Hub — 用于管理云上容器應用程序的工具。
3.為何選擇容器
了解容器的重要性和實用性非常重要,雖然它和直接將應用部署到服務器沒有多大區別,但是當涉及到比較復雜的且相當吃資源的應用,尤其是多個應用部署在同一臺服務器,或是同一應用要部署到多臺服務器時。容器就變得非常有用。
在容器之前,這是通過 VMWare 和 Hypervisor 等虛擬機解決的,但是它們在效率、速度和可移植性方面已被證明并不是最佳選擇。
Docker 容器是虛擬機的輕量級的替代品-與 VM 不同,我們不需要為它預先分配 RAM、CPU 或其他資源,也不需要為每個應用程序啟動一個 VM,僅僅只需要一個操作系統即可。
使用容器開發人員就不需要為不同環境制定特殊版本,這樣可以專注于應用程序的核心業務邏輯。
4.創建 Python 應用
Flask 是 Python 的一個輕量級 Web 應用框架,簡單易用,可以很快速地創建 web 應用。我們用它來創建此 demo 應用。
如果還沒有安裝 Flask 模塊,可以使用下面命令安裝:
- $ pip install flask
安裝成功后,新建一個應用目錄,命名為 FlaskDemo。并在該目錄下創建應用代碼文件app.py。
在 app.py 中,首先引入 Flask 模塊,然后創建一個 web 應用:
- from flask import Flask
- app = Flask(__name__)
然后定義路由/和其對應的請求處理程序:
- @app.route("/")
- def index():
- return """
- <h1>Python Flask in Docker!</h1>
- <p>A sample web-app for running Flask inside Docker.</p>
- """
最后,添加運行主程序并啟動該腳本:
- if __name__ == "__main__":
- app.run(debug=True, host='0.0.0.0')
- $ python app.py
然后在瀏覽器中訪問 http://localhost:5000/,可以看到 Dockerzing Python app using Flask這樣的頁面。
5.Dokcer打包應用
要在 Docker 上運行應用程序,首先必須構建一個容器,而且必須包含使用的所有依賴項——在我們的例子中只有Flask。因此,新建一個包含所有依賴包的 requirements.txt 文件,然后創建一個 Dockerfile,該文件用來描述構建映像過程。
此外,當啟動容器時還需要放開應用程序的HTTP端口。
準備工作
requirements.txt 文件非常簡單,只需要填入項目的依賴包和其對應版本即可:
- Flask==1.0.2
接下來,需要將應用程序運行所需的所有Python文件都放在頂層文件夾中,例如,名為app的目錄。
同時建議將主入口程序命名為 app.py ,將腳本中創建的Flask對象命名為 app 是一種通常的做法,這樣也可以簡化部署。
- FlaskApp
- ├── requirements.txt
- ├── Dockerfile
- └── app
- └── app.py
- └── <other .py files>
創建Dockerfile
Dockerfile 本質上是一個文本文件,其中明確定義了如何為我們的項目構建 Docker 鏡像。
接下來創建一個基于 Ubuntu 16.04 和 Python 3.X 的 Dokcer 鏡像:
- FROM ubuntu:16.04
- MAINTAINER jhao104 "j_hao104@163.com"
- RUN apt-get update -y && \
- apt-get install -y python3-pip python3-dev
- COPY ./requirements.txt /requirements.txt
- WORKDIR /
- RUN pip3 install -r requirements.txt
- COPY . /
- ENTRYPOINT [ "python3" ]
- CMD [ "app/app.py" ]
Dockerfile 的基本指令有十三個,上面用到了部分;
- FROM - 所有Dockerfile的第一個指令都必須是 FROM ,用于指定一個構建鏡像的基礎源鏡像,如果本地沒有就會從公共庫中拉取,沒有指定鏡像的標簽會使用默認的latest標簽,如果需要在一個Dockerfile中構建多個鏡像,可以使用多次。
- MAINTAINER - 描述鏡像的創建者,名稱和郵箱。
- RUN - RUN命令是一個常用的命令,執行完成之后會成為一個新的鏡像,通常用于運行安裝任務從而向映像中添加額外的內容。在這里,我們需更新包,安裝 python3 和 pip 。在第二個 RUN 命令中使用 pip 來安裝 requirements.txt 文件中的所有包。
- COPY - 復制本機文件或目錄,添加到指定的容器目錄, 本例中將 requirements.txt 復制到鏡像中。
- WORKDIR - 為RUN、CMD、ENTRYPOINT指令配置工作目錄。可以使用多個WORKDIR指令,后續參數如果是相對路徑,則會基于之前命令指定的路徑。
- ENTRYPOINT - 在啟動容器的時候提供一個默認的命令項。
- RUN - 運行 app 目錄中的 app.py 。
Docker鏡像構建原理
Docker鏡像是使用 Docker build 命令構建的。在構建鏡像時,Docker創建了所謂的“層(layers)”。每一層都記錄了Dockerfile中的命令所導致的更改,以及運行命令后鏡像的狀態。
Docker在內部緩存這些層,這樣在重新構建鏡像時只需要重新創建已更改的層。例如,這里使用了 ubuntu:16.04 的基礎鏡像,相同容器的所有后續構建都可以重用它,因為它不會改變。但是,因為項目修改,在下次重新構建過程中 app 目錄的內容可能會有所不同,因此只會重新構建這一層。
需要注意的是,每當重新構建某一層時,Dockerfile 中緊隨其后的所有層也都需要重新構建。例如,我們首先復制 requirements.txt 文件,然后再復制應用程序的其余部分。這樣之前安裝的依賴項只要沒有新的依賴關系,即使應用程序中的其他文件發生了更改,也不需要重新構建這一層。這一點在創建 Dockerfiles 時一定要注意。
因此,通過將 pip 安裝與應用程序其余部分的部署分離,可以優化容器的構建過程。
構建Docker鏡像
現在 Dockerfile 已經準備好了,而且也了解了Docker的構建過程,接下來為我們的應用程序創建Docker映像:
- docker build -t docker-flask:0.1 .
調試模式運行
根據前面講到的容器化的優點,開發的應用程序通過容器部署,這從一開始就確保了應用程序構建的環境是干凈的,從而消除了交付過程中的意外情況。
但是呢,在開發應用程序的過程中,更重要的是要快速重新構建和測試,以檢查驗證過程中的每個中間步驟。為此,web應用程序的開發人員需要依賴于Flask等框架提供的自動重啟功能(Debug模式下,修改代碼自動重啟)。而這一功能也可以在容器中使用。
為了啟用自動重啟,在啟動Docker容器時將主機中的開發目錄映射到容器中的app目錄。這樣Flask就可以監聽主機中的文件變化(通過映射)來發現代碼更改,并在檢測到更改時自動重啟應用程序。
此外,還需要將應用程序的端口從容器轉發到主機。這是為了能夠讓主機上的瀏覽器訪問應用程序。
因此,啟動Dokcer容器時需要使用 volume-mapping 和 port-forwarding 選項:
- docker run --name flask_app -v $PWD/app:/app -p 5000:5000 docker-flask:0.1
改命令將會執行以下操作:
- 基于之前構建的 docker-flask 鏡像啟動一個容器;
- 這個容器的名稱被設置為 flask_app 。如果沒有 ——name 選項,Docker將為容器生成一個名稱。顯式指定名稱可以幫助我們定位容器(用來停止等操作);
- -v 選項將主機的app目錄掛載到容器;
- -p 選項將容器的端口映射到主機。
現在可以通過http://localhost:5000 或者 http://0.0.0.0:5000/ 訪問到應用:
如果我們在容器運行的時候,修改應用程序代碼,Flask會檢測到更改并重新啟動應用程序。

要停止容器的話,可以使用 Ctrl + C, 并運行 docker rm flask_app 移除容器。
生產模式運行
雖然直接使用Flask裸跑運行應用程序對于開發來說已經足夠好了,但是我們需要在生產中使用更健壯的部署方法。
目前主流的部署方案是 nginx + uwsgi,下面我們將介紹如何為生產環境部署web應用程序。Nginx是一個開源web服務器,uWSGI是一個快速、自我修復、開發人員和系統管理員友好的服務器。
首先,我們創建一個入口腳本,用來控制以開發模式還是生產模式啟動我們的應用程序,這兩者區別是選擇直接運行python還是nginx模式。
然后再寫一個簡單shell啟動腳本 entry-point.sh:
- #!/bin/bash
- if [ ! -f /debug0 ]; then
- touch /debug0
- while getopts 'hd:' flag; do
- case "${flag}" in
- h)
- echo "options:"
- echo "-h show brief help"
- echo "-d debug mode, no nginx or uwsgi, direct start with 'python3 app/app.py'"
- exit 0
- ;;
- d)
- touch /debug1
- ;;
- *)
- break
- ;;
- esac
- done
- fi
- if [ -e /debug1 ]; then
- echo "Running app in debug mode!"
- python3 app/app.py
- else
- echo "Running app in production mode!"
- nginx && uwsgi --ini /app.ini
- fi
然后創建uWSGI配置文件 app.ini:
- [uwsgi]
- plugins = /usr/lib/uwsgi/plugins/python3
- chdir = /app
- module = app:app
- uid = nginx
- gid = nginx
- socket = /run/uwsgiApp.sock
- pidfile = /run/.pid
- processes = 4
- threads = 2
和nginx配置文件 nginx.conf:
- user nginx;
- worker_processes 4;
- pid /run/nginx.pid;
- events {
- worker_connections 20000;
- }
- http {
- include mime.types;
- sendfile on;
- keepalive_timeout 65;
- gzip off;
- server {
- listen 80;
- access_log off;
- error_log off;
- location / { try_files $uri @flaskApp; }
- location @flaskApp {
- include uwsgi_params;
- uwsgi_pass unix:/run/uwsgiApp.sock;
- }
- }
- }
最后,修改Dockerfile 將nginx和uWSGI安裝到鏡像,將配置文件復制到鏡像中,并設置運行nginx所需的用戶權限:
- FROM ubuntu:16.04
- MAINTAINER jhao104 "j_hao104@163.com"
- RUN apt-get update -y && \
- apt-get install -y python3-pip python3-dev && \
- apt-get install -y nginx uwsgi uwsgi-plugin-python3
- COPY ./requirements.txt /requirements.txt
- COPY ./nginx.conf /etc/nginx/nginx.conf
- WORKDIR /
- RUN pip3 install -r requirements.txt
- COPY . /
- RUN adduser --disabled-password --gecos '' nginx\
- && chown -R nginx:nginx /app \
- && chmod 777 /run/ -R \
- && chmod 777 /root/ -R
- ENTRYPOINT [ "/bin/bash", "/entry-point.sh"]
然后重新打包鏡像:
- docker build -t docker-flask:0.1 .
然后使用nginx啟動應用程序:
- docker run -d --name flaskapp --restart=always -p 8091:80 docker-flask:0.1
該鏡像包含python、ngix、uwsgi完整環境,只需要在部署時指定端口映射便可以自動部署應用。要停止并刪除此容器,請運行下面命令:
- docker stop flaskapp && docker rm flaskapp
此外,如果我們仍然需要上面調試功能或修改部分代碼,也可以像上面一樣以調試模式運行容器:
- docker run -it --name flaskapp -p 5000:5000 -v $PWD/app:/app docker-flask:0.1 -d debug
6.管理外部依賴
如果將應用程序作為容器交付時,需要記住的一個關鍵事項是,開發人員管理依賴項的責任增加了。除了識別和指定正確的依賴項和版本之外,還需要負責在容器環境中安裝和設置這些依賴項。
在Python項目中管理安裝依賴比較容易,可以使用requirements.txt指定依賴項和對應版本,然后通過 pip 安裝。
需要重申的是是,無論何時修改 requirements.txt 文件,都需要重新構建Docker鏡像。
啟動時安裝依賴項
可能在某次版本更新時需要安裝額外的依賴項。比如,在開發過程中使用了一個新的包。如果不希望每次都重新構建Docker鏡像,或者希望在啟動時使用最新的可用版本。可以通過修改啟動程序在應用程序啟動時運行安裝程序來實現這一點。
同樣,我們也可以安裝額外的系統級包依賴項。修改 entry-point.sh:
- #!/bin/bash
- if [ ! -f debug0 ]; then
- touch debug0
- if [ -e requirements_os.txt ]; then
- apt-get install -y $(cat requirements_os.txt)
- fi
- if [-e requirements.txt ]; then
- pip3 install -r requirements.txt
- fi
- while getopts 'hd:' flag; do
- case "${flag}" in
- h)
- echo "options:"
- echo "-h show brief help"
- echo "-d debug mode, no nginx or uwsgi, direct start with 'python3 app/app.py'"
- exit 0
- ;;
- d)
- touch debug1
- ;;
- *)
- break
- ;;
- esac
- done
- fi
- if [ -e debug1 ]; then
- echo "Running app in debug mode!"
- python3 app/app.py
- else
- echo "Running app in production mode!"
- nginx && uwsgi --ini /app.ini
- fi
這樣我們可以在 requirements_os.txt 中指定將要安裝的系統軟件包名稱,這些包名以空格分隔放在同一行。他們將和 requirements.txt 中的Python依賴庫一樣在應用程序啟動之前安裝。
盡管這樣對應用的迭代開發期間提供了便利,但是出于幾個原因,在啟動時安裝依賴項不是一個好的實踐:
- 它破壞了容器化的目標之一,即修復和測試由于部署環境的變化而不會改變的依賴關系;
- 增加了應用程序啟動的額外開銷,這將增加容器的啟動時間;
- 每次啟動應用程序時需要安裝依賴項,這樣對網絡資源有要求。