Docker มาใช้กับ Django Web Framework ได้อย่างไร?

Docker มาใช้กับ Django Web Framework ได้อย่างไร?

ช่วงนี้ที่ Pronto ได้นำเอา Docker เข้ามาใช้งานเป็นที่เรียบร้อยแล้ว แต่ประเด็นคือเราก็อยากให้คนอื่นใช้เป็นด้วย! บทความนี้เลยมาอธิบายถึงขั้นตอนต่างๆ ในการหยิบเอาเทคโนโลยีสุดฮิตอย่าง Docker มาใช้กับ Django Web Framework ว่าเราเริ่มต้นสร้างกันอย่างไร รวมไปถึงการนำเอา Nginx มาเป็น Web server และ PostgreSQL มาเป็นฐานข้อมูล จับทั้ง 3 มาประสานพลังกันโดยใช้ Docker Compose ซึ่งหวังว่าถ้าได้อ่านแล้วจะสามารถนำไปประยุกต์ใช้ในโปรเจคอื่นๆ กันได้ หรืออย่างน้อยได้ลองเล่นดูเนอะ 🙂

เริ่มโปรเจค Django

ขอข้ามขั้นตอนการติดตั้ง pip กับ virtualenv นะครับ ถ้ายังทำไม่เป็น อยากให้ลองไปอ่าน ติดตั้ง virtualenv สำหรับเขียน code Python กันก่อนนะ ทีนี้พอเรามี pip หรือ virtualenv แล้วเราก็จะมาสร้างโฟลเดอร์สำหรับโปรเจคเราก่อน

$ mkdir django-with-docker
$ cd django-with-docker

แล้วติดตั้ง Django กับ uWSGI (เป็น Python WSGI HTTP Server)

$ pip install Django==1.10.1 uWSGI==2.0.13.1

ในบทความนี้ขอกำหนดเวอร์ชั่นของ package ต่างๆ ไว้นะครับ ซึ่งจริงๆ ไม่จำเป็นต้องเป็นเวอร์ชั่นตามนี้ก็ได้นะครับ เผื่อหลงเข้ามาอ่านตอนที่เวอร์ชั่นอัพเดทไปแล้ว 😛

ซึ่งตรงนี้เราควรเก็บว่าเราได้ติดตั้งอะไรไปบ้าง ใน Python เราจะใส่ไว้ในไฟล์ที่ชื่อ requirements.txt ครับ หน้าตาที่ได้ก็จะประมาณนี้

Django==1.10.1
uWSGI==2.0.13.1

เวลาที่เราอยากจะลง Python package อะไรเพิ่มเติมให้มาอัพเดทไฟล์นี้ แล้วสั่งคำสั่งด้านล่างนี้คำสั่งเดียว

$ pip install -r requirements.txt

เราก็จะได้ทุก package ที่เราใส่ไว้ในไฟล์นั้นครับ ง่ายดีเนอะ 😀 ต่อมาก็สร้างโปรเจค Django โดยสั่ง

$ django-admin startproject hellodocker

หน้าตาโครงสร้างของโปรเจคเราตอนนี้ควรจะเป็นแบบนี้

.
└── django-with-docker
    ├── hellodocker
    |   ├── hellodocker
    |   │   ├── __init__.py
    |   │   ├── settings.py
    |   │   ├── urls.py
    |   │   └── wsgi.py
    |   └── manage.py
    └── requirements.txt

เสร็จแล้วลองทดสอบก่อนครับว่าโปรเจคเราทำงานได้จริงหรือเปล่า ให้เข้าไปในโฟลเดอร์ที่มีไฟล์ manage.py แล้วสั่ง

$ python manage.py runserver 0.0.0.0:8000

โดย server เราจะรันที่ http://0.0.0.0:8000/ ลองเข้าดูควรจะต้องเห็นหน้าตาตามรูป 1 ด้านล่างนี้ครับ

First Django-powered Page

รูปที่ 1: First Django-powered Page

เนื่องจากบทความนี้ไม่ได้เป็นบทความเกี่ยวกับ Django อย่างเดียว ผมขอสร้างหน้าเว็บแบบเร็วๆ ง่ายๆ ที่แสดงคำว่า Hello, Docker! ซึ่งวิธีสร้างก็ให้สร้างไฟล์ชื่อ views.py ไว้ที่โฟลเดอร์ hellodocker ที่เป็นโฟลเดอร์เดียวกับไฟล์ urls.py แล้วแปะโค้ดตามนี้

from django.http import HttpResponse


