W tym artykule pokażę Ci po krótce:

  1. dlaczego warto mieć aplikację w kontenerze;
  2. jak skonteneryzować aplikację w Django, czyli móc uruchamiać ją w Dockerze;
  3. jak jedną komendą postawić tę aplikację razem z bazą danych PostgreSQL, używając Docker Compose.

Sprawdź też film, w którym dokładnie tłumaczę ten proces 👇.

Dlaczego?

Jest kilka zalet umieszczenia aplikacji w kontenerze.

Po pierwsze, ułatwiamy sobie i innym członkom zespołu uruchomienie aplikacji. Mamy wszystkie zależności — takie jak biblioteki, ale też bazę danych i inne serwisy — oraz konfigurację w jednym miejscu. Nie ma więc problemu z ich instalacją i dostosowaniem na maszynie każdej kolejnej osoby, która ma pracować z naszą aplikacją.

Po drugie, rozwiązujemy też problem z wdrożeniem i utrzymywaniem aplikacji na serwerze. Od teraz aktualizowanie bibliotek i zarządzanie konfiguracją nie będzie stanowiło dla nas problemu.

👉 Więcej o zaletach konteneryzacji

Aplikacja w Django

Dla przykładu użyję pustej aplikacji w Django.

Jeżeli masz już swoją aplikację to przejdź dalej TODO link do następnego headera. Nie bój się, jedyna zmiana, jaką zrobimy w tej aplikacji, to dostosowanie konfiguracji. To dlatego, że przeniesiemy bazę danych do kontenera. Istniejące widoki, szablony, modele itd. pozostaną nienaruszone.

Postępuję więc zgodnie z instrukcją ze strony Django.

Instaluję Django (komenda w wierszu poleceń):

pip install Django

Tworzę katalog o nazwie django_docker_compose na nasze pliki, a w nim nowy projekt Django:

django-admin startproject moj-projekt

I w taki sposób uzyskaliśmy podstawowe pliki:

Struktura katalogów w projekcie

Lista zależności

Tworzę plik requirements.txt w głównym katalogu. Zapiszę w nim listę paczek pythonowych dla naszej aplikacji.

Django==3.1.5
psycopg2-binary==2.8.6

Oczywiście potrzebuję Django, a dodatkowo bibliotekę do połączenia z Postgresem psycopg2. Żeby ułatwić sobie zadanie, wykorzystam paczkę psycopg2-binary, która zawiera od razu skompilowane rozszerzenia. Dzięki temu nie będę musiał bawić się z narzędziami potrzebnymi do kompilacji.

Konteneryzacja aplikacji w Django

Przejdźmy do stworzenia obrazu kontenera dla naszej aplikacji. Jeżeli dopiero zaczynasz z kontenerami, wspomóż się moim filmem tłumaczącym jak pisać Dockerfile.

Tworzę plik Dockerfile w głównym katalogu i lecimy!

FROM python:3.8-slim

Zaczynam od obrazu python:3.8-slim. Wersję 3.8 wybieram, bo jest najnowsza w momencie pisania tego artykułu :). Wariant slim wybieram, bo jest to odchudzona wersja obrazu (o mniejszym rozmiarze na dysku). Nie wybieram obrazu alpine ze względu na pewne problemy, które może sprawiać z Pythonem.

WORKDIR /app

Będę umieszczał cały kod w katalogu /app.

COPY requirements.txt .
RUN pip install -r requirements.txt

Wrzucam plik requirements.txt i instaluję zawarte w nim paczki.

Robię to, zanim wrzucę resztę kodu, żeby wykorzystać cache warstw w obrazie. Zazwyczaj kod aplikacji będzie się zmieniał częściej niż sama lista używanych bibliotek. W związku z tym nie chcę, żeby po każdej zmianie kodu trzeba było od nowa instalować wszystkie paczki, co może trwać dosyć długo — paczki są ściągane z internetu.

Jeżeli dam tę instrukcję na początku, warstwa obrazu stworzy się raz i będzie używana do budowy wszystkich następnych obrazów. Oczywiście dopóty, dopóki nie zmieni się zawartość pliku requirements.txt. Więcej o budowie obrazów znajdziesz tutaj.

COPY moj_projekt .

Mogę teraz z czystym sumieniem wrzucić kod naszej aplikacji do obrazu.

CMD python manage.py migrate && \
    python manage.py runserver 0.0.0.0:8080

Komendą CMD określam jaki program (komenda) będzie wywołana przy uruchomieniu kontenera z tego obrazu. W moim przypadku oczywiście uruchamiam serwer Django, wcześniej tylko upewniając się, że wszystkie migracje zostały wgrane do bazy.

Ponieważ używam tutaj serwera deweloperskiego (manage.py runserver), taki kontener nie będzie nadawał się do wrzucenia na produkcję, czyli otwarcia na ruch publiczny z zewnątrz. Spokojnie jednak wystarczy do uruchamiania środowiska lokalnie przez deweloperów (na ich własnych komputerach).

Do zastosowań produkcyjnych powinienem użyć serwera aplikacyjnego, np. gunicorn. Tak robię w filmie powyżej 👆

EXPOSE 8080

I na koniec instrukcja EXPOSE, która pełni głównie funkcję informacyjną. Daje użytkownikowi tego obrazu znać, że usługa jest dostępna w kontenerze na porcie 8080.

Plik Dockerfile w całości:

FROM python:3.8-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY moj_projekt .

CMD python manage.py migrate && \
    python manage.py runserver 0.0.0.0:8080
EXPOSE 8080

Docker Compose — Django + Baza danych

Skoro mamy gotowy obraz dla naszej aplikacji, od razu napiszemy też plik docker-compose.yml. Dzięki niemu będziemy jedną komendą uruchamiać wszystkie serwisy dla naszego systemu — i bazę, i aplikację w Django.

