# 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)