def index(request):
    return HttpResponse('Hello, Docker!')

การสร้าง view ด้านบนนี้เรียกว่าเป็นการสร้างแบบ function-based view ครับ จะมีอีกวิธีคือแบบ class-based view ก็มีข้อดีข้อเสียต่างกันไป อ่านเพิ่มเติมเรื่อง view ได้ที่ Writing views นะครับ

เสร็จแล้วเราต้องทำให้ Django รู้จัก view ที่เราสร้างขึ้นนี้ ก็ให้ไปใส่เพิ่มในไฟล์ urls.py ครับ ตามนี้

from django.conf.urls import url
from django.contrib import admin

from .views import index


urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^$', index, name='home'),
]

ถ้าแก้ตามนี้เสร็จแล้วลองสั่ง python manage.py runserver 0.0.0.0:8000 อีกรอบ น่าจะเห็นคำว่า Hello, Docker! แล้ว ถ้าได้ตามนี้ก็ถือว่าเสร็จสิ้นส่วนสำหรับโปรเจค Django 🙂

จับ Docker มาใช้กับ Django

เราจะเขียน Dockerfile กัน เพื่อสร้าง image มาใช้งาน เนื่องจาก Django เป็น Python เนอะ เราควรจะใช้ base image ที่เป็น Python โดย Dockerfile เราจะมีหน้าตาประมาณนี้

FROM python:2.7.9

ENV APPLICATION_ROOT /app/

RUN apt-get update
RUN pip install --upgrade pip

RUN mkdir -p $APPLICATION_ROOT
WORKDIR $APPLICATION_ROOT
ADD . $APPLICATION_ROOT
RUN pip install -r requirements.txt

EXPOSE 8000

ENTRYPOINT ["python", "hellodocker/manage.py", "runserver", "0.0.0.0:8000"]

ณ ตอนนี้ หน้าตาโครงสร้างโปรเจคเราจะเป็นแบบนี้

.
└── django-with-docker
     ├── hellodocker
     |   ├── hellodocker
     |   │   ├── __init__.py
     |   │   ├── settings.py
     |   │   ├── urls.py
     |   |   ├── views.py
     |   │   └── wsgi.py
     |   └── manage.py
     ├── Dockerfile
     └── requirements.txt

แล้วสั่งสร้าง image

$ docker build -t app .

เราจะได้ image ของเราที่ชื่อ app มา ลองรันดู

docker run -p 8000:8000 app

เสร็จแล้ว! (เราต้อง bind port ออกมาที่เครื่อง host ด้วยนะครับ คำสั่งด้านบนจะ bind ออกมาที่ port 8000) ทีนี้เราก็สามารถ push image เราเข้า Docker Hub เสร็จแล้วก็เอาไป pull ที่ไหนก็ได้ ควรจะต้องได้ผลเหมือนกัน คือมีหน้า Hello, Docker! เหมือนกันเป๊ะๆ

หมายเหตุ สำหรับคนที่ใช้ Docker Machine อาจะต้องเปลี่ยนจาก 0.0.0.0 เป็น IP ของเครื่อง virtual machine นั้นๆ นะครับ

ส่วนถ้าใครอยากปรับให้ image เรามีขนาดเล็กลง หรืออยากลดเวลาในการสร้างลง ลองอ่านบทความเกี่ยวกับ Docker Image ที่ผมเขียนไว้ดู

เอา uWSGI มาใช้กับ Django

ถ้าเราอยากเอา Docker image เราไปรันบน production จริงๆ เราสามารถไป pull แล้วก็รันได้เลยนะ แต่ว่า performance จะห่วยมาก เพราะว่าเรารันแบบ development mode อยู่ ถ้าจะให้แจ่มคือเราต้องเอา Web Server Gateway Interface (WSGI) มาใช้ ของ Python เราจะใช้ uWSGI ครับ (หรือจะใช้ Gunicorn ก็ได้ครับ แต่สำหรับผมแล้ว uWSGI ดูง่ายกว่า)

ก่อนอื่นต้องมี configuration ที่เป็นไฟล์ INI ก่อนครับ เอาไว้ให้ uWSGI มาอ่าน ผมขอสร้างไฟล์ hellodocker.ini ไว้แบบนี้

[uwsgi]
http = :8000
chdir = /app/hellodocker
module = django.core.wsgi:get_wsgi_application()
env = DJANGO_SETTINGS_MODULE=hellodocker.settings
master = true
processes = 10
vacuum = true

