文中涉及的示例代碼,已同步更新到 HelloGitHub-Team 倉庫python
以前一系列繁瑣的部署步驟讓咱們感到痛苦。這些痛苦包括:linux
那麼咱們有沒有辦法,讓本地開發環境和線上環境保持一致?這樣咱們在部署上線前,就能夠在本地進行驗證,只要驗證沒問題,咱們就有 99% 的把握保證部署上線後也沒有問題(1%保留給程序玄學)。nginx
這個辦法就是使用 Docker。git
Docker 是一種容器技術,能夠爲咱們提供一個隔離的運行環境。要使用 Docker,首先咱們須要編排一個鏡像,鏡像就是用來描述這個隔離環境應該是什麼樣子的,它須要安裝哪些依賴,須要運行什麼應用等,能夠把它類比成一搜貨輪的製造圖。github
有了鏡像,就能夠在系統中構建出一個實際隔離的環境,這個環境被稱爲容器,就比如根據設計圖,工廠製造了一條船。工廠也能夠製造無數條這樣的船。sql
容器造好了,只要啓動它,隔離環境便運行了起來。因爲事先編排好了鏡像,所以不管是在本地仍是線上,運行的容器內部環境都同樣,因此保證了本地和線上環境的一致性,大大減小了由於環境差別致使的各類問題。docker
因此,咱們首先來編排 Docker 鏡像。shell
相似於分離 settings.py 文件爲 local.py 和 production.py,咱們首先創建以下的目錄結構,分別用於存放開發環境的鏡像和線上環境的鏡像:數據庫
HelloDjango-blog-tutorial\
blog\
...
compose\
local\
production\
django\
nginx\
...
複製代碼
local 目錄下存放開發環境的 Docker 鏡像文件,production\ 下的 django 文件夾存放基於本項目編排的鏡像,因爲線上環境還要用到 Nginx,因此 nginx 目錄下存放 Nginx 的鏡像。django
咱們先來在 production\django 目錄下編排博客項目線上環境的鏡像文件,鏡像文件以 Dockerfile 命名:
FROM python:3.6-alpine
ENV PYTHONUNBUFFERED 1
RUN apk update \ # Pillow dependencies && apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev
WORKDIR /app
RUN pip install pipenv -i https://pypi.douban.com/simple
COPY Pipfile /app/Pipfile COPY Pipfile.lock /app/Pipfile.lock RUN pipenv install --system --deploy --ignore-pipfile
COPY . /app
COPY ./compose/production/django/start.sh /start.sh RUN sed -i 's/\r//' /start.sh RUN chmod +x /start.sh 複製代碼
首先咱們在鏡像文件開頭使用 FROM python:3.6-alpine
聲明此鏡像基於 python:3.6-alpine 基礎鏡像構建。alpine 是一個 Linux 系統發行版,主打小巧、輕量、安全。咱們程序運行須要 Python 環境,所以使用這個小巧但包含完整 Python 環境的基礎鏡像來構建咱們的應用鏡像。
ENV PYTHONUNBUFFERED 1
設置環境變量 PYTHONUNBUFFERED=1
接下來的一條 RUN 命令安裝圖像處理包 Pilliow 的依賴,由於若是使用 django 處理圖片時,會使用到 Pillow 這個Python 庫。
接着使用 WORKDIR /app 設置工做目錄,之後在基於此鏡像啓動的 Docker 容器中執行的命令,都會以這個目錄爲當前工做目錄。
而後咱們使用命令 RUN pip install pipenv
安裝 pipenv,-i 參數指定 pypi 源,國內通常指定爲豆瓣源,這樣下載 pipenv 安裝包時更快,國外網絡能夠省略 -i 參數,使用官方的 pypi 源便可。
而後咱們將項目依賴文件 Pipfile 和 Pipfile.lock copy 到容器裏,運行 pipenv install 安裝依賴。指定 --system 參數後 pipenv 不會建立虛擬環境,而是將依賴安裝到容器的 Python 環境裏。由於容器自己就是個虛擬環境了,因此不必再建立虛擬環境。
接着將這個項目的文件 copy 到容器的 /app 目錄下(固然有些文件對於程序運行是沒必要要的,因此一下子咱們會設置一個 dockerignore 文件,裏面指定的文件不會被 copy 到容器裏)。
而後咱們還將 start.sh 文件複製到容器的 / 目錄下,去掉回車符(windows 專用,容器中是 linux 系統),並賦予了可執行權限。
start.sh 中就是啓動 Gunicorn 服務的命令:
#!/bin/sh
python manage.py migrate
python manage.py collectstatic --noinput
gunicorn blogproject.wsgi:application -w 4 -k gthread -b 0.0.0.0:8000 --chdir=/app
複製代碼
咱們會讓容器啓動時去執行此命令,這樣就啓動了咱們的 django 應用。--chdir=/app 代表以 /app 爲根目錄,這樣才能找到 blogproject.wsgi:application。
在項目根目錄下創建 .dockerignore 文件,指定不 copy 到容器的文件:
.*
_credentials.py
fabfile.py
*.sqlite3
複製代碼
線上環境使用 Nginx,一樣來編排 Nginx 的鏡像,這個鏡像文件放到 compose\production\nginx 目錄下:
FROM nginx:1.17.1
# 替換爲國內源
RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak COPY ./compose/production/nginx/sources.list /etc/apt/ RUN apt-get update && apt-get install -y --allow-unauthenticated certbot python-certbot-nginx
RUN rm /etc/nginx/conf.d/default.conf COPY ./compose/production/nginx/HelloDjango-blog-tutorial.conf /etc/nginx/conf.d/HelloDjango-blog-tutorial.conf 複製代碼
這個鏡像基於 nginx:1.17.1 基礎鏡像構建,而後咱們更新系統並安裝 certbot 用於配置 https 證書。因爲要安裝大量依賴, nginx:1.17.1 鏡像基於 ubuntu,因此安裝會比較慢,咱們將軟件源替換爲國內源,這樣稍微提升一下安裝速度。
最後就是把應用的 nginx 配置複製到容器中 nginx 的 conf.d 目錄下。裏面的內容和直接在系統中配置 nginx 是同樣的。
upstream hellodjango_blog_tutorial {
server hellodjango_blog_tutorial:8000;
}
server {
server_name hellodjango-blog-tutorial-demo.zmrenwu.com;
location /static {
alias /apps/hellodjango_blog_tutorial/static;
}
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://hellodjango_blog_tutorial;
}
listen 80;
}
複製代碼
相比以前直接在宿主機配置 Nginx,這裏使用了 Nginx 的 upstream 模塊,實際上就是作一個請求轉發。Nginx 將全部請求轉發給上游 hellodjango_blog_tutorial 模塊處理,而 hellodjango_blog_tutorial 這個模塊的服務實際就是運行 django 應用的容器 hellodjango_blog_tutorial(接下來會運行這個容器)。
鏡像編排完畢,接下來就能夠經過鏡像構建容器並運行容器了。可是先等一等,咱們有兩個鏡像,一個是 django 應用的,一個是 Nginx 的,這意味着咱們須要構建 2 次容器,而且啓動容器 2 次,這會比較麻煩。有沒有辦法一次構建,一條命令運行呢?答案就是使用 docker-compose。
docker-compose 將各個容器的鏡像,以及構建和運行容器鏡像時的參數等編寫在一個 ymal 文件裏。這樣咱們只須要一條 build 命令就能夠構建多個容器,使用一條命令 up 就能夠啓動多個容器。
咱們在項目根目錄建一個 production.yml 文件來編排 django 容器和 nginx 容器。
version: '3'
volumes:
static:
database:
services:
hellodjango_blog_tutorial:
build:
context: .
dockerfile: compose/production/django/Dockerfile
image: hellodjango_blog_tutorial
container_name: hellodjango_blog_tutorial
working_dir: /app
volumes:
- database:/app/database
- static:/app/static
env_file:
- .envs/.production
ports:
- "8000:8000"
command: /start.sh
nginx:
build:
context: .
dockerfile: compose/production/nginx/Dockerfile
image: hellodjango_blog_tutorial_nginx
container_name: hellodjango_blog_tutorial_nginx
volumes:
- static:/apps/hellodjango_blog_tutorial/static
ports:
- "80:80"
- "443:443"
複製代碼
version: '3'
聲明 docker-compose 爲第三代版本的語法
volumes:
static:
database:
複製代碼
聲明瞭 2 個命名數據卷,分別爲 static 和 database。數據卷是用來幹嗎的呢?因爲 docker 容器是一個隔離環境,一旦容器被刪除,容器內的文件就會一併刪除。試想,若是咱們啓動了博客應用的容器並運行,一段時間後,容器中的數據庫就會產生數據。後來咱們更新了代碼或者修改了容器的鏡像,這個時候就要刪除舊容器,而後從新構建新的容器並運行,那麼舊容器中的數據庫就會連同容器一併刪除,咱們辛苦寫的博客文章付之一炬。
因此咱們使用 docker 的數據捲來管理須要持久存儲的數據,只要數據被 docker 的數據卷管理起來了,那麼新的容器啓動時,就能夠從數據卷取數據,從而恢復被刪除容器裏的數據。
咱們有 2 個數據須要被數據卷管理,一個是數據庫文件,一個是應用的靜態文件。數據庫文件容易理解,那麼爲何靜態文件也要數據卷管理呢?啓動新的容器後使用 python manage.py collectstatic 命令從新收集不就行了?
答案是不行,數據卷不只有持久保存數據的功能,還有跨容器共享文件的功能。要知道,容器不只和宿主機隔離,並且容器之間也是互相隔離的。Nginx 運行於獨立容器,那麼它處理的靜態文件從哪裏來呢?應用的靜態文件存放於應用容器,Nginx 容器是訪問不到的,因此這些文件也經過數據卷管理,nginx 容器從數據卷中取靜態文件映射到本身的容器內部。
接下來定義了 2 個 services,一個是應用服務 hellodjango_blog_tutorial,一個是 nginx 服務。
build:
context: .
dockerfile: compose/production/django/Dockerfile
複製代碼
告訴 docker-compose,構建容器是基於當前目錄(yml 文件所在的目錄),且使用的鏡像是 dockerfile 指定路徑下的鏡像文件。
image 和 container_name 分別給構建的鏡像和容器取個名字。
working_dir 指定工做目錄。
volumes:
- database:/app/database
- static:/app/static
複製代碼
同時這裏要注意,數據卷只能映射文件夾而不能映射單一的文件,因此對咱們應用的數據庫來講,db.sqlite3 文件咱們把它挪到了 database 目錄下。所以咱們要改一下 django 的配置文件中數據庫的配置,讓它正確地將數據庫文件生成在項目根目錄下的 database 文件夾下:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'database', 'db.sqlite3'),
}
}
複製代碼
env_file:
- .envs/.production
複製代碼
容器啓動時讀取 .envs/.production文件中的內容,將其注入環境變量。
咱們建立一下這個文件,把 secret_key 寫進去。
DJANGO_SECRET_KEY=2pe8eih8oah2_2z1=7f84bzme7^bwuto7y&f(#@rgd9ux9mp-3
複製代碼
注意將這些包含敏感信息的文件加入版本控制工具的忽略列表裏,防止一不當心推送到公開倉庫供大衆觀光。
ports:
- "8000:8000"
複製代碼
暴露容器內的 8000 端口而且和宿主機的 8000 端口綁定,因而咱們就能夠經過宿主機的 8000 端口訪問容器。
command: /start.sh 容器啓動時將執行 start.sh,從而啓動 django應用。
nginx 服務容器也相似,只是注意它從數據卷 static 中取靜態文件並映射到 nginx 容器內的 /apps/hellodjango_blog_tutorial/static,因此咱們在 nginx 的配置中:
location /static {
alias /apps/hellodjango_blog_tutorial/static;
}
複製代碼
這樣能夠正確代理靜態文件。
萬事具有,在本地執行一下下面的兩條命令來構建容器和啓動容器。
docker-compose -f production.yml build
docker-compose -f production.yml up
複製代碼
此時咱們能夠經過域名來訪問容器內的應用,固然,因爲 Nginx 在本地環境的容器內運行,須要修改一下 本地 hosts 文件,讓域名解析爲本地 ip 便可。
若是本地訪問沒有問題了,那麼就能夠直接在服務器上執行上面兩條命令以一樣的方式啓動容器,django 應用就順利地在服務上部署了。
既然線上環境都使用 Docker 了,不妨開發環境也一併使用 Docker 進行開發。開發環境的鏡像和 docker-compose 文件比線上環境簡單一點,由於不用使用 nginx。
開發環境的鏡像文件,放到 compose\local 下:
FROM python:3.6-alpine
ENV PYTHONUNBUFFERED 1
RUN apk update \
# Pillow dependencies
&& apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev
WORKDIR /app
RUN pip install pipenv -i https://pypi.douban.com/simple
COPY Pipfile /app/Pipfile
COPY Pipfile.lock /app/Pipfile.lock
RUN pipenv install --system --deploy --ignore-pipfile
COPY ./compose/local/start.sh /start.sh
RUN sed -i 's/\r//' /start.sh
RUN chmod +x /start.sh
複製代碼
要注意和線上環境不一樣的是,咱們沒有把整個代碼 copy 到容器裏。線上環境代碼通常比較穩定,而對於開發環境,因爲須要頻繁修改和調試代碼,若是咱們把代碼 copy 到容器,那麼容器外作的代碼修改,容器內部是沒法感知的,這樣容器內運行的應用就無法同步咱們的修改了。因此咱們會把代碼經過 Docker 的數據捲來管理。
start.sh 再也不啓動 gunicorn,而是使用 runserver 啓動開發服務器。
#!/bin/sh
python manage.py migrate
python manage.py runserver 0.0.0.0:8000
複製代碼
而後建立一個 docker-compose 文件 local.yml(和 production.yml 同級),用於管理開發容器。
version: '3'
services:
djang_blog_tutorial_v2_local:
build:
context: .
dockerfile: ./compose/local/Dockerfile
image: django_blog_tutorial_v2_local
container_name: django_blog_tutorial_v2_local
working_dir: /app
volumes:
- .:/app
ports:
- "8000:8000"
command: /start.sh
複製代碼
注意咱們將整個項目根目錄下的文件掛載到了 /app 目錄下,這樣就能容器內就能實時反映代碼的修改了。
若是容器在本地運行沒有問題了,線上環境的容器運行也沒有問題,由於理論上,咱們在線上服務器也會構建和本地測試用的容器如出一轍的環境,因此幾乎能夠確定,只要咱們服務器有 Docker,那麼咱們的應用就能夠成功運行。
首先在服務安裝 Docker,安裝方式因系統而異,方式很是簡單,咱們以 CentOS 7 爲例,其它系統請參考 Docker 的官方文檔。
首先安裝必要依賴:
$ sudo yum install -y yum-utils \
device-mapper-persistent-data \
lvm2
複製代碼
而後添加倉庫源:
$ sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
複製代碼
最後安裝 Docker:
$ sudo yum install docker-ce docker-ce-cli containerd.io
複製代碼
啓動 Docker:
$ sudo systemctl start docker
複製代碼
(境外服務器忽略)設置 Docker 源加速(使用 daocloud 提供的鏡像源),不然拉取鏡像時會很是慢
curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://f1361db2.m.daocloud.io
複製代碼
在 docker 中運行一個 hello world,確認 docker 安裝成功:
$ sudo docker run hello-world
複製代碼
docker 安裝成功了,還要安裝一下 docker-compose。實際上是一個 python 包,咱們直接經過 pip 安裝就能夠了:
$ pip install docker-compose
複製代碼
爲了不運行一些 docker 命令時可能產生的權限問題,咱們把系統當前用戶加入到 docker 組裏:
$ sudo usermod -aG docker ${USER}
複製代碼
添加組後要重啓一下 shell(ssh 鏈接的話就斷開重連)。
萬事俱備,只欠東風了!
開始準備讓咱們的應用在 docker 容器裏運行。因爲以前咱們把應用部署在宿主機上,首先來把相關的服務停掉:
# 停掉 nginx,由於咱們將在容器中運行 nginx
$ sudo systemctl stop nginx
# 停掉博客應用
$ supervisorctl stop hellodjango-blog-tutorial -c ~/etc/supervisord.conf
複製代碼
接下來拉取最新的代碼到服務器,進入項目根目錄下,建立線上環境須要的環境變量文件:
$ mkdir .envs
$ cd .envs
$ vi .production
複製代碼
將線上環境的 secret key 寫入 .production 環境變量文件,
DJANGO_SECRET_KEY=2pe8eih8oah2_2z1=7f84bzme7^bwuto7y&f(#@rgd9ux9mp-3
複製代碼
保存並退出。
回到項目根目錄,運行 build 命令構建鏡像:
$ docker-compose -f prodcution.yml build
複製代碼
而後咱們能夠開始啓動根據構建好的鏡像啓動 docker 容器,不過爲了方便,咱們的 docker 進程仍然由 supervisor 來管理,咱們修改一下博客應用的配置,讓它啓動時啓動 docker 容器。
打開 ~/etc/supervisor/conf.d/hellodjango-blog-tutorial.ini,修改成以下內容:
[program:hellodjango-blog-tutorial]
command=docker-compose -f production.yml up --build
directory=/home/yangxg/apps/HelloDjango-blog-tutorial
autostart=true
autorestart=unexpected
user=yangxg
stdout_logfile=/home/yangxg/etc/supervisor/var/log/hellodjango-blog-tutorial-stdout.log
stderr_logfile=/home/yangxg/etc/supervisor/var/log/hellodjango-blog-tutorial-stderr.log
複製代碼
主要就是把以前的使用 Gunicorn 來啓動服務換成了啓動 docker。
修改 ini 配置 要記得 reread 使配置生效:
$ supervisorctl -c ~/etc/supervisord.conf
> reread
> start
複製代碼
docker 容器順利啓動,訪問咱們的博客網站。拋掉鏡像編排的準備工做,至關於咱們只執行了一條構建容器並啓動容器的命令就部署了咱們的博客應用。若是換臺服務器,也只要再執行一下鏡像構建和啓動容器的命令,服務就又能夠起來!這就是 docker 的好處。
因爲開發 django 用的最多的 IDE Pycharm 也能很好地集成 Docker,我如今開發工做已經全面擁抱 Docker 了,史無前例的體驗,史無前例的方便和穩定,必定要學着用起來!
最後,因爲 Nginx 在新的容器裏運行,因此須要從新申請和配置 https 證書,這和以前是同樣,只是此前 Nginx 在宿主機上,此次咱們在容器裏運行 certbot 命令。編排 nginx 鏡像時已經安裝了 certbot,直接執行命令便可,在 docker 容器內執行命令以下:
咱們首先經過 docker ps 命令查看正在運行的容器,記住 nginx 容器的名字,而後使用 docker exec -it 容器名 命令的格式在指定容器內執行命令,因此咱們執行:
$ docker exec -it nginx certbot --nginx
複製代碼
根據提示輸入信息便可,過程和上一節在宿主機上部署如出一轍,這裏再也不重複。
fabric 無需修改,來嘗試本地執行一下:
pipenv run fab -H server_ip --prompt-for-login-password -p deploy
複製代碼
完美!至此,咱們的博客已經穩定運行於線上,陸陸續續會有更多的人來訪問咱們的博客,讓咱們來繼續完善它的功能吧!
『講解開源項目系列』——讓對開源項目感興趣的人再也不畏懼、讓開源項目的發起者再也不孤單。跟着咱們的文章,你會發現編程的樂趣、使用和發現參與開源項目如此簡單。歡迎留言聯繫咱們、加入咱們,讓更多人愛上開源、貢獻開源~