Un serveur Raspberry avec Docker

Dans ce guide, je vous propose d'utiliser Docker et Compose pour déployer et orchestrer vos services sur  un Raspberry Pi.

L'architecture du processeur d'un Raspberry Pi est de type ARM, très courante dans l'embarqué, mais la plupart des images disponibles sur le Docker Hub sont destinées aux architectures x64 – les processeurs Intel et AMD grand public.

Il faudra donc sélectionner les images Docker adaptées à cette contrainte. J'ai écrit ce guide au fur et à mesure d'une installation que j'ai menée ; ça devrait être quasiment prêt à copier-coller :-)

La distribution GNU/Linux la plus courante pour un Raspberry Pi est Raspbian, disponible pour les plate-formes ARM. C'est là-dessus que je suis parti pour cette installation, mais depuis ce mois-ci, Ubuntu 19.10 Server propose un kernel ARMv8 64 bits compatible avec le Pi 4. À tester, donc...

Architecture

  • Un VPN qui fonctionne sur le port HTTPS standard afin d'être accessible depuis n'importe quel réseau, même limité, et qui transfère au reverse proxy les requêtes HTTP qui lui sont destinées ;
  • Un reverse proxy pour traiter les requêtes HTTP passées par le VPN ;
  • Un ensemble de services, selon vos besoins, exposés par l'intermédiaire du reverse proxy :
On communiquera par HTTPS avec tous les services, et les certificats seront générés et renouvelés automatiquement.

Pré-requis

