Django + Docker

8 minut(y)

W tym wpisie chciałbym przedstawić Wam w jaki sposób zbudować od podstaw aplikację Django z wykorzystaniem Dockera i Docker Compose. Będzie to bardzo prosta aplikacja, a w zasadzie jej zalążek, która będzie współpracowała z bazą danych PostgreSQL, a przy pisaniu jej kodu będziemy wykorzystywali kontenery Dockera.

Czym jest Docker?

Na początku zaczniemy od wyjaśnienia czym jest sam Docker. Docker jest to otwarta platforma, która realizuje wirtualizację na poziomie systemu operacyjnego zwaną także kontenerowaniem. Docker daje możliwość uruchamiania aplikacji w wydzielonym kontenerze bez konieczności emulowania warstwy sprzętowej i systemu operacyjnego. Każdy taki kontener posiada wydzielony obszar pamięci, odrębny interfejs sieciowy z własnym prywatnym adresem IP oraz wydzielony obszar na dysku, na którym znajduje się zainstalowany obraz systemu operacyjnego wraz z zależnościami i bibliotekami potrzebnymi do działania aplikacji.

Instalacja Dockera oraz docker-compose

Pierwszym naszym zadaniem jest instalacja Dockera oraz docker-compose w systemie operacyjnym. Docker w chwili obecnej dostępny jest dla najpopularniejszych systemów operacyjnych takich jak Windows, MacOS, Linux i jego instalacja nie powinna stanowić problemu. Po szczegóły dotyczące instalacji dla konkretnej wersji systemu operacyjnego polecam wejść na stronę dokumentacji Dockera oraz dokumentacji docker-compose.

Jeśli wszystko wykonamy zgodnie z dokumentacją, to powinniśmy uzyskać wynik działania poleceń podobny do poniższego:

$ docker version
Client:
Version: 18.06.1-ce
API version: 1.38
Go version: go1.11
Git commit: e68fc7a215
Built: Fri Sep 7 11:26:59 2018
OS/Arch: linux/amd64
Experimental: false

Server:
Engine:
Version: 18.06.1-ce
API version: 1.38 (minimum version 1.12)
Go version: go1.11
Git commit: e68fc7a215
Built: Fri Sep 7 11:26:11 2018
OS/Arch: linux/amd64
Experimental: false

$ docker-compose version
docker-compose version 1.22.0, build unknown
docker-py version: 3.5.1
CPython version: 3.7.1
OpenSSL version: OpenSSL 1.1.1 11 Sep 2018

Przygotowujemy podwaliny pod naszą aplikację

Zaczniemy od utworzenia pliku requirements.txt zawierającego spis wymaganych pakietów języka Python, które będą nam niezbędne do uruchomienia aplikacji. Plik ten wykorzystamy za chwilę w procesie budowania obrazu kontenera dla naszej aplikacji.

# requirements.txt
django>=2.1
psycopg2-binary

W kolejnym kroku tworzymy plik Dockerfile (Dockerfile_django), który zawierał będzie “przepis” na zbudowanie obrazu kontenera naszej aplikacji.

1 # Dockerfile_django
2 FROM python:3.7
3 ENV PYTHONUNBUFFERED 1
4 ADD requirements.txt /
5 RUN pip install -r /requirements.txt
6 WORKDIR /app

Analizując powyższy plik, możemy zauważyć, że obraz kontenera django będzie oparty na obrazie python w wersji 3.7 (linia 2), do katalogu głównego dodany zostanie plik requirements.txt (linia 4), a następnie przy pomocy polecenia pip zostaną zainstalowane wszystkie niezbędne zależności (linia 5). Wszystkie te powyższe kroki wykonają się podczas budowanie obrazu kontenera.

Budujemy obrazy kontenerów

Podczas procesu programowania naszej aplikacji będziemy wykorzystywać dwa kontenery, jeden z nich będzie odpowiedzialny za uruchamianie naszej aplikacji (framework django), a drugi będzie odpowiedzialny za uruchomienie silnika bazy danych PostgreSQL, z którym będzie ona (aplikacja) współpracować. Definicję kontenerów umieścimy w pliku konfiguracyjnym docker-compose.yml w katalogu głównym naszego projektu:

 1 # docker-compose.yml
 2 version: '2'
 3 
 4 volumes:
 5   local_postgres_data: {}
 6 
 7 services:
 8   django:
 9     build:
10       context: .
11       dockerfile: Dockerfile_django
12     depends_on:
13       - postgres
14     volumes:
15       - .:/app
16     ports:
17       - "8000:8000"
18     command: python manage.py runserver 0.0.0.0:8000
19 
20   postgres:
21     image: postgres:latest
22     volumes:
23       - local_postgres_data:/var/lib/postgresql/data

