1. 程式人生 > >使用 Docker 讓部署 Django 專案更加輕鬆

使用 Docker 讓部署 Django 專案更加輕鬆

作者:HelloGitHub-追夢人物

文中涉及的示例程式碼,已同步更新到 HelloGitHub-Team 倉庫

之前一系列繁瑣的部署步驟讓我們感到痛苦。這些痛苦包括:

  • 要去伺服器上執行 n 條命令
  • 本地環境和伺服器環境不一致,明明本地執行沒問題,一部署伺服器上就掛掛,死活啟動不起來
  • 如果上面的情況發生了,又要去伺服器上執行 n 條命令以解決問題
  • 本地更新了程式碼,部署上線後,上述歷史又重演一遍,想死的心都有了

那麼我們有沒有辦法,讓本地開發環境和線上環境保持一致?這樣我們在部署上線前,就可以在本地進行驗證,只要驗證沒問題,我們就有 99% 的把握保證部署上線後也沒有問題(1%保留給程式玄學)。

這個辦法就是使用 Docker。

Docker 是一種容器技術,可以為我們提供一個隔離的執行環境。要使用 Docker,首先我們需要編排一個映象,映象就是用來描述這個隔離環境應該是什麼樣子的,它需要安裝哪些依賴,需要執行什麼應用等,可以把它類比成一搜貨輪的製造圖。

有了映象,就可以在系統中構建出一個實際隔離的環境,這個環境被稱為容器,就好比根據設計圖,工廠製造了一條船。工廠也可以製造無數條這樣的船。

容器造好了,只要啟動它,隔離環境便運行了起來。由於事先編排好了映象,因此無論是在本地還是線上,執行的容器內部環境都一樣,所以保證了本地和線上環境的一致性,大大減少了因為環境差異導致的各種問題。

所以,我們首先來編排 Docker 映象。

類似於分離 settings.py 檔案為 local.py 和 production.py,我們首先建立如下的目錄結構,分別用於存放開發環境的映象和線上環境的映象:

HelloDjango-blog-tutorial\
      blog\
      ...
      compose\
            local\
            production\
                  django\
                  nginx\
    ...

local 目錄下存放開發環境的 Docker 映象檔案,production 下的 django 資料夾存放基於本專案編排的映象,由於線上環境還要用到 Nginx,所以 nginx 目錄下存放 Nginx 的映象。

線上環境

映象檔案

我們先來在 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 了,前所未有的體驗,前所未有的方便和穩定,一定要學著用起來!

HTTPS

最後,由於 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

完美!至此,我們的部落格已經穩定運行於線上,陸陸續續會有更多的人來訪問我們的部落格,讓我們來繼續完善它的功能吧!


『講解開源專案系列』——讓對開源專案感興趣的人不再畏懼、讓開源專案的發起者不再孤單。跟著我們的文章,你會發現程式設計的樂趣、使用和發現參與開源專案如此簡單。歡迎留言聯絡我們、加入我們,讓更多人愛上開源、貢獻開源