W tym artykule pokażę Ci po krótce:
- dlaczego warto mieć aplikację w kontenerze;
- jak skonteneryzować aplikację w Django, czyli móc uruchamiać ją w Dockerze;
- 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:
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. 😎