W ciągu ostatnich kilku dni podjąłem się przeniesienia aplikacji portalu sędziów PLQ z hostingu sloppy.io na mój własny serwer ze względu na chęć obniżenia kosztów i zwiększenia dostępnych zasobów.
W tym celu musiałem się zapoznać z systemem docker-compose i skonfigurować reverse proxy, bo w przyszłości na tym serwerze usiądzie więcej moich projektów.
Postanowiłem stworzyć post, który będzie niejako bazą wiedzy do robienia podobnych konfiguracji w przyszłości.
TL:DR; Mam projekt z trzema usługami w kontenerach, z których aplikacja WWW jest połączona ze światem zewnętrznym przy pomocy reverse proxy NGINX, a do tego całość działa z HTTPS i certyfikatem od Let’s Encrypt.
Co to docker
Docker jest systemem zarządzania kontenerami, które same w sobie są zapewniane przez jądro systemu operacyjnego. Obecnie zarówno Linux jak i Windows pozwalają na tworzenie kontenerów.
Aplikacja żyjąca w kontenerze to taka aplikacja, która działa na tym samym jądrze - czyli sercu naszego systemu, co nasz główny system i aplikacje, ale ma specyficznie dobrany system plików - organy, który jest z nią związany - tworzą razem obraz. Ten obraz możemy przenosić między komputerami i na każdym z nich aplikacja powinna działać tak samo. Jądro zapewnia izolację kontenera i musimy użyć dodatkowych mechanizmów, aby móc komunikować się między tymi izolowanymi środowiskami.
Docker jest jednym z tych mechanizmów - pozwala na tworzenie obrazów i kontenerów, tworzenie sieci, którymi będą one połączone, i wykonywanie poleceń wewnątrz kontenera.
Instalacja (Ubuntu)
Poniżej krótki skrypt instalacji na Ubuntu (lub pochodnym Linux Mint). W celu instalacji na innej dystrybucji należy odnieść się do instrukcji z dokumentacji.
# instalacja zależności
sudo apt-get install apt-transport-https ca-certificates curl software-properties-common
# dodanie repozytorium dockera
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - sudo apt-key fingerprint 0EBFCD88
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
# instalacja dockera
sudo apt-get update
sudo apt-get install docker-ce
Następnie będziemy chcieli dodać się do grupy docker
aby nie musieć uruchamiać poleceń dockera jako root.
sudo usermod -a -G docker <user>
Używanie dockera
W tym poście nie będę pisał o tym jak używać dockera. Napisałem o tym kiedyś post, przy czym obecnie część jego zawartości się przeterminowała. Jedyne polecenie którego czasem użyłem to
docker inspect <container>
które dostarcza nam szczegółowych informacji, o uruchomionym kontenerze (m.in. adres IP).
Docker compose - określa ustawienia dockera
System dockera jest spoko i bardzo łatwo się go używa do jednorazowego uruchomienia jednego obrazu. Ale czasem tworzymy aplikacje, które są bardziej zaawansowane. Wtedy chcemy mieć prosty system konfiguracji wielu kontenerów.
I to nam właśnie dostarcza docker-compose. Zapisujemy konfigurację systemu w pliku YAML, wywołujemy jedno polecenie i system tworzy wszystko co potrzebujemy za nas.
Instalacja
Musimy mieć zainstalowany docker.
sudo curl -L "https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
Podejście projektowe
Możemy podzielić wszystkie kontenery w naszym systemie na projekty.
Np. portal sędziów PLQ to projekt o nazwie plqref
, który zawiera w sobie trzy usługi:
- baza danych MySQL
- aplikacja ASP.NET Core
- phpmyadmin - do zarządzania danymi bazy MySQL
Każdy projekt ma swój oddzielny plik docker-compose.yml
, który określa jego konfigurację.
Podział aplikacji na usługi
Moglibyśmy całą naszą aplikację, wraz z bazą danych i innymi potrzebnymi elementami umieścić w jednym kontenerze. Ale wtedy przy dowolnej aktualizacji jednej z usług musimy zatrzymywać cały system, robić przebudowę obrazu, co jest czasochłonne.
Dlatego dzielimy ją na usługi, które komunikują się przez sieć.
Co siedzi w pliku docker-compose.yml
?
Polecam zapoznać się z dokumentację tego pliku, ale wytłumaczenie na przykładzie na pewno pomoże.
# wersja opisuje schemat konfiguracji
version: '3'
services:
# na tym poziomie wymyślamy nazwę usługi
database:
# moje usługi bazują na gotowych obrazach
# ale mogą też na bieżąco być budowane
image: mysql:5.6
# exponujemy port do innych kontenerów w sieci
# ale nie dla hosta
expose: ["3306"]
# ustalamy zmienne środowiskowe
environment:
MYSQL_ROOT_PASSWORD: "root"
# podpinamy <folder hosta>:<folder kontenera>
# żeby po usunięciu kontenera dane zostały zachowane
volumes:
- ./volumes/database:/var/lib/mysql
# określamy do jakich sieci należy kontener
networks:
- plqlocal
dbadmin:
image: phpmyadmin/phpmyadmin:latest
# udostępniamy port z wewnątrz kontenera do hosta
ports: ["300:80"] # host:container
# inny sposób zapisu zmiennych środowiskowych
environment:
- PMA_HOST=mysql
- MYSQL_ROOT_PASSWORD=root
# nadajemy alias sieciowy usłudze
links:
- database:mysql # service:alias
networks:
- plqlocal
app:
image: manio143/plqref:latest
ports: ["500:443"]
# żeby podpiąć pojedynczy plik do naszego systemu
# musi on mieć absolutną ścieżkę
volumes:
- ${PWD}/volumes/app/config.json:/var/app/appsettings.json
links:
- database:mysql
networks:
- proxy_global
- plqlocal
# definiujemy sieci dla projektu
networks:
# ta sieć odwołuje się do zewnętrznego projektu
# projekt proxy <- sieć o nazwie global
proxy_global:
external: true
plqlocal:
Łączność między usługami
Usługi wewnątrz jednego projektu są łączone przy użyciu domyślnie tworzonej sieci, jeśli sami jej nie zadeklarujemy.
Możemy tworzyć dowolnie wiele aliasów dyrektywą links:
, aby ułatwić sobie konfigurację. Dodatkowo jeśli mamy zewnętrzną sieć to możemy też użyć dyrektywy external_links:
i wskazać konkretny kontener (np. plqref_app_1
, czyli <project>_<service>_<instance>
).
Musimy pamiętać, aby upublicznić odpowiednie porty, których używają nasze aplikacje.
Bramka na świat - reverse proxy
Więc mam teraz kilka projektów, z których część będzie potrzebowała dostępu z zewnątrz. Mógłbym upublicznić je na różnych portach, ale to nie jest wygodne. Wobec tego chcę postawić proxy przed aplikacjami, które będzie działało na publicznych portach http:80
i https:443
, a następnie przekierowywało lokalnie żądania do odpowiednich aplikacji.
Czym te żądania się będą różnić? Parametrem Host
protokołu HTTP, czyli innymi słowy domeną.
Więc utworzyłem nowy projekt docker-compose, w którym uruchomię usługę NGINX
version: '3'
services:
nginx:
image: nginx
volumes:
# folder na pliki konfiguracji
- ./conf:/etc/nginx
# folder na certyfikaty SSL
- /etc/letsencrypt/:/etc/certs
ports:
- "80:80"
- "443:443"
external_links:
- plqref_app_1:plqref
networks:
- global
networks:
global:
Widzimy zadeklarowaną sieć global
, do której należy moja aplikacja w projekcie plqref
. Dzięki temu jestem w stanie się z nią przez to proxy komunikować.
Konfiguracja NGINX
W folderze conf
, który mapuje się na folder /etc/nginx
tworzę plik nginx.conf
worker_processes 1;
events { worker_connections 1024; }
http {
# określamy serwery zewnętrzne
upstream plqref {
server plqref:443;
}
# port 80 przekierowywuje na HTTPS
server {
listen 80;
return 301 https://$host$request_uri;
}
# robimy po jednym wpisie server na aplikację
server {
# nasłuchujemy na porcie 443, używając ssl i jeśli się da to http2
listen 443 ssl http2;
# domena dla tej aplikacji
server_name ref.polskaligaquidditcha.pl;
# certyfikaty
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
# nie weryfikuj certyfikatu aplikacji (może być self signed)
ssl_verify_client off;
location / {
proxy_pass https://plqref; # tu podajemy nazwę upstream
proxy_set_header Host $host; # przekaż informację o domenie
# oraz dodaj informacje o żądaniu
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
}
}
Let’s Encrypt
Żeby nasza strona była bezpieczna przed wieloma różnymi atakami, musimy mieć włączone HTTPS. Ale żeby to działało to potrzebny nam jest certyfikat, który przeglądarka zaakceptuje.
Na szczęście za nami już czasy, kiedy za certyfikat trzeba płacić, a jego instalacja jest skomplikowana.
Let’s Encrypt pozwala jednym poleceniem zdobyć certyfikat, a także utworzyć zadanie automatycznego odnawiania certyfikatu, który ważny jest przez 3 miesiące.
Instalacja
Let’s Encrypt używa pythona i virtualenv, więc jeśli możliwe, że będziemy musieli dopisać universe
do naszych źródeł apt-get
, co zostało opisane tutaj.
Sprawdzimy czy nasza domena wskazuje na nasz serwer (musimy zedytować wpis DNS i włączyć przekierowanie portów jeśli serwer stoi za routerem), bo Let’s Encrypt wykonuje walidację czy my posiadamy domenę dla której chcemy dostać certyfikat.
Należy się jeszcze upewnić, że port 80 jest wolny (zatrzymać nasze proxy).
Następnie wykonamy
git clone https://github.com/letsencrypt/letsencrypt
cd letsencrypt
./letsencrypt-auto certonly --standalone --email <email> -d <domena>
Zostaną zainstalowane potrzebne pakiety i zainstalowany w systemie certyfikat.
Podpinamy certyfikat
Zostało nam wtedy wpisanie właściwej ścieżki do konfiguracji NGINX, dlatego w jego docker-compose.yml
jest dodany wolumen /etc/letsencrypt
, gdzie w folderze live/<domena>
znajduje się certyfikat.