Il voudra faudra un nom de domaine (optez pour l'un des nombreux fournisseurs, parfois même gratuit). Pour l'exemple, mon domaine sera chezoim.com.

Faîtes pointer votre nom de domaine ainsi que tous ses sous-domaines vers votre adresse IP publique. La marche est à suivre diffère selon les fournisseurs (registrars), mais en gros, dans la configuration DNS de votre domaine, vous devrez créer deux enregistrements A de type :

A chezoim.com 12.123.123.123
A *.chezoim.com 12.123.123.123

Il faudra également ouvrir trois ports sur votre routeur ou box : 80 (HTTP), 443 (HTTPS) et, temporairement, 81 (pour l'administration du reverse proxy). La marche à suivre dépend du modèle, mais tout est documenté.

On considère que Raspbian Buster a été déployée sur le Raspberry, et on installe Docker :

sudo apt-get install apt-transport-https ca-certificates software-properties-common -y   
cd /tmp
curl -fSLs https://get.docker.com | sudo sh

On installe également docker-compose, qui permet de définir un ensemble de services dans un même fichier de configuration. Comme il n'y a pas de binaire compilé pour l'ARM, on passe par pip, le gestionnaire de paquets de Python (d'après le guide de With Blue Ink) – c'est assez long :

sudo apt install -y python python-pip libffi-dev python-backports.ssl-match-hostname
sudo pip install docker-compose

Préambule

Pour résumer, voilà de quoi on va avoir besoin :

Puisque OpenVPN et Traefik vont écouter sur des ports réservés (443 et 80), un simple utilisateur ne pourra pas créer les conteneurs. On va donc passer en root avant d'effectuer la suite des manipulations.

Mise en route

On crée un répertoire pour stocker la configuration des services :

sudo su
mkdir /home/docker
chmod 700 /home/docker
cd /home/docker

Docker propose de gérer des volumes nommés, par défaut stockés dans /var/lib/docker/volumes. C'est la solution que l'on va utiliser pour centraliser les données et la configuration des services.

Définition des services

On crée le fichier /home/docker/docker-compose.yml avec la définition de l'ensemble de nos services. La configuration de Traefik est dynamique : on la passe à la création du conteneur. Les labels que l'on ajoute à chacun des conteneurs permettent à Traefik d'identifier les services et de créer les routes nécessaires.

Je me suis largement appuyé sur un article du blog de Gérald Croës que je vous recommande pour comprendre en détails le fonctionnement du reverse proxy.

version: "2"

services:
  openvpn:
    image: darathor/openvpn
    restart: always
    cap_add:
      - NET_ADMIN
    ports:
      - "443:1194"
    volumes:
      - openvpn_data:/etc/openvpn
      
  traefik:
    image: traefik:latest
    command:
      - --api
      - --providers.docker
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --serverstransport.insecureskipverify=true
      - --certificatesresolvers.le.acme.email=oim@chezoim.com
      - --certificatesresolvers.le.acme.storage=/etc/traefik/acme/acme.json
      - --certificatesresolvers.le.acme.tlschallenge=true
    restart: always
    ports:
      - "80:80"
    expose:
      - "443"
    volumes:
      - traefik_certs:/etc/traefik/acme
      - /var/run/docker.sock:/var/run/docker.sock:ro
    labels:
      # redirection automatique HTTP vers HTTPS
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
      - "traefik.http.routers.redirs.rule=hostregexp(`{host:.+}`)"
      - "traefik.http.routers.redirs.entrypoints=web"
      - "traefik.http.routers.redirs.middlewares=redirect-to-https"
      # dashboard
      - "traefik.http.routers.traefik.rule=Host(`manage.chezoim.com`)"
      - "traefik.http.routers.traefik.service=api@internal"
      - "traefik.http.routers.traefik.middlewares=admin"
      - "traefik.http.routers.traefik.tls.certresolver=le"
      - "traefik.http.routers.traefik.entrypoints=websecure"
      - "traefik.http.middlewares.admin.basicauth.users=oim:$$2y$$05$$rlynTRy5irXJOrblhU7fB.cttyycvqaQEtd2GOipF/J8YkR1Z3FcC"

  mariadb:
    image: webhippie/mariadb:latest
    restart: always
    expose:
      - "3306"
    volumes:
      - mariadb_data:/var/lib/mysql
      - mariadb_backup:/var/lib/backup
    environment:
      - MARIADB_ROOT_PASSWORD=m0nM0td3p4553r007!
      - MARIADB_DATABASE=nextcloud
      - MARIADB_USERNAME=nextcloud
      - MARIADB_PASSWORD=nextcloud

  nextcloud:
    image: linuxserver/nextcloud:latest
    restart: always
    expose:
      - "443"
    volumes:
      - nextcloud_storage:/data
      - nextcloud_data:/config
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Paris
    labels:
      - "traefik.http.services.nextcloud.loadbalancer.server.port=443"
      - "traefik.http.services.nextcloud.loadbalancer.server.scheme=https"
      - "traefik.http.routers.nextcloud.rule=Host(`cloud.chezoim.com`)"
      - "traefik.http.routers.nextcloud.tls.certresolver=le"
      - "traefik.http.routers.nextcloud.entrypoints=websecure"

  portainer:
    image: portainer/portainer:latest
    restart: always
    expose:
      - "9000"
    volumes:
      - portainer_data:/data
      - /var/run/docker.sock:/var/run/docker.sock
    labels:
      - "traefik.http.services.portainer.loadbalancer.server.port=9000"
      - "traefik.http.routers.portainer.rule=Host(`admin.chezoim.com`)"
      - "traefik.http.routers.portainer.tls.certresolver=le"
      - "traefik.http.routers.portainer.entrypoints=websecure"

  transmission:
    image: linuxserver/transmission:latest
    restart: always
    mem_limit: 1024m
    expose:
      - "9091"
    ports:
      - "51413:51413"
      - "51413:51413/udp"
    volumes:
      - transmission_data:/config
      - transmission_downloads:/downloads
      - transmission_watch:/watch
      - /media/torrents:/media/torrents
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Paris
    labels:
      - "traefik.http.services.transmission.loadbalancer.server.port=9091"
      - "traefik.http.routers.transmission.rule=Host(`torrents.chezoim.com`)"
      - "traefik.http.routers.transmission.tls.certresolver=le"
      - "traefik.http.routers.transmission.entrypoints=websecure"

volumes:
  openvpn_data:
  traefik_certs:
  mariadb_data:
  mariadb_backup:
  nextcloud_storage:
  nextcloud_data:
  portainer_data:
  transmission_data:
  transmission_downloads:
  transmission_watch:

À noter qu'on utilise une authentification basique pour l'interface de Traefik. Pour générer un identifiant, utilisez la commande suivante – ici, l'utilisateur oim avec comme mot de passe chezoim.com :

echo $(htpasswd -nbB oim "chezoim.com") | sed -e s/\\$/\\$\\$/g

On sauvegarde également dans un volume le fichier acme.json qui contient nos certificats Let's Encrypt. Cela évite la mauvaise surprise d'atteindre les limites de renouvellement de certificats du service quand on fait ses tests...

Configuration d'OpenVPN

On génère la configuration d'OpenVPN (TCP sur le port 443, "partagé" avec Traefik) :

# rentrez un mot de passe solide
docker-compose run --rm openvpn ovpn_genconfig -u tcp://chezoim.com:443 -e 'port-share traefik 443'

# faîtes correspondre le Common Name demandé au nom d'hôte de votre serveur (i.e. raspberrypi)
docker-compose run --rm openvpn ovpn_initpki

On peut prendre de l'avance et créer un certificat pour un client. Le fichier .ovpn généré est auto-suffisant – vous pourrez l'importer dans n'importe quel client OpenVPN moderne :

docker-compose run --rm openvpn easyrsa build-client-full mon_client
docker-compose run --rm openvpn ovpn_getclient mon_client > mon_client.ovpn

À ce stade, dans /home/docker, vous avez deux fichiers :

  • docker-compose.yml qui définit les services à exécuter ;
  • client.ovpn qui est votre fichier de configuration drag'n'drop pour OpenVPN.

Vos données sont stockées dans les volumes nommés définis par Docker Compose, sous le répertoire /var/lib/docker/volumes.

Configuration des services

On va créer les conteneurs pour initialiser leur configuration. Les volumes correspondant seront créés à la volée :

docker-compose up -d
Le dashboard de Traefik, désormais visible à l'adresse manage.chezoim.com.

Le cycle de vie d'un conteneur est court : on peut le détruire et le recréer comme on le souhaite, et il s'initialisera de la même manière à chaque lancement. D'ailleurs, on va tout arrêter pour modifier la configuration de Transmission :

docker-compose down

Sur votre disque, le contenu du volume nommé transmission_data se trouve sous /var/lib/docker/volumes/docker_transmission_data/_data/. On y va et on modifie le fichier settings.json – je vous propose de regarder uniquement les quelques lignes qui nous intéressent, le reste peut rester aux valeurs par défaut :

    "blocklist-enabled": true,
    "blocklist-url": "https://john.bitsurge.net/public/biglist.p2p.gz",
    "dht-enabled": false,
    "pex-enabled": false,
    "rpc-authentication-required": true,
    "rpc-bind-address": "0.0.0.0",
    "rpc-enabled": true,
    "rpc-host-whitelist": "",
    "rpc-host-whitelist-enabled": true,
    "rpc-password": "m0n5up3rm07d3p4553!",
    "rpc-port": 9091,
    "rpc-url": "/transmission/",
    "rpc-username": "oim",
    "rpc-whitelist": "127.0.0.1",
    "rpc-whitelist-enabled": false,
    "utp-enabled": false,
    "watch-dir": "/watch",
    "watch-dir-enabled": true

En une commande, nos conteneurs sont recréés et redémarrés :

docker-compose up -d

Il vous reste à accéder à Portainer et Nextcloud pour initialiser leur configuration. Pour Nextcloud, n'oubliez pas de préciser les informations sur la base de données :

On fait dérouler le menu "Stockage & base de données" pour remplir les infos de connexion à MariaDB.

Il est fort probable que la finalisation prenne trop de temps et que vous obteniez une erreur 504. Laissez passer quelques minutes, l'installation de Nextcloud se termine en tâche de fond.

Il est temps de se reposer. On a bien bossé :-)

Et ensuite ?

Mettez en place des sauvegardes – c'est simple, vous avez deux répertoires à surveiller :

  • /home/docker qui contient la déclaration de vos services ;
  • /var/lib/docker/volumes où se trouvent les volumes montés dans vos conteneurs, c'est-à-dire vos données.

N'oubliez pas de mettre à jour régulièrement les images de vos services :

docker-compose pull
docker-compose up -d

Au fur et à mesure, les versions antérieures des images peuvent être effacées pour récupérer de l'espace disque :

docker system prune

Enfin en cas de problème, consultez les journaux des conteneurs :

docker-compose logs

Pour garder un œil sur l'activité de vos conteneurs, je vous recommande ctop. Vous pouvez obtenir de belles visualisations avec Grafana ; j'ai suivi un guide chez Eleven Labs pour mettre tout ça en place avec Docker.

Capture d'écran du stack cAdvisor / Prometheus / Grafana sur mon serveur. Faîtes-moi signe si vous le mettez en place sur votre Raspberry !