# Labo Docker
## Installatie Docker en Docker-Compose
Zoals aangehaald in de theorieles, is de installatie van Docker en Docker-compose erg eenvoudig.
Volg in [deze tutorial](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-18-04#step-3-%E2%80%94-using-the-docker-command) stap 1 & 2 om Docker te installeren. Volg daarna de eerste stap in [deze tutorial](https://www.digitalocean.com/community/tutorials/how-to-install-docker-compose-on-ubuntu-18-04#step-1-%E2%80%94-installing-docker-compose) uit om Docker-Compose te installeren
## Flask web app
In dit eerste deel ga je een simpele Flask applicatie maken. Je zorgt er voora dat deze applicatie "production-ready is".
Maak volgende directory structuur:
```
mkdir docker-labo
cd !$
mkdir web
cd !$
```
### Hello world app
Volgend stukje code is alles wat we nodig hebben om een Flask web applicatie te runnen. Maak dit bestand aan, noem het `main.py`.
```
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello world!'
if __name__ == '__main__':
app.run()
```
### Flask webserver
Flask heeft een zogenaamde [build-in development webserver](http://flask.pocoo.org/docs/1.0/server/). Deze server is prima zolang je aan het ontwikkelen bent, maar is niet bedoeld voor productie loads. Deze server kan bijvoorbeeld maximaal 1 request tegelijk behandelen.
Bij web applicaties, geschreven in Python is het gebruikelijk om de ["Web Server Gateway Interface"](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) (kortweg WSGI) te implementeren. [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) houdt zich aan de opgegeven conventies. Deze webserver is wel geschikt om in productie te gebruiken.
Maak volgend configuratie bestand aan, met als naam `app.ini`. Je hoeft niet elke lijn perfect te begrijpen.
```
[uwsgi]
; See docs: https://uwsgi-docs-additions.readthedocs.io/en/latest/Options.html
; Main app
module = main:app
; Enable uWSGI master process
master = true
processes = 5
; uWSGI socket
socket = 0.0.0.0:5000
; Try to remove all of the generated files/sockets (UNIX sockets and pidfiles) upon exit
vacuum = true
; Exit instead of brutal reload on SIGTERM.
die-on-term = true
; Force the specified protocol (uwsgi, http, fastcgi) for default sockets
protocol = http
```
### Docker image builden
Volgend `Dockerfile` is al wat we nodig hebben om een production-ready image te bouwen. Bekijk elke lijn aandachtig, achterhaal elke lijn.
```
FROM python:3.7-alpine
WORKDIR '/app'
RUN apk add --no-cache linux-headers g++
RUN pip install Flask
RUN pip install uwsgi
COPY ./ ./
RUN addgroup -S uwsgi && adduser -S uwsgi -G uwsgi
USER uwsgi
CMD ["uwsgi", "--ini", "app.ini"]
```
Omschrijf wat elke lijn precies doet.
> Antwoord
> ...
> ...
Om een image te bekomen, moeten we een zogenaamde build maken. De basis hiervoor is een `Dockerfile`. Met welk commando kan je dit (voer eveneens uit)?
> Antwoord
> ...
> ...
De output zou er ongeveer als volgt moeten uitzien:
```
Sending build context to Docker daemon 4.096kB
Step 1/9 : FROM python:3.7-alpine
---> 715a1f28828d
Step 2/9 : WORKDIR '/app'
---> Running in 5a89ec50f12f
Removing intermediate container 5a89ec50f12f
---> b4b9a1b589d9
Step 3/9 : RUN apk add --no-cache linux-headers g++
---> Running in a7ae43b9d84a
fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/community/x86_64/APKINDEX.tar.gz
(1/14) Installing libgcc (8.3.0-r0)
(2/14) Installing libstdc++ (8.3.0-r0)
(3/14) Installing binutils (2.31.1-r2)
(4/14) Installing gmp (6.1.2-r1)
(5/14) Installing isl (0.18-r0)
(6/14) Installing libgomp (8.3.0-r0)
(7/14) Installing libatomic (8.3.0-r0)
(8/14) Installing mpfr3 (3.1.5-r1)
(9/14) Installing mpc1 (1.0.3-r1)
(10/14) Installing gcc (8.3.0-r0)
(11/14) Installing musl-dev (1.1.20-r4)
(12/14) Installing libc-dev (0.7.1-r0)
(13/14) Installing g++ (8.3.0-r0)
(14/14) Installing linux-headers (4.18.13-r1)
Executing busybox-1.29.3-r10.trigger
OK: 177 MiB in 49 packages
Removing intermediate container a7ae43b9d84a
---> d8a760345c28
Step 4/9 : RUN pip install Flask
---> Running in c3001ba09be0
Collecting Flask
Downloading https://files.pythonhosted.org/packages/7f/e7/08578774ed4536d3242b14dacb4696386634607af824ea997202cd0edb4b/Flask-1.0.2-py2.py3-none-any.whl (91kB)
Collecting Werkzeug>=0.14 (from Flask)
Downloading https://files.pythonhosted.org/packages/18/79/84f02539cc181cdbf5ff5a41b9f52cae870b6f632767e43ba6ac70132e92/Werkzeug-0.15.2-py2.py3-none-any.whl (328kB)
Collecting Jinja2>=2.10 (from Flask)
Downloading https://files.pythonhosted.org/packages/1d/e7/fd8b501e7a6dfe492a433deb7b9d833d39ca74916fa8bc63dd1a4947a671/Jinja2-2.10.1-py2.py3-none-any.whl (124kB)
Collecting itsdangerous>=0.24 (from Flask)
Downloading https://files.pythonhosted.org/packages/76/ae/44b03b253d6fade317f32c24d100b3b35c2239807046a4c953c7b89fa49e/itsdangerous-1.1.0-py2.py3-none-any.whl
Collecting click>=5.1 (from Flask)
Downloading https://files.pythonhosted.org/packages/fa/37/45185cb5abbc30d7257104c434fe0b07e5a195a6847506c074527aa599ec/Click-7.0-py2.py3-none-any.whl (81kB)
Collecting MarkupSafe>=0.23 (from Jinja2>=2.10->Flask)
Downloading https://files.pythonhosted.org/packages/b9/2e/64db92e53b86efccfaea71321f597fa2e1b2bd3853d8ce658568f7a13094/MarkupSafe-1.1.1.tar.gz
Building wheels for collected packages: MarkupSafe
Building wheel for MarkupSafe (setup.py): started
Building wheel for MarkupSafe (setup.py): finished with status 'done'
Stored in directory: /root/.cache/pip/wheels/f2/aa/04/0edf07a1b8a5f5f1aed7580fffb69ce8972edc16a505916a77
Successfully built MarkupSafe
Installing collected packages: Werkzeug, MarkupSafe, Jinja2, itsdangerous, click, Flask
Successfully installed Flask-1.0.2 Jinja2-2.10.1 MarkupSafe-1.1.1 Werkzeug-0.15.2 click-7.0 itsdangerous-1.1.0
Removing intermediate container c3001ba09be0
---> 6ddf3e9b74b4
Step 5/9 : RUN pip install uwsgi
---> Running in b2da85403827
Collecting uwsgi
Downloading https://files.pythonhosted.org/packages/e7/1e/3dcca007f974fe4eb369bf1b8629d5e342bb3055e2001b2e5340aaefae7a/uwsgi-2.0.18.tar.gz (801kB)
Building wheels for collected packages: uwsgi
Building wheel for uwsgi (setup.py): started
Building wheel for uwsgi (setup.py): finished with status 'done'
Stored in directory: /root/.cache/pip/wheels/2d/0c/b0/f3ba1bbce35c3766c9dac8c3d15d5431cac57e7a8c4111c268
Successfully built uwsgi
Installing collected packages: uwsgi
Successfully installed uwsgi-2.0.18
Removing intermediate container b2da85403827
---> bf2054626772
Step 6/9 : COPY ./ ./
---> 0116f0640e3a
Step 7/9 : RUN addgroup -S uwsgi && adduser -S uwsgi -G uwsgi
---> Running in f49c0a3ba331
Removing intermediate container f49c0a3ba331
---> 0209e9adf58b
Step 8/9 : USER uwsgi
---> Running in 6fcf2863a2fa
Removing intermediate container 6fcf2863a2fa
---> eea00d37abcb
Step 9/9 : CMD ["uwsgi", "--ini", "app.ini"]
---> Running in ffca6f344938
Removing intermediate container ffca6f344938
---> 0debc5273a9e
Successfully built 0debc5273a9e
```
Met het commando `docker history <image | container id>` krijg je een inzicht uit welke layers het image is opgebouwd (laatste lijn bij build is altijd container id). Voer dit commando uit, hoeveel layers zijn er in totaal? Denk je dat het aantal layers eventueel omlaag kan?
> Antwoord
> ...
> ...
Indien je bij het vorige commando gebruik hebt gemaakt van een `container id` mag je nog even uitzoeken hoe je het image kan taggen. Dat kan op twee manieren. Wat is het voordeel van tagging? Geeft als tag `jouw-naam/web`, je hoeft geen versie op te geven (default wordt dat dan latest).
> Antwoord
> ...
> ...
### Docker container starten
Je kan nu een container starten op basis van het gemaakte image. Dit kan met `docker run -p 5000:5000 -it <image>`
Wat doet de `-p` parameter, en de `-it` parameter (kan ook geschreven worden als `-i -t`)?
> Antwoord
> ...
> ...
Surf naar je applicatie, als alles goed gaat zie je `Hello world` verschijnen in je browser. Bekijk zeker ook is de output op je terminal.
### Werken met package installers
Het gebruikte `Dockerfile` is ver van perfect opgesteld. Je hebt wellicht zelf al wat pijnpunten gevonden.
De uitgewekte applicatie is op deze moment nog erg eenvoudig, m.a.w. nog zeer weinig dependencies zijn vereist. Indien je deze applicatie verder gaat uitwerken, ga je (als je dezelfde manier van opbouwen gebruikt) ontzettend veel `RUN pip install <package>` commando's schrijven. Uiteraard, zou je die verschillende RUN commando's kunnen samenvoegen.
Door het samenvoegen van de `RUN`'s zal het aantal layers gereduceerd worden, en bijgevolg ook de build-time evenals de start-time van je container. Anderzijds, hoe meer je in een layer steekt, hoe minder herbruikbaar die is. Er is dus geen "perfecte" oplossing.
In ieder geval, wanneer je gebruik maakt van een hele hoop dependencies zal het Dockerfile sowieso erg slordig, moeilijk leesbaar worden. Een best practice is om je vereiste dependencies in een text-file op te nemen en via een package manager (bv. pip, npm, composer, ...) deze te installeren.
Maak een bestand `requirements.txt` aan met volgende inhoud:
```
Flask==1.1.1
uWSGI==2.0.18
```
Je Dockerfile kan je aanpassen als volgt:
```
FROM python:3.7-alpine
WORKDIR '/app'
RUN apk add --no-cache linux-headers g++
COPY ./ ./
RUN pip install -r requirements.txt
RUN addgroup -S uwsgi && adduser -S uwsgi -G uwsgi
USER uwsgi
CMD ["uwsgi", "--ini", "app.ini"]
```
Rebuild je image, en run je container ter verificatie. Pas daarna je `main.py` file aan, voeg volgend stukje code toe.
```
@app.route('/about')
def about():
return 'This app is powered by Docker!'
```
Rebuild opnieuw je image, run je container en controleer of je kan surfen naar je about page. Gaat het builden snel?
> Antwoord
> ...
> ...
Pas het Dockerfile aan om dit efficiënter te maken (tip: theorieles!). Rebuild daarna opnieuw je image.
> Antwoord
> ...
> ...
Pas opnieuw `main.py` aan, voeg volgende toe:
```
@app.route('/info')
def info():
return 'Some important information!'
```
Rebuild opnieuw, dit zou nu hoogstens een paar seconden in beslag mogen nemen. Indien dat niet het geval is, denk even na, en vraag indien nodig raad aan je docent.
### Multi-stage builds
Bekijk nogmaals de layers van je image: `docker history jouw-naam/web`. Welke layer heeft de grootste omvang? Wat heb je gedaan om die layer te bekomen? Waarom is deze layer noodzakelijk (tip: zet `RUN` commando in commentaar `#` en rebuild)
> Antwoord
> ...
> ...
We kunnen jammer genoeg niet zonder deze layer, maar we hebben deze layer zeker en vast niet nodig in productie. Om dit op te lossen kunnen we gebruik maken van zogenaamde [multi-stage builds](https://docs.docker.com/develop/develop-images/multistage-build/).
Er zijn verschillende mogelijkheden om dit te gaan uitwerken, een daarvan is werken met [wheel](https://pythonwheels.com/).
Je nieuwe `Dockerfile` gaat eigenlijk de commando's bevatten om twee images te builden.
Normaal gezien zou volgende code geen verrassingen mogen bevatten (buiten de erste lijn):
```
FROM python:3.7-alpine as build-image
WORKDIR '/app'
RUN apk add --no-cache linux-headers g++
COPY ./requirements.txt ./
RUN pip wheel --wheel-dir=/root/wheels -r requirements.txt
```
In je `Dockerfile` kan je nu als volgt een prodction-ready image beschrijven door gebruik te maken van het intermediair `build-image`:
```
FROM python:3.7-alpine as production-image
WORKDIR '/app'
COPY --from=build-image /root/wheels /root/wheels
COPY --from=build-image /app/requirements.txt ./
RUN pip install --no-index --find-links=/root/wheels -r requirements.txt
RUN apk add --no-cache postgresql-dev
COPY ./ ./
RUN addgroup -S uwsgi && adduser -S uwsgi -G uwsgi
USER uwsgi
CMD ["uwsgi", "--ini", "app.ini"]
```
Bekijk nogmaals de `docker history`, je zal zien dat je eigen toegevoegde layers nu veel compacter zijn. Merk op dat je `requirements.txt` eventueel zou kunnen kopiëren naar `/root/wheels` en in je `production-image` de `COPY` verwijderen, dat spaart een layer uit.
## Applicatie uitbreiden
Tot dusver is de opgebouwde applicatie bijster simpel. In deze sectie ga je de applicatie herschrijven. Je bouwt aan een applicatie waarbij gebruikers zich kunnen registreren en bijgevolg dus ook kunnen inloggen en uitloggen.
Verwijder in je `web` directory alles behalve je `Dockerfile`, `app.ini`, en `requirements.txt`. Maak daarna een nieuwe folder aan, genaamd `webapp`.
### User model
Het Python package `Flask-SQLAlchemy` biedt een makkelijke manier om met verschillende type databases aan de slag te gaan. Het is een zogenaamde [object-relational mapping (kortweg ORM)](https://en.wikipedia.org/wiki/Object-relational_mapping) laag. Bekijk eventueel de [documentatie](https://flask-sqlalchemy.palletsprojects.com/en/2.x/).
In dit labo maak je gebruik van [PostgreSQL](https://www.postgresql.org/). Dat impliceert dat `Flask-SQLAlchemy` (eigenlijk onderliggend het package `SQLAlchemy`) verwacht dat je gebruikt maakt van `psycopg2`, een package om met PostgreSQL aan de slag te gaan.
Pas je `requirements.txt` file aan als volgt:
```
Flask==1.1.1
uWSGI==2.0.18
Flask-SQLAlchemy==2.4.1
psycopg2==2.8.4
Flask-Login==0.5.0
```
<small>Notitie: merk op dat psycopg3 reeds beschikbaar is. Feel free...</small>
Om gebruik te maken van `psycopg2` heb je nog Alpine packages nog. Pas je `Dockerfile` aan, vervang de huidige `RUN apk add ...` lijn door:
```
RUN apk add --no-cache linux-headers g++ postgresql-dev gcc python3-dev musl-dev
```
Maak daarna in je `webapp` directory `models.py` met volgende inhoud:
```
from . import db
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(80), nullable=False)
def __init__(self, username, password):
self.username = username
self.password = password
def __repr__(self):
return '<User {}>'.format(self.username)
```
Zorg dat je de code begrijpt. Je luie docent heeft nog geen comments toegevoegd aan de code, **misschien niet onverstandig om dit even te doen**.
### Routering
Onderstaande code zou weinig verrassingen mogen bevatten. Merk op dat er gebruik gemaakt is van `Flask-Login`, een package dat het mogelijk maakt om gebruikers te authenticeren. Vergeet dit niet te voegen aan je `requirements.txt`: `Flask-Login==0.4.1`. **Voeg comments toe.**
Sla volgende code op in een bestand genaamd `webapp/routes.py`.
```
from flask import url_for, render_template, request, redirect, session
from flask import current_app as app
from .models import db, User
@app.route('/', methods=['GET'])
def home():
if not session.get('logged_in'):
return render_template('index.html')
else:
return render_template('index.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')
else:
username = request.form['username']
password = request.form['password']
try:
data = User.query.filter_by(username=username, password=password).first()
if data is not None:
session['logged_in'] = True
return redirect(url_for('home'))
else:
return render_template('index.html', data={'username': username, 'password': password})
except Exception as e:
return "Some very good exception handling!"
@app.route('/registration', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
new_user = User(username=request.form['username'], password=request.form['password'])
db.session.add(new_user)
db.session.commit()
return render_template('login.html')
return render_template('register.html')
@app.route('/logout')
def logout():
session['logged_in'] = False
return redirect(url_for('home'))
```
### Init applicatie
Volgende code, `webapp/__init__.py` zou je grotendeels moeten herkennen uit vorige secties. De nodige code om een database te werken is toegevoegd. Alsook leest de applicatie `enviroment variables` in, je gaat die (straks) dus nog moeten instellen. Vergeet de comments niet ;-)!
```
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os
pg_user = os.getenv('PG_USER', 'postgres')
pg_host = os.getenv('PG_HOST', 'localhost')
pg_port = os.getenv('PG_PORT', '5432')
pg_database = os.getenv('PG_DATABASE', 'postgres')
pg_password = os.getenv('PG_PASSWORD', 'postgres_password')
db = SQLAlchemy()
def create_app():
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] \
= 'postgresql://' + pg_user + ':' + pg_password + '@' + pg_host + ':' + pg_port + '/' + pg_database
app.config['SECRET_KEY'] = 'super secret key'
# app.config['DEBUG'] = True
# app.config['ENVIRONMENT'] = 'development'
db.init_app(app)
with app.app_context():
# Imports
from . import routes
db.create_all()
return app
```
### Templates
Maak een nieuwe directory `templates` aan, onder `webapp`, m.a.w. `~/docker-labo/web/webapp/templates`.
Flask heeft standaard ondersteuning voor de Jinja2 template engine. Dit maakt het makkelijk om enerzijds templates aan te maken en anderzijds hierin data te injecteren.
Je krijgt opnieuw alles cadeau. De templates zijn opgemaakt op basis van [Twitter Bootstrap](https://getbootstrap.com/). Merk op dat er hier en daar `if-else` structuren terug te vinden zijn. Maak volgende bestaden met bijhorende inhoud aan.
**base.html**
```
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Coolest Docker Example Ever!</title>
<link rel="canonical" href="https://getbootstrap.com/docs/4.0/examples/album/">
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</head>
<body>
<header>
<div class="navbar navbar-dark bg-dark box-shadow">
<div class="container d-flex justify-content-between">
<a href="/" class="navbar-brand d-flex align-items-center">
<strong>Some cool Docker App</strong>
</a>
</div>
</div>
</header>
<main role="main">
<section class="jumbotron text-center">
<div class="container">
{% block content %}{% endblock %}
<p>
{% if session['logged_in'] %}
<a href="/logout" class="btn btn-danger">Logout</a>
{% else %}
{% if request.path!="/login" and request.path!="/registration" %}
<a href="/login" class="btn btn-primary">Login</a>
<a href="/registration" class="btn btn-secondary">Register</a>
{% endif %}
{% endif %}
</p>
</div>
</section>
</main>
</body>
</html>
```
**index.html**
```
{% extends "base.html" %}
{% block content %}
{% if session['logged_in'] %}
<h1 class="jumbotron-heading">Index - Logged in!</h1>
<p class="lead text-muted">Some inspiring text. Give it another <a href="/logout">try</a>?</p>
{% else %}
<h1 class="jumbotron-heading">Index</h1>
<p class="lead text-muted">Some inspiring text.</p>
{% if data %}
<div class="alert alert-danger" role="alert">
<strong>You must be retarded!</strong> No user with username <strong>{{ data.username }}</strong> and password <strong>{{ data.password }}</strong>!
</div>
<a href="/login" class="btn btn-warning">Try again!</a>
<a href="/registration" class="btn btn-success">Register!</a>
{% endif %}
{% endif %}
{% endblock %}
```
**login.html**
```
{% extends "base.html" %}
{% block content %}
{% if session['logged_in'] %}
<h1 class="jumbotron-heading">Nothing to do here...</h1>
<p class="lead text-muted">Seems like you're already logged in!</p>
{% else %}
<h1 class="jumbotron-heading">Login</h1>
<p class="lead text-muted">This is a login form!</p>
<form action="/login" method="POST">
<div class="form-group">
<label for="username">Username</label>
<input type="username" class="form-control" id="username" name="username"
placeholder="Enter username">
</small>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" placeholder="Password" name="password">
</div>
<button type="submit" class="btn btn-primary">Login!</button>
</form>
{% endif %}
{% endblock %}
```
**register.html**
```
{% extends "base.html" %}
{% block content %}
{% if session['logged_in'] %}
<h1 class="jumbotron-heading">Nothing to do here...</h1>
<p class="lead text-muted">Seems like you're already logged in!</p>
{% else %}
<h1 class="jumbotron-heading">Register</h1>
<p class="lead text-muted">You won't regret, coolest app ever!</p>
<form action="/registration" method="POST">
<div class="form-group">
<label for="username">Username</label>
<input type="username" class="form-control" id="username" name="username"
placeholder="Enter username">
</small>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" placeholder="Password" name="password">
</div>
<button type="submit" class="btn btn-success">Register!</button>
</form>
{% endif %}
{% endblock %}
```
### WSGI applicatie
In principe heb je nu een vrij complete app. Hier en daar zeker wel nog voor verbetering vatbaar. Ga gerust je gang, ongetwijfeld *bijna* zo nuttig én leuk als gamen in het forum.
Het enige wat nog ontbreekt is code om je WSGI app, meer bepaald Flask, te starten.
Maak in `~/docker-labo/web` een bestand `wsgi.py` aan met volgende code:
```
from webapp import create_app
app = create_app()
if __name__ == "__main__":
app.run()
```
Vergeet niet je `app.ini` aan te passen, de module is nu uiteraard `wsgi:app`.
Je directory structuur zou er als volgt moeten uitzien:
```
student@docker-student-00:~/docker-lab$ tree
.
└── web
├── app.ini
├── Dockerfile
├── requirements.txt
├── webapp
│ ├── __init__.py
│ ├── models.py
│ ├── routes.py
│ └── templates
│ ├── base.html
│ ├── index.html
│ ├── login.html
│ └── register.html
└── wsgi.py
```
### Imperfecties
Merk op dat deze applicatie slechts een voorbeeld is, dus om met Docker te leren werken. Wachtworden worden opgeslagen in plain-text, foutafhandeling is zeer beperkt, geen form validation, ... .
## Docker compose
### Postgres database
Je hebt nu een geweldige, fantastische, prachtige, ... applicatie. Je hebt echter nog geen database. In dit labo ga je zelf een database opzetten, via Docker. Indien je deze app online zou zetten is het misschien interessanter om te kiezen voor een hosted oplossing, denk aan AWS RDS, Azure SQL Database.
Je kan je applicatie best al even testen, zonder `docker-compose`. Een PostgreSQL server kan je runnen als volgt:
```
docker run -itd -p 5432:5432 postgres:11.2-alpine
```
Geef de database even de tijd om te starten. In de theorieles zag je een manier om logs te bekijken, de ideale moment om dit eens te gebruiken. Hoe bekijk je de logs van deze container?
> Antwoord
> ...
> ...
Je ziet eveneens dat poort 5432 geopend wordt. Je docent is benieuwd: is dit al dan niet verstandig?
> Antwoord
> ...
> ...
Build je image, en run op basis hiervan een container als volgt (op voorwaarde dat je SQL server actief is!):
```
docker run -e PG_HOST=IP_VAN_JOUW_VM -e PG_PORT=5432 -p 5000:5000 -it student/web
```
Merk op dat we slechts twee `environment variables` meegeven. De reden hiervoor is dat we in de applicatie de overige (onveilige) defaults (ingebakken in postgres image) hebben opgegeven, bekijk `__init.py__` nog maar eens goed! De app zou nu moeten werken, test maar uit.
Na het testen, vergeet niet je database en app te stoppen.
### docker-compose.yml opstellen
Zoals gezien in de theorieles heeft `docker-compose` twee grote voordelen:
1. Eenvoudiger runnen van containers
2. Eenvoudiger om meerdere containers te combineren
Navigeer naar je `docker-lab` directory en maak `docker-compose.yml` aan, met volgende inhoud:
```
version: '3.7'
services:
web:
build: ./web
ports:
- 5000:5000
environment:
- PG_PASSWORD=student_password
- PG_USER=student_user
- PG_DATABASE=labo
- PG_PORT=5432
- PG_HOST=postgres
postgres:
image: 'postgres:11.2-alpine'
environment:
- POSTGRES_PASSWORD=student_password
- POSTGRES_USER=student_user
- POSTGRES_DB=labo
```
Probeer bovestaand bestand volledig te begrijpen, bekijk zeker en vast eens de [documentatie](https://docs.docker.com/compose/). Merk ook op dat bij de Postgres component geen poort wordt opgegeven (noch via expose, noch via ports). Waarom is dit niet nodig ([tip](https://github.com/docker-library/postgres/blob/master/Dockerfile-alpine.template))? Wat is het verschil / nut van expose vs ports?
> Antwoord
> ...
> ...
### Meerdere containers runnen
Je kan nu je build process en run process, voor beide containers, met volgend commando uitvoeren:
```
docker-compose up --build
```
Uiteraard dien je `--build` enkel op te geven indien je aanpassingen hebt doorgevoerd aan je images.
Ooh nee! De applicatie werkt (wellicht) niet! Wat loopt er mis?
Tips:
1. `docker-compose ps`
2. `docker-compose logs <naam van de service>`
> Antwoord
> ...
> ...
Je zou kunnen denken dat dit op te lossen is met `depends_on`. Jammer genoeg is het complexer dan dat, `depends_on` zorgt er enkel voor dat een bepaalde service pas kan starten van zodra de desbetreffende service gestart is. Gestart betekent in dit geval niet per se dat de databaseserver klaar is om connecties af te handelen.
Het idee is dat elke service onafhankelijk kan werken. Wanneer je applicatie een connectie verwacht naar een database, en die is er niet, moet je dat opvangen in je applicatie. Dus niet via een of andere Docker-manier.
Een mogelijke oplossing is om gebruik te maken van zogenaamde database pools. Dit is echter niet de focus van dit labo, kijk [https://docs.sqlalchemy.org/en/13/core/pooling.html#disconnect-handling-pessimistic](hier) voor meer informatie.
#### Simpele 'hack'
Je start de services op, d.m.v. `docker-compose up` (e.v.t. `-d` voor background). Daarna (e.v.t. in een andere terminal) kan je `docker-compose restart web` uitvoeren. Uiteraard, enkel en alleen indien je databaseserver intussentijd beschikbaar is geworden.
### Environment files
Typisch gezien ga je alle bovenstaande code op een git repository plaatsen. Dat impliceert dus ook de gevoelige gegevens opgegeven in je `docker-compose.yml`. Om dit te oplossen kan je gebruik maken van Docker secrets, bekijk gerust de [documentatie](https://docs.docker.com/compose/compose-file/).
Dit is (meestal) vrij omslachtig. Een alternatief is werken met `enviroment files`. Je verwijst ernaar in `docker-compose.yml`. Via `.gitignore` kan je er voor zorgen dat je `environment files` niet worden gecommit.
Zoek dit zelf uit, en uiteraard, implementeer dit. Tip [documentatie](https://docs.docker.com/compose/environment-variables/)
## Nginx reverse proxy
= Extra, vraag even :-)
## Mogelijke examenvragen
Verzin minstens 3 goede examenvragen over de inhoud van deze en vorige les (theorie & labo). Deze vragen moeten peilen naar kennis, kunde of beide over het onderwerp in kwestie. Ze moeten doenbaar zijn, maar ook niet te gemakkelijk.
1)
2)
3)