Django + Docker

7 minut(y)

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

Czym jest 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. Docker w chwili obecnej dostępny jest dla najpopularniejszych systemów operacyjnych taki 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ą 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 będzie 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.

# Dockerfile_django
FROM python:3.7
ENV PYTHONUNBUFFERED 1
ADD requirements.txt /
RUN pip install -r /requirements.txt
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 3), 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 uruchamienie 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: '3'
 3 volumes: 
 4   local_postgres_data: {}
 5 
 6 services:
 7   django:
 8     build: 
 9       context: .
10       dockerfile: Dockerfile_django
11     depends_on: 
12       - postgres
13     volumes:
14       - .:/app
15     ports:
16       - "8000:8000"
17     command: python manage.py runserver 0.0.0.0:8000
18 
19   postgres:
20     image: postgres:latest
21     volumes:
22       - local_postgres_data:/var/lib/postgresql/data

Kontener postgres będzie oparty na domyślnym najnowszym obrazie PostgresSQL (linia 20), do przechowywania danych wykorzystywany będzie wewnętrzny nazwany wolumen (local_postgres_data), który to będzie podmontowany jako katalog /var/lib/postgresql/data wewnątrz tego kontenera (linia 22).

Obraz kontenera django zostanie zbudowany w oparciu o polecenia z pliku Dockerfile_django (linia 10), do swojej pracy będzie wymagał działającego kontenera postgres (linia 12), bieżący katalog projektu (.) zostanie wewnątrz kontenera podmontowany w katalogu /app (linia 14). Port 8000 kontenera zostanie zmapowany jako port 8000 w naszym systemie operacyjnym (linia 16), a w momencie uruchomienia kontenera zostanie wykonane polecenie python manage.py runserver (linia 17).

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

$ docker-compose build

Startujemy z projektem

Mając już zbudowany obraz django (wewnątrz kontenera mamy zainstalowany framework django w wersji przynajmniej 2.1) możemy rozpocząć proces tworzenia projektu i pierwszej aplikacji. Wszystko to będziemy wykonwywać 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 (a także uruchomienie kontenera postgres poprzez system zależności), w kontenerze tym zostanie uruchomione polecenie django-admin, które będzie miało za zadanie stworzyć szkielet z kodem projektu. Gdy polecenie django-admin się wykona (zkończy działanie), to kontener django zakończy działanie i zostanie usunięty (–rm). 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 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. Korygujemy uprawnienia 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 wykorzystwał 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 naszego 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 powiniśmy otrzymać stronę zbliżoną do poniżeszej:

Django Is Running

Rodzi się tutaj pytanie skąd docker-compose “wie” co w tym wypadku uruchomić w poszczególnych kontnerach przy ich startowaniu? Otóż 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.

W tym miejscu w konsoli cały czas mamy uruchomiony docker-compose, więc musimy 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.

Możemy stworzyć w naszym projekcie szkielet pierwszej aplikacji przy pomocy narzędzi django poprzez uruchomienie polecenia w kontenerze: docker-compose run –rm django python manage.py startapp todo lub możemy tworzyć ręcznie w katalogu projektu poszczególne pliki aplikacji.

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