เอาไว้ที่เดียวกับ Dockerfile ก็ได้ครับ โครงสร้างโปรเจคตอนนี้ควรจะเป็นแบบนี้

.
└── django-with-docker
     ├── hellodocker
     |   ├── hellodocker
     |   │   ├── __init__.py
     |   │   ├── settings.py
     |   │   ├── urls.py
     |   |   ├── views.py
     |   │   └── wsgi.py
     |   └── manage.py
     ├── Dockerfile
     ├── hellodocker.ini
     └── requirements.txt

ถ้าอยากรู้ว่าแต่ละ option คืออะไร ตามไปอ่านที่ uWSGI Options ได้เลยครับ น่าจะเข้าใจได้ไม่ยาก

ทีนี้ให้เรากลับมาแก้ Dockerfile ก่อนเพื่อเปลี่ยนไปใช้ uWSGI ในการรัน server ให้เปลี่ยนตรง ENTRYPOINT เป็นตามนี้

ENTRYPOINT ["uwsgi", "--ini", "hellodocker.ini"]

สร้าง image ใหม่ แล้วก็รัน เสร็จแล้วเข้าหน้าเว็บควรจะเจอ Hello, Docker! คำเดิม คำที่เราคุ้นเคย 🙂

เอา Nginx มาร่วมวง

เราจะใช้ base image ของ Nginx ครับ ลอง pull ลงมาเล่นก่อน

$ docker pull nginx

แล้วก็ลองรันดู

$ docker run -p 8080:80 nginx

ในทีนี้ผม bind port ของบนเครื่องผมเป็น 8080 กับ port ของ Nginx ที่ default เป็น 80 นะครับ ไม่ใช้ port 80 บนเครื่องผมเพราะว่ามันอาจจะไปชนกัน Web server อื่นที่ผมรันอยู่

ลองเข้า http://0.0.0.0:8000/ ควรจะต้องเห็นหน้าตาตามรูปที่ 2 แบบนี้ครับ

Welcome to Nginx

รูปที่ 2: Welcome to Nginx

ถึงเวลาจับ Nginx กับ Django มาร่วมวงกันแหละ เราจะใช้ Docker Compose กัน ให้สร้างไฟล์ชื่อ docker-compose.yml ขึ้นมา แล้วใช้โค้ดตามนี้

version: '2'

services:
  nginx:
    image: nginx
    ports:
      - 8080:80
    depends_on:
      - app

  app:
    build:
      context: .
      dockerfile: Dockerfile

แล้วลองสั่ง docker-compose up -d (รันเป็น daemon) ดู แล้วสั่ง docker ps ดู

Docker Compose Up

รูปที่ 3: ผลจาก Docker Compose Up

ตามรูปที่ 3 จะเห็นได้ว่ามีทั้ง Nginx และ Django รันอยู่ เรายังคงเข้าไปที่ http://0.0.0.0:8000/ แต่หน้านั้นยังคงเป็นหน้า Welcome ของ Nginx ซึ่งเป้าหมายของเราคือให้หน้านั้นเป็น Hello, Docker! เราต้องเซต Nginx เพื่อให้มันสามารถไปต่อกับ uWSGI ที่เรารันอยู่ได้ ให้สร้างไฟล์ hellodocker.conf ขึ้นมา

upstream django {
    http://app:8000;
}

server {
    listen 80;
    server_name 127.0.0.1;
    charset utf-8;

    location / {
        proxy_pass django;
        include /etc/nginx/uwsgi_params;
    }
}

สังเกตว่าตรง upstream ผมใช้คำว่า app คำๆ นี้ผมได้มาจากชื่อ service ที่ผมตั้งไว้ใน docker-compose.yml ครับ ซึ่ง service ที่ชื่อ nginx มัน depends_on service ที่ชื่อ app เราจะสามารถใช้คำว่า app ใน Nginx ได้ครับ Docker Compose มัน link แต่ละ container ให้เราอัตโนมัติ แจ่มจริงๆ ชีวิตสบายขึ้นเยอะ

แล้วก็สร้างไฟล์ uwsgi_params ตามนี้

uwsgi_param QUERY_STRING $query_string;
uwsgi_param REQUEST_METHOD $request_method;
uwsgi_param CONTENT_TYPE $content_type;
uwsgi_param CONTENT_LENGTH $content_length;

uwsgi_param REQUEST_URI $request_uri;
uwsgi_param PATH_INFO $document_uri;
uwsgi_param DOCUMENT_ROOT $document_root;
uwsgi_param SERVER_PROTOCOL $server_protocol;
uwsgi_param REQUEST_SCHEME $scheme;
uwsgi_param HTTPS $https if_not_empty;