Kontener postgres oprzemy na domyślnym najnowszym obrazie PostgresSQL (linia 21). Do przechowywania danych wykorzystamy wewnętrzny nazwany wolumen (local_postgres_data), który będzie podmontowany jako katalog /var/lib/postgresql/data wewnątrz kontenera (linia 23).

Obraz kontenera django zbudujemy w oparciu o wcześniej przygotowany plik Dockerfile_django (linia 11). Kontener ten do swojej pracy będzie wymagał działającego kontenera postgres (linia 13), bieżący katalog projektu (./) zostanie wewnątrz kontenera podmontowany w katalogu /app (linia 15). Port 8000 kontenera zostanie zmapowany jako port 8000 w naszym systemie operacyjnym (linia 17), a w momencie uruchomienia kontenera zostanie wykonane polecenie python manage.py runserver (linia 18).

Wszystko co niezbędne mamy już gotowe, więc możemy przystąpić do właściwego procesu zbudowania obrazów kontenerów. Wydajmy polecenie budowania:

$ docker-compose build

Jeśli wcześniej nie był pobierany obrazy dockera dla pythona (i postgresa) to najpierw nastąpi jego pobranie i rozpakowanie. W kolejnym kroku zostanie do kontenera przekopiownay plik z zależnościami requirements.txt i w dalszej części przy pomocy polecenia pip zostaną pobrane i zainstalowane zależności języka Python wewnątrz kontenera.

Startujemy z projektem

Mając już zbudowany obraz kontenera django (wewnątrz kontenera zainstalowany jest framework django w wersji przynajmniej 2.1) możemy rozpocząć właściwy proces tworzenia projektu Django i pierwszej aplikacji. Wszystko to będziemy wykonywać poprzez wywoływanie poleceń z wnętrza kontenera. Zaczniemy od utworzenia projektu:

$ docker-compose run --rm django django-admin startproject mytodo .

Nastąpi teraz uruchomienie kontenera django na podstawie wcześniej zbudowanego obrazu (wraz z nim zostanie uruchomiony kontenera postgres poprzez zdefiniowaną zależność). W kontenerze tym zostanie wykonane polecenie django-admin, które stworzy szkielet z kodem projektu. Gdy polecenie django-admin się wykona swoje zadanie, to kontener django zakończy działanie i zostanie usunięty (–rm). W wyniku działania kontenra powinniśmy otrzymać następującą strukturę katalogów i plików w bieżącym katalogu projektu:

.
├── docker-compose.yml
├── Dockerfile_django
├── manage.py
├── mytodo
│   ├── **init**.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── requirements.txt

W tym miejscu, jeśli korzystamy z systemu Linux, zwracam jeszcze uwagę na uprawnienia plików. Właścicielem plików tworzonych wewnątrz kontenera (także na zewnętrznych wolumenach) będzie w tym przypadku użytkownik root (możemy się o tym przekonać wydając polecenie ls -l). Dzieje się tak, gdyż kontener uruchamiany jest domyślnie z poziomu użytkownika root i to on jest właścicielem tworzonych plików, pomimo tego, że samo uruchomienie kontenera nastąpiło z poziomu normalnego użytkownika. Uprawnienia do stworzonych plików i katalogów możemy skorygować poleceniem:

$ sudo chown -R $USER:$USER .

Połączenie z bazą danych

Przed pierwszym uruchomieniem projektu niezbędne jeszcze jest skonfigurowanie połączenia z bazą danych. Jak już wspomniałem wcześniej nasz projekt będzie wykorzystywał bazę danych PostgreSQL, która będzie uruchomiona w niezależnym kontenerze o nazwie postgres. Gdy korzystamy z docker-compose (o ile nie wyspecyfikujemy inaczej w pliku konfiguracyjnym) kontenery między sobą komunikują się przy pomocy wydzielonej wirtualnej sieci ethernet. Mamy do dyspozycji nazwy kontenerów jako nazwy DNS z których możemy korzystać przy wzajemnych odwołaniach.

W przypadku projektu Django konfiguracja połączenia z bazą danych znajduje się w pliku mytodo/settings.py w słowniku DATABASES. Odszukujemy słownik DATABASES i zmieniamy na następujące dane:

# mytodo/settings.py
...
DATABASES = {
  'default': {
    'ENGINE': 'django.db.backends.postgresql',
    'NAME': 'postgres',
    'USER': 'postgres',
    'HOST': 'postgres',
    'PORT': 5432,
  }
}
...

Wartości NAME oraz USER ustawione na postgres wynikają z domyślnej konfiguracji obrazu dla kontenera postgres (podobnie ma się rzecz z PORT - domyślnie kontener postgres oparty na obrazie postgres nasłuchuje na porcie 5432). Wartość HOST ustawiona na ‘postgres’ wynika z nazwy jakiej użyliśmy dla kontenera bazy danych w pliku docker-compose.yml.

Uruchamiamy kontenery