version: "3"

Zaczynam od wskazania wersji specyfikacji pliku Compose, z której będę korzystał. Wersja 3 jest w momencie pisania najnowsza (bez podawania konkretnej minor wersji jak 3.8, 3.9, itd.).

services:
  backend:
    ... Serwis backend
  database:
    ... Serwis database

Tworzę dwa serwisy (dwa kontenery) w moim systemie. Będą to: backend, czyli moja aplikacja w Django; oraz database, czyli baza danych Postgresql. Teraz wypełnię te klucze konfiguracją serwisów.

Serwis backend — kontener z aplikacją Django

Pod kluczem backend: dodaję kolejno:

  build: .

Kluczem build informuję Compose, że ten serwis będzie tworzony na podstawie Dockerfile, znajdującego się w obecnym katalogu (czyli katalogu . - kropka). Inaczej mówiąc, obraz dla tego serwisu będzie musiał być zbudowany przed jego uruchomieniem.

  ports:
    - "9000:8080"

Klucz ports umożliwi mi przekierowanie portów z kontenera na hosta. W moim wypadku podpinam port 9000 na hoście ( pierwsza wartość), tak by przerzucał ruch na port 8080 w kontenerze (druga wartość).

  depends_on:
    - database

Ponieważ moja aplikacja potrzebuje bazy danych do działania, chcę, żeby serwis backend był uruchamiany po serwisie database. Nie ma sensu uruchamiać mojej aplikacji bez bazy danych — wtedy i tak tylko otrzymam błąd.

Do zapisania takiego założenia dla Compose służy klucz depends_on. W ten sposób proszę, by ten serwis był uruchamiany dopiero po tym, jak zostanie uruchomiony serwis database.

Uwaga: depends_on bierze pod uwagę wyłącznie start samego kontenera, a nie to czy program w środku (np. baza danych) jest gotowy do działania. Jeżeli aplikacja za szybko będzie próbowała się połączyć z bazą, to czasami może to powodować problemy.

Wtedy trzeba opóźnić jej start w kontenerze, na przykład skryptem, który poczeka, aż baza zacznie przyjmować połączenia od klientów. Więcej o kolejności uruchamiania kontenerów w dokumentacji Dockera tutaj.

Serwis database – obraz Postgres

Dla serwisu database potrzebujemy poniższych kluczy:

  image: postgres:13.1

Inaczej niż w przypadku mojej aplikacji, tutaj korzystam z gotowego obrazu. Za pomocą klucza image informuję Compose, żeby nie budował nowego obrazu, a użył już istniejącego, o podanej nazwie. Mógłby to być zarówno obraz, który zbudowałem lokalnie (na przykład ręcznie), jak i obraz z DockerHuba — tak będzie w tym przypadku.

  environment:
    - POSTGRES_PASSWORD=tajnehaslo
    - POSTGRES_USER=uzytkownik
    - POSTGRES_DB=baza

Kluczem environment konfiguruję zmienne środowiskowe, które mają zostać ustawione w kontenerze. To jakie konkretnie zmienne muszę ustawić, zależy od obrazu, z którego korzystam. Dla każdej aplikacji będą one inne.

Jeżeli chodzi o obraz postgres:13.1, to ma on ustawione jakieś domyślne wartości. Ja jednak zdecydowałem się zmienić nazwę bazy, użytkownika i jego hasło za pomocą odpowiednich zmiennych środowiskowych. Jakie zmienne powinienem ustawić, dowiedziałem się z opisu obrazu na DockerHubie (sekcja Environment Variables).

  volumes:
    - "dane:/var/lib/postgresql/data"

Tworzę wolumen o nazwie dane, który będzie przechowywał dane zapisane w bazie (w przypadku Postgresa jest to katalog /var/lib/postgresql/data). W ten sposób przechowam dane pomiędzy uruchomieniami systemu. Oczywiście każde uruchomienie tworzy nowy kontener, więc gdybym nie umieścił tych danych w wolumenie, to traciłbym zawartość bazy.

Ten wolumen zostanie automatycznie stworzony przez Compose i będzie istniał, dopóki usuniemy go ręcznie albo wydamy poleceniedocker-compose down -v [--volumes].

Cały plik Compose

Plik docker-compose.yml w całości prezentuje się tak:

version: "3"
services:
  backend:
    build: .
    ports:
      - "8000:8080"
    depends_on:
      - database

  database:
    image: postgres:13.1
    environment:
      - POSTGRES_PASSWORD=tajnehaslo
      - POSTGRES_USER=uzytkownik
      - POSTGRES_DB=baza

Uruchamianie aplikacji w Django i bazy danych

Teraz mogę uruchomić cały mój system jednym poleceniem, wydanym w tym katalogu, w którym jest plik docker-compose.yml:

docker-compose up --build

Polecenie up nakazuje uruchomienie całego systemu (w tym stworzenie potrzebnych sieci i wolumenów).

Flaga --build dodatkowo zleca przebudowanie (lub zbudowanie po raz pierwszy) obrazów, które są tworzone lokalnie na podstawie Dockerfile. Inaczej mówiąc, dotyczy tylko obrazów (build contextów) wymienionych w kluczach build: w pliku Compose.

Wyłączyć działające kontenery mogę wciskając CTRL+C lub wydając polecenie docker-compose down w drugim terminalu.

Docker + Compose + Django + Postgres

Dotarliśmy do końca! Udało mi się skonteneryzować i uruchomić moją aplikację.

Teraz wtajemniczanie nowych osób w zespole i wdrażanie aplikacji na produkcję stało się odrobinę łatwiejsze. 😎