uwsgi_param REMOTE_ADDR $remote_addr;
uwsgi_param REMOTE_PORT $remote_port;
uwsgi_param SERVER_PORT $server_port;
uwsgi_param SERVER_NAME $server_name;

เราจำเป็นต้องมี uwsgi_params เพื่อให้ Nginx ส่งค่าต่างๆ เหล่านี้ไปที่ uWSGI เพื่อให้จัดการ request ที่เข้ามาได้ถูกต้อง (โดยปกติแล้ว Nginx จะไม่สามารถ process พวก dynamic content ได้ จะต้องหาตัวช่วยสักตัวมาทำหน้าที่ตรงนั้น ซึ่งในที่นี้คือ uWSGI) ตามไปอ่านว่าตัวแปรแต่ละตัวคืออะไรได้ที่ uwsgi protocol magic variables

ต่อมาเราจะเอาไฟล์ configuration พวกนี้เข้าไปอยู่ใน Nginx image ได้อย่างไร? เราจะทำโดยใช้ Dockerfile ครับ แต่คราวนี้ให้ตั้งชื่อให้ต่างกับ Dockerfile ที่มีอยู่แล้ว ผมขอตั้งชื่อเป็น Dockerfile.nginx

FROM nginx

RUN rm /etc/nginx/conf.d/default.conf
COPY uwsgi_params /etc/nginx/uwsgi_params
COPY hellodocker.conf /etc/nginx/conf.d/hellodocker.conf

CMD /usr/sbin/nginx -g "daemon off;"

ไฟล์เริ่มเยอะ เดี๋ยวจะเริ่มงง เราย้อนกลับมาดูโครงสร้างของโปรเจคกันหน่อยดีกว่า

.
└── django-with-docker
     ├── hellodocker
     |   ├── hellodocker
     |   │   ├── __init__.py
     |   │   ├── settings.py
     |   │   ├── urls.py
     |   |   ├── views.py
     |   │   └── wsgi.py
     |   └── manage.py
     ├── Dockerfile
     ├── Dockerfile.nginx
     ├── docker-compose.yml
     ├── hellodocker.conf
     ├── hellodocker.ini
     ├── requirements.txt
     └── uwsgi_params

ถ้าใครมีหน้าตาโครงสร้างของโปรเจคไม่ค่อยเหมือน ลองทำให้เหมือนก่อนนะครับ แล้วค่อยทำต่อ

ต่อไปให้เราแก้ docker-compose.yml เพื่อเปลี่ยน Nginx จากเดิมที่ใช้ base image ให้มาใช้ Dockerfile.nginx แทน จะได้ประมาณนี้

version: '2'

services:
  nginx:
    build:
      context: .
      dockerfile: Dockerfile.nginx
    ports:
      - 8080:80
    depends_on:
      - app

  app:
    build:
      context: .
      dockerfile: Dockerfile

เสร็จแล้วลองสั่ง docker-compose up -d (ถ้าใช้ docker-compose down จะเป็นการ stop & remove ตัว container ที่รันอยู่นะครับ)

ณ จุดนี้เราควรจะเห็น Hello, Docker! บนหน้าเว็บแทน Welcome ที่มาจาก Nginx แล้ว

เชื่อมต่อกับ PostgreSQL

ให้เราไปที่ไฟล์ settings.py ในของ hellodocker แล้วแก้ตรงส่วน DATABASES เป็นตามนี้

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'hellodocker',
        'USER': 'postgres',
        'PASSWORD': '',
        'HOST': 'postgres',
        'PORT': '5432',
    }
}

สังเกตว่าตรง HOST ผมใช้ postgres เฉยๆ ไม่ได้ใช้ IP หรือ URL ต่อไปยังเครื่อง database ซึ่งถ้ายังจำกันได้ข้างต้นตรง upstream ของ Nginx ตรงนี้คงจะเดากันได้ไหมว่าผมจะเขียน docker-compose.yml อย่างไร? 🙂 ผมจะใส่ depends_on ตรง app ชี้ไปยัง service ที่ชื่อ postgres ครับ ตามนี้เลย

version: '2'

services:
  nginx:
    build:
      context: .
      dockerfile: Dockerfile.nginx
    ports:
      - 8080:80
    depends_on:
      - app

  app:
    build:
      context: .
      dockerfile: Dockerfile
    depends_on:
      - postgres

  postgres:
    image: postgres

เรียบร้อย