Po tych zmianach przyszła wreszcie pora na uruchomienie naszego całego projektu: W tym celu wykorzystamy komendę up docker-compose, która uruchomi nam wszystkie kontenery i pozostanie w oczekiwaniu na ich ewentualne zakończenie działania.

$ docker-compose up
Starting djangodocker_postgres_1 ... done
Starting djangodocker_django_1 ... done
Attaching to djangodocker_postgres_1, djangodocker_django_1
postgres_1 | 2018-11-02 15:54:53.138 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
postgres_1 | 2018-11-02 15:54:53.138 UTC [1] LOG: listening on IPv6 address "::", port 5432
postgres_1 | 2018-11-02 15:54:53.152 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
postgres_1 | 2018-11-02 15:54:53.201 UTC [25] LOG: database system was interrupted; last known up at 2018-11-02 15:49:55 UTC
postgres_1 | 2018-11-02 15:54:53.522 UTC [25] LOG: database system was not properly shut down; automatic recovery in progress
postgres_1 | 2018-11-02 15:54:53.536 UTC [25] LOG: redo starts at 0/16345B0
postgres_1 | 2018-11-02 15:54:53.536 UTC [25] LOG: invalid record length at 0/16345E8: wanted 24, got 0
postgres_1 | 2018-11-02 15:54:53.536 UTC [25] LOG: redo done at 0/16345B0
postgres_1 | 2018-11-02 15:54:53.717 UTC [1] LOG: database system is ready to accept connections
django_1 | Performing system checks...
django_1 |
django_1 | System check identified no issues (0 silenced).
django_1 |
django_1 | You have 15 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
django_1 | Run 'python manage.py migrate' to apply them.
django_1 | November 02, 2018 - 15:54:54
django_1 | Django version 2.1.3, using settings 'mytodo.settings'
django_1 | Starting development server at http://0.0.0.0:8000/
django_1 | Quit the server with CONTROL-C.

Uruchamiamy przeglądarkę i wpisujemy w pasku adresu http://127.0.0.1:8000. Jeśli wszystko poszło zgodnie z planem powinniśmy otrzymać stronę zbliżoną do poniższej:

Django Is Running

Rodzi się tutaj pytanie skąd docker-compose “wie” co w tym wypadku uruchomić w poszczególnych kontenerach przy ich starcie? W przypadku kontenera django mamy wprost podaną komendę do uruchomienia w pliku docker-compose.yml (linia 17, polecenie command). W przypadku kontenera postgres komenda do uruchomienia występuje w pliku Dockerfile użytym do zbudowania obrazu kontenera - ENTRYPOINT.

Po wydaniu powyższego polecenia w konsoli cały czas mamy uruchomiony docker-compose, więc możemy go przerwać poprzez naciśnięcie kombinacji klawiszy CONTROL-C. (Jednokrotne naciśnięcie CONTROL-C spowoduje rozpoczęcie procesu zatrzymywania kontenerów, co może czasami potrwać dłuższą chwile. Jeśli z jakiego powodu chcemy szybko “zabić” uruchomione kontenery to naciskamy ponownie CONTROL-C). Uruchamiając kontenery możemy do komendy up dodać przełącznik -d (detach) co spowoduje uruchomienie kontenerów w tle. W taki przypadku nie widzimy bezpośrednio na konsoli logów z ich działania - logi możemy oglądać na bieżąco np. na innym oknie konsoli poprzez wydanie polecenia: docker-compose logs -f

Teraz przyszła pora na wykonanie migracji (stosowny komunikat został zresztą wyświetlony podczas startu kontenera). W celu przeprowadzania migracji uruchomiamy kontener django i wewnątrz niego wykonujemy stosowne polecenie:

$ docker-compose run --rm django python manage.py migrate

Gdy wykonywanie migracji zostanie zakończone, możemy ponownie uruchomić kontenery poprzez up i przystąpić do modyfikowania kodu naszej aplikacji.

Szkielet pierwszej aplikacji w projekcie Django możemy teraz stworzyć przy pomocy narzędzi frameworka poprzez uruchomienie polecenia w kontenerze: docker-compose run –rm django python manage.py startapp todo. Nic nie stoi na przeszkodzie, aby samodzielnie stworzyć aplikację w katalogu projektu poprzez dodawania poszczególnych plików.

Jeśli wykonaliśmy polecenie startapp to struktura naszego projektu będzie wyglądać następująco:

.
├── docker-compose.yml
├── Dockerfile_django
├── manage.py
├── mytodo
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── requirements.txt
└── todo
├── admin.py
├── apps.py
├── __init__.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py

W tym miejscu mamy już w pełni gotowy i działający szkielet projektu do dalszego rozwoju.

Dla zainteresowanych kod źródłowy udostępniony jest na Gitlab. Zapraszam do samodzielnych eksperymentów i komentowania.

Zostaw komentarz