Docker, Django et le Mac

Ce billet date de plusieurs années, ses informations peuvent être devenues obsolètes.

Voici quelques notes en vrac sur Docker for Mac et la façon d'utiliser des conteneurs Linux avec une application Django.

Docker for Mac

Depuis ses débuts, Docker a popularisé les conteneurs Linux.

Mais Docker n'a pas l'air simple à porter sous BSD.

Pourtant ça tourne sous le kernel XNU de macOS (launchd as PID 1 to rule 'em all).

C'est parce qu'avec Docker for Mac il y a toujours une VM qui fait tourner un Linux basé sur Alpine embarquant le démon Docker.

Mais à la place de passer par un hyperviseur lourd comme VirtualBox, Docker for Mac utilise Hyperkit pour faire tourner une VM dans xhyve au dessus de l'Hypervisor.framework. L'Hypervisor.framework a été ajouté dans OS X 10.10 sans tambour ni trompette (pas un mot dans la review de Siracusa) et expose les fonctionnalités de l'Intel VT.

C'est fortiche d'avoir réussi à faire tourner ça sous Mac out of the box avec cette impression d'app native.

Docker for Django

Je vais pas tout refaire car il existe déjà de la lecture sur Docker et Django :

Si vous avez l'habitude de déployer du code Python pour le web, il n'y a pas de grosse difficulté sinon celle d'avoir lu la documentation de Docker et de connaître son écosystème.

Après, c'est fun à mettre en place.

Docker Compose

Sur mon dernier projet, on a utilisé Docker pour la production et pour le développement.

Le point d'entrée de l'application est un fichier docker-compose.yml dans lequel tous les éléments de l'architecture sont définis. Il suffit de lire ce fichier de configuration pour comprendre de quelles briques est composée l'application.

Le jeu est de rendre ce fichier le plus flexible possible afin de pouvoir l'utiliser dans plusieurs environnements.

Pour cela on a plusieurs leviers :

Du côté des variables d'environnement, il est possible d'utiliser un fichier .env (attention à la syntaxe, pas de quotes). Les variables déclarées dans ce fichier ne pourront être utilisée que dans la configuration de Docker Compose. Pour qu'elles deviennent aussi visible à l'intérieur d'un conteneur, il faut utiliser l'option de configuration env_file. L'avantage du fichier .env est alors que vous pouvez y centraliser tout ce qui est susceptible de varier entre des déploiements, et que c'est un format qui peut aussi être utilisé par uWSGI, par Pipenv etc.

On se sert de docker-compose.override.yml pour pouvoir utiliser différents Dockerfiles en fonction de l'environnement dans lequel on se trouve. Ça nous permet aussi d'avoir une organisation du code propre avec un répertoire par environnement comprenant des fichiers de configurations spécifiques quand les variables d'environnement ne sont pas suffisantes (uWSGI etc.).

Les images

On essaye de garder les images Docker les plus petites possibles. Pour cela on se base sur des micro-distributions Linux comme Busybox ou Alpine.

La seule difficulté est alors de savoir ce dont vos dépendances Python auront besoin au niveau du système pour pouvoir s'installer et tourner dans l'instance de l'image (libc-dev, make, gettext, tzdata etc.).

Pour les dépendances justement, on utilise Pipenv (encore neuf) dans l'image de développement. Ça fait franchement double emploi parce que le conteneur est déjà un isolant. Pour le moment c'est la seule façon que j'ai trouvée pour pouvoir faire tourner un pipenv update afin de mettre à jour le fichier Pipfile.lock qui rend le build déterministe. On se retrouve avec un double wrap docker et pipenv pour lancer une commande en développement :

docker exec -t wif_django pipenv run django-admin migrate

Un peu lourd… mais ça fait le taf.

Le fait d'avoir conteneurisé l'environnement de développement permet à un nouvel arrivant d'avoir son application installée en quelques minutes et de pouvoir commencer à coder tout de suite.

En production, on demande à Pipenv d'installer les dépendances au niveau du système, on peut alors se passer de pipenv run dans les commandes.

uWSGI

Si vous avez un projet sur lequel vous savez qu'il n'y aura pas un trafic de folie, vous pouvez faire servir les fichiers statiques par uWSGI directement. Dans ce cas là assurez-vous que votre micro-distribution embarque une liste des types MIME.

Si vous avez bien configuré votre uWSGI, il devrait se charger tout seul de relancer ses workers en cas de défaillance. A priori il n'y a pas de raison d'embarquer un supervisord dans le conteneur.

Combien de workers faut-il spawner ? En cas de doute il existe le cheaper subsystem et la variable magique %k.

On fait parler notre proxy avec uWSGI via des sockets HTTP. Vous pouvez le faire aussi avec des sockets Unix, c'est plus rapide mais moins direct avec Docker et pas encore sec avec Docker for Mac.

Enfin, parmi la myriade d'options avec lesquelles il est possible de lancer uWSGI il y a le touch-reload qui pourra vous permettre de relancer uWSGI via un touch sur un fichier. C'est pratique dans les cas où vous devez déployer un hotfix basé uniquement sur du code et que celui-ci est monté dans un volume.

Pour aller plus loin

En vrac et à discuter avec un bon sysadmin :

  • orchestration (Kubernetes, Docker Swarm, etc.)
  • sécurité : correctifs de sécurité du kernel de l'hôte et de Docker
  • Containers-on-Bare-Metal VS Containers-on-VM
  • centralisation des logs des conteneurs
  • rollback de l'application
  • etc.

Avant Déploiement d'une application web Python Après Reproduction du Super Mario d'Olly Moss

Tag Kemar Joint