บทความนี้เราจะใช้ base image ของ PostgreSQL เลย (ถ้าใครอยากปรับแต่งก็ให้ทำคล้ายๆ กับที่เราทำกับ Nginx ครับ คือสร้างไฟล์ configuration มา แล้วสร้าง Dockerfile.postgres เพื่อเอา configuration ของเราใส่เข้าไปใน image)

เสร็จแล้วลองสั่ง docker-compose up --build ดูครับ (ไม่ต้องใส่ -d) ที่ใส่ --build ก็เพราะเราอยากให้ build ตัว Django ของเราใหม่ครับ เนื่องจากมีการแก้โค้ด

ทีนี้ระหว่างที่กำลัง up อยู่ เราน่าจะเห็น error message ประมาณนี้

app_1 | django.core.exceptions.ImproperlyConfigured: Error loading psycopg2 module: No module named psycopg2

นั่นเป็นเพราะเรายังไม่มี Python module ที่เอาไว้เชื่อมต่อกับ PostgreSQL ครับ เราก็ต้องเพิ่ม psycopg2==2.6.2 เข้าไปใน requirements.txt ด้วย เพื่อ Django ให้ต่อกับ PostgreSQL ได้ เสร็จแล้วลองสั่ง docker-compose up --build อีกรอบ error message ข้างต้นน่าจะหายไปแล้ว เย้

ทีนี้เราต้องเข้าไปสร้างฐานข้อมูล หรือ database ก่อน ให้เราเข้าไปยัง container ที่รัน PostgreSQL อยู่ สั่ง

$ docker-compose exec postgres bash

พออยู่ใน container แล้วเราก็สั่ง

$ su postgres
$ createdb hellodocker

พอเสร็จแล้วก็ exit ออกมา ทีนี้วิธีทดสอบง่ายๆ ว่า Django ของเราต่อ PostgreSQL ได้จริงหรือเปล่า ให้เรา exec เข้าไปยัง container ที่รัน Django อยู่ สั่ง

$ docker-compose exec app bash

เราก็จะเข้ามาใน container ตัวนั้น ต่อไปให้สั่ง

$ cd hellodocker
$ python manage.py migrate

ถ้าต่อ PostgreSQL ได้ เราจะได้ข้อความตามรูปที่ 4 ด้านล่างนี้

Django Database Migrations

รูปที่ 4: Django Database Migrations

ทีนี้เรื่อง database เวลาที่เรามีการอัพเดทตัว image ของ PostgreSQL แล้วรัน container ใหม่ ข้อมูลเราจะหาย วิธีแก้คือให้สร้าง volumes แล้ว mount ออกมายังเครื่อง host ครับ อ่าน Dockerizing PostgreSQL หรือ Postgres in Docker persistent data ดูครับ

แล้วก็อีกปัญหาหนึ่งที่เคยเจอตอนเวลาใช้ Docker Compose คือ แต่ละ service ตอนที่เปิดขึ้นมา อาจจะไม่ทันกัน เช่น อย่าง app ของเราต้องการให้ database เปิดขึ้นมาก่อน แล้วถึงค่อยเริ่มรัน app แต่เวลาใช้ Docker Compose บางที app ของเรารันขึ้นมาก่อนที่ database จะทำงาน ทำให้เกิดข้อผิดพลาด ถ้าเจอกรณีแบบนี้ให้ลองดูวิธีจากบทความนี้ครับ Controlling startup order in Compose

จบ! เราได้ decouple ระบบของเราออกมาเป็น 3 ส่วนคือ Django, Nginx และ PostgreSQL ทีนี้เวลาที่เราอยากจะอัพเกรดอะไร หรือเปลี่ยนแปลงอะไร เราก็เปลี่ยนแค่ส่วนนั้นๆ พอ และการทำให้สภาพแวดล้อมในช่วง development เหมือนกับ production ก็ไม่ใช่แค่ความหวังลมๆ แล้งๆ อีกต่อไปครับ สามารถทำได้จริง (แต่ต้องไปต่อยอดจากบทความนี้กันเองนะครับ)

Happy Dockerizing! 🙂

ปล. ขอบคุณรูปจาก Docker, Django, Nginx และ รูป PostgreSQL จาก Logonoid


Kan Ouivirach

Kan Ouivirach

Lead Software Architect

Being interested in Agile software development, I joined an Agile team at Pronto Tools as a Research & Development Architect (as Lead Software Architect now). I am an enthusiastic architect who not only has a scientific mindset, but also a practical approach to software solutions.

  • nuboat in wonderland

    เยี่ยมเลยครับ