Aller au contenu
  1. Posts/

QFieldCloud auto-hébergé [E05] : l'instance de base, prête pour la production

Sommaire
QFieldCloud auto-hébergé - Cet article fait partie d'une série.
Partie 5: Cet article

Introduction
#

L’épisode précédent est parti d’un dossier vide et s’est arrêté sur une instance qui fabrique de vrais paquets QField. C’est une réussite technique, et ce n’est pas encore une instance qu’on laisse tourner : elle s’ouvre à qui trouve son adresse, elle présente un certificat que personne ne reconnaît, et tout ce qu’elle contient tient sur un seul disque.

Épisode 4 — du dossier vide au premier paquet QField
Cet épisode monte l’instance de base : celle qui se suffit à elle-même, qu’on peut exposer sans rougir, et sur laquelle on branchera ensuite les briques extérieures. Ni plus, ni moins.

C’est aussi une réponse à une question qu’on se pose mal. « Mettre en production » n’est pas une case à cocher : c’est décider, brique par brique, ce qui vit dans l’instance et ce qui vit à côté. Cet article traite le dedans. Le dehors — stockage objet, courriel, certificat quand un frontal est devant (traefik), sauvegardes — fait l’objet de six annexes, à lire le jour où vous en avez besoin, pas d’affilée. La cinquième annexe n’installe rien : c’est le récit d’une panne, celle de photos de terrain dont le stockage externe avait disparu. La sixième ne déplace rien non plus : elle met l’instance aux couleurs de la maison, et c’est le meilleur test du pari de l’épisode 3.

Le point de départ : un dépôt, pas un tutoriel
#

Tout ce qui suit part du dépôt template construit à l’épisode 4 :

Son principe tient en une phrase : opengisch/QFieldCloud est un sous-module épinglé sur un tag, en lecture seule, et tout ce qui vous appartient vit un cran au-dessus — un .env, un unique fichier Compose d’override, un Makefile. Le contrôle qui dit si vous avez contracté une dette est simple :

$ git -C src status --short
$

S’il n’affiche rien, vous pourrez monter de version en changeant un tag. S’il affiche quelque chose, vous venez de créer du travail pour votre futur vous. Toute la suite respecte cette règle : pas une ligne modifiée dans le sous-module, y compris pour retirer des services.

Le démarrage tient dans le README du dépôt et n’est pas répété ici. Ce qui suit commence là où il s’arrête : une instance qui répond, et qu’il faut maintenant rendre présentable.

Durcir l’instance de base
#

Cinq réglages, tous dans le .env ou dans l’override, tous vérifiés sur l’instance.

Fermer les inscriptions
#

C’est le plus important, et le plus facile à manquer :

QFIELDCLOUD_ACCOUNT_ADAPTER=qfieldcloud.core.adapters.AccountAdapterSignUpClosed

Le défaut du dépôt upstream est AccountAdapterSignUpOpen : n’importe qui trouvant votre URL peut se créer un compte. Le passage en SignUpClosed bascule l’instance en mode sur invitation, sans rien casser d’autre — les invitations continuent de fonctionner, l’administration Django aussi.

Attention : ce réglage a une conséquence directe sur le courriel. Une fois les inscriptions fermées, l’invitation par courriel devient le seul chemin d’entrée d’un nouvel utilisateur. Le courriel cesse d’être un confort et devient un mécanisme d’inscription — voir l’annexe A03.

Publier les ports au bon endroit
#

Le profil standalone publie plusieurs services sur toutes les interfaces, ce qui est cohérent pour du développement et beaucoup moins pour une machine exposée. L’override reprend la main :

  nginx:
    ports: !override
      - "127.0.0.1:${WEB_HTTP_PORT}:80"     # le port clair reste sur loopback
      - "${WEB_BIND_IP}:${WEB_HTTPS_PORT}:443"

  smtp4dev:
    ports: !override
      - "${SMTP4DEV_BIND_IP}:${SMTP4DEV_WEB_PORT}:80"   # 127.0.0.1
      - "${SMTP4DEV_BIND_IP}:${SMTP4DEV_SMTP_PORT}:25"
      - "${SMTP4DEV_BIND_IP}:${SMTP4DEV_IMAP_PORT}:143"

Le principe : rien sur 0.0.0.0. Ce qui ne doit être atteint que par vous va sur 127.0.0.1 et se consulte par un tunnel SSH ; ce qui doit être atteint par d’autres conteneurs va sur la passerelle Docker ; et seul le port HTTPS est publié sur une adresse du réseau.

Un détail qui coûte une soirée : SMTP4DEV_SMTP_PORT vaut 25 par défaut. Sur une Debian avec un agent de transport local, le démarrage échoue sur un address already in use qui ne nomme pas le coupable. Vérifiez avec ss -tlnp | grep ':25 ', et déplacez-le — 2525 fait très bien l’affaire.

Ne pas journaliser toutes les requêtes SQL
#

L’override standalone de l’upstream lance PostgreSQL avec log_statement=all. C’est précieux en développement, et c’est un remplissage de disque en production :

  db:
    command:
      - postgres
      - -c
      - log_statement=ddl
      - -c
      - shared_buffers=256MB

Les deux réglages qu’on oublie
#

QFIELDCLOUD_DEFAULT_TIME_ZONE="Europe/Paris"   # défaut upstream : Europe/Zurich
DEBUG=0

DEBUG=0 mérite un rappel : c’est lui qui déclenche la panne des workers décrite à l’épisode 4, et le correctif — NGINX_ALLOW_INTERNAL_HTTP=1, un alias réseau sur nginx, et l’URL du worker qui passe par ce nom — fait partie du template. Sans lui, une instance en DEBUG=0 répond parfaitement et aucun job ne se termine.

Le certificat, en exposition directe
#

Si votre instance est joignable directement depuis Internet — pas de frontal, le DNS public pointe sur cette machine — la stack sait obtenir son propre certificat, et c’est le cas le plus simple.

Ce qui est déjà en place
#

Deux services y participent, et il faut comprendre leur rôle respectif pour ne pas se retrouver bloqué.

mkcert génère au premier démarrage un certificat signé par une CA privée, dans conf/nginx/certs/. Il ne sert à rien en production — mais il évite le blocage classique de tout déploiement certbot : nginx refuse de démarrer sans certificat, et certbot ne peut pas en obtenir un sans que nginx réponde sur le port 80. Ici, nginx a toujours quelque chose à présenter dès le premier up.

certbot tourne en boucle (certbot renew toutes les douze heures) et partage avec nginx un volume servi sur /.well-known/acme-challenge/. Le template nginx exclut d’ailleurs explicitement ce chemin de la redirection vers HTTPS — c’est prévu.

La marche à suivre
#

# 1. Le DNS public doit pointer sur cette machine, et les deux ports
#    doivent lui parvenir. C'est la seule condition non négociable.
QFIELDCLOUD_HOST=qfieldcloud.example.org
WEB_BIND_IP=0.0.0.0        # ici, et seulement ici, on publie largement

# 2. On commence en staging : Let's Encrypt applique des quotas stricts,
#    et une erreur de configuration se paie en heures d'attente.
LETSENCRYPT_EMAIL="vous@example.org"
LETSENCRYPT_STAGING=1

# 3. Une fois un certificat de staging obtenu, on repasse en production
#    et on pointe nginx vers les fichiers de Let's Encrypt.
LETSENCRYPT_STAGING=0
QFIELDCLOUD_TLS_CERT="/etc/letsencrypt/live/${QFIELDCLOUD_HOST}/fullchain.pem"
QFIELDCLOUD_TLS_KEY="/etc/letsencrypt/live/${QFIELDCLOUD_HOST}/privkey.pem"

Ces deux derniers chemins sont ceux que l’upstream documente lui-même dans .env.example ; par défaut, le .env pointe vers les fichiers de mkcert. C’est la bascule qu’on oublie : on obtient un vrai certificat, et nginx continue de servir celui de la CA privée parce que personne n’a changé ces deux lignes.

Info : les sorties de commande de cette section sont les seules de toute la série qui ne proviennent pas de mon instance. Celle-ci est aujourd’hui derrière un frontal, cas traité dans l’annexe A01 : elle n’a donc jamais joué le scénario certbot en direct. La procédure ci-dessus s’appuie sur le fichier .env.example de l’upstream et sur la lecture des fichiers Compose, pas sur une exécution. Je la corrigerai le jour où je basculerai l’instance en direct.

Si un frontal est devant
#

C’est l’autre cas, et il inverse le travail : le certificat existe déjà, un cran plus haut, et il faut retirer certbot plutôt que le configurer. Ça paraît anodin, ça ne l’est pas — le trafic entre le frontal et l’instance ne peut pas simplement passer en clair, et toutes les adresses IP de vos journaux changent de sens.

Annexe A01 — un frontal devant l'instance

Ce qui reste dehors, et pourquoi
#

L’instance de base embarque tout : son stockage objet, son serveur de courriel, sa base. C’est un choix parfaitement défendable pour démarrer, et il a une limite : la colocalisation. Base, objets, images et journaux vivent sur le même disque, dans le même conteneur. Une saturation les emporte tous ensemble.

Trois briques gagnent à sortir, et chacune a son annexe. Aucune n’est urgente le premier jour ; toutes le deviennent. Une quatrième annexe ferme la section sans rien déplacer : elle raconte ce qu’il advient quand le dehors, lui, disparaît.

Le stockage objet. C’est le seul déplacement qui change vraiment l’architecture. Une fois le bucket ailleurs, deux services de la stack disparaissent — et une option que la documentation ne mentionne pas devient indispensable.

Annexe A02 — sortir le stockage objet

Le courriel. L’instance de base embarque un piège à courriels : rien ne part. Tant que vos utilisateurs sont deux ou trois et que vous leur transmettez les liens à la main, c’est tenable. Au-delà, ça ne l’est plus.

Annexe A03 — brancher un vrai relais de courriel

Les sauvegardes. Trois choses, et trois seulement : la base, le bucket, et le .env — sans lequel les deux autres sont inexploitables, puisque ses clés déchiffrent les champs chiffrés de la base. Le dispositif s’appuie sur un ordonnanceur déjà présent dans la stack, ofelia, et l’annexe raconte le piège qui a failli en rendre la moitié inopérante sans le moindre message d’erreur.

Annexe A04 — sauvegardes ordonnancées, restauration testée

Et quand le dehors a disparu. La dernière annexe ne se range pas avec les autres : elle n’installe rien, elle enquête. Un projet QGIS qui range ses photos dans un S3 externe, par URL absolue, et un serveur qui n’existe plus. Ce qu’il en reste, ce qui relie encore une photo à son objet, et pourquoi la remise en service passe par reconstruire l’endpoint plutôt que par réécrire la donnée. C’est le cas d’usage qui donne rétrospectivement leur valeur aux quatre autres.

Annexe A05 — quand le stockage externe a disparu

Et ce qui reste dedans, mais change de couleur
#

Une dernière annexe ne déplace rien du tout. Elle répond à la demande qui arrive toujours en dernier, une fois l’instance en service : y mettre le logo et les couleurs de la maison.

Elle mérite sa place ici parce qu’elle est le meilleur test de tout ce qui précède. Le gabarit est là, sous les yeux, dans src/ : deux minutes de travail, et une montée de version qui l’écrase sans prévenir. La faire sans toucher à src/ demande de connaître le point d’extension de Django — DJANGO_SETTINGS_MODULE —, de comprendre que la racine du site redirige vers l’admin et qu’il y a donc deux thèmes à poser, et d’accepter une seule dette, nommée et outillée.

Annexe A06 — habiller l'instance sans toucher au sous-module

Conclusion
#

Une instance de base, ce n’est pas une instance au rabais : c’est une instance dont on sait exactement où s’arrêtent les responsabilités.
  • git -C src status doit rester vide. C’est le seul contrôle qui dit si votre déploiement vieillira bien. Retirer un service upstream se fait par un profil Compose, pas par une édition.
  • Le défaut upstream laisse les inscriptions ouvertes. AccountAdapterSignUpClosed est le premier réglage à changer, et il rend le courriel critique.
  • Rien sur 0.0.0.0 sauf le port HTTPS. Le reste va sur 127.0.0.1 ou sur la passerelle Docker.
  • log_statement=all n’a rien à faire en production, et c’est le défaut du profil standalone.
  • En exposition directe, obtenir le certificat ne suffit pas : il faut aussi faire pointer QFIELDCLOUD_TLS_CERT et QFIELDCLOUD_TLS_KEY vers les fichiers de Let’s Encrypt. Sinon nginx continue de servir le certificat de la CA privée.
  • Commencez toujours en LETSENCRYPT_STAGING=1. Les quotas de Let’s Encrypt se paient en heures d’attente.
  • Ce qui reste dedans est un choix, pas une fatalité. Stockage, courriel et sauvegardes se branchent dehors quand le besoin arrive, sans rien réécrire.

L’instance de base tient donc sur un sous-module épinglé, un .env, un fichier d’override d’une soixantaine de lignes et un Makefile. Les six annexes qui suivent ne la modifient jamais : elles ajoutent des lignes à ces deux fichiers, retirent des services par des profils, et — pour la dernière — montent des fichiers à nous par-dessus l’image. C’était le pari de l’épisode 3 ; c’est ce qui rend cette série finie plutôt qu’interminable.

Documentation self-hosted de QFieldCloud

Image de couverture : Scott Blake sur Unsplash.

QFieldCloud auto-hébergé - Cet article fait partie d'une série.
Partie 5: Cet article

Articles connexes