À qui s’adresse cette annexe #
QFieldCloud auto-hébergé [E05] : l'instance de base, prête pour la production
Les fichiers qu’on touche #
Deux, et la bascule tient dans le premier. Chaque bloc de code plus bas rappelle en première ligne le fichier auquel il appartient.
| Fichier | Ce qu’on y met |
|---|---|
.env |
les cinq variables EMAIL_* et DEFAULT_FROM_EMAIL |
docker-compose.override.yml |
le profil never sur smtp4dev, ou ses trois ports dissociés tant qu’on le garde |
L’effet de bord qu’on n’avait pas vu venir #
À la fin de l’épisode 4, l’instance est passée sur AccountAdapterSignUpClosed, un adaptateur pour allauth — la bibliothèque tierce qui gère l’inscription et la connexion dans QFieldCloud. Résultat : plus personne ne peut créer son compte depuis la page d’inscription. Bonne décision de sécurité — sur une instance publiée, le défaut upstream laisse n’importe qui s’enregistrer.
Elle a une conséquence qu’il faut regarder en face : le seul chemin d’entrée en libre-service devient l’invitation par courriel. Le courriel cesse d’être un confort et devient le mécanisme d’inscription.
docker compose se lancent depuis src/, le répertoire du sous-module — c’est de là que COMPOSE_FILE est résolu — et avec --env-file ../.env. Chaque bloc le rappelle par son cd src, à ne faire qu’une fois par session. Le $ en début de ligne est le symbole du terminal, il ne se copie pas.
Une autre voie subsiste, et elle mérite d’être connue avant de se croire bloqué : l’administrateur peut créer le compte lui-même, depuis l’admin Django (/admin/, modèle Person) ou en ligne de commande.
$ cd src && docker compose --env-file ../.env exec app python manage.py createuser \
--username=dupont --email=dupont@exemple.fr --password='…'
User dupont has been successfully created
Le compte obtenu est complet : User.save() crée le UserAccount et l’abonnement courant dans la même transaction. Et comme ACCOUNT_EMAIL_VERIFICATION vaut optional, l’adresse non vérifiée n’empêche pas la connexion.
Deux réserves, cependant. La docstring de createuser annonce elle-même un usage de test, et le mot de passe passe en clair par la ligne de commande — donc par l’historique du shell : préférez l’admin Django, ou changez le mot de passe juste après. Surtout, cette voie ne remplace pas le courriel, elle le déplace : il faut transmettre l’identifiant et le mot de passe par un autre canal, et l’utilisateur qui oubliera ce dernier n’aura toujours aucun moyen de le réinitialiser seul. Elle dépanne pour trois comptes ; elle ne tient pas pour trente.
Or voici ce que fait l’instance quand on lui demande d’envoyer :
$ cd src && docker compose --env-file ../.env exec app python manage.py sendtestemail vous@exemple.fr
$ curl -s 'http://127.0.0.1:8012/api/Messages?pageSize=5'
messages piégés: 1
- 2026-09-04T14:08:19 | webmaster@localhost -> ['vous@exemple.fr']
| Test email from 7bd08766e2a9 on 2026-09-04 14:08:19
Le message n’est pas parti : il est dans le piège, consultable sur http://127.0.0.1:8012. Et son expéditeur est webmaster@localhost, valeur par défaut de DEFAULT_FROM_EMAIL — une adresse sans domaine pleinement qualifié, que le premier relais venu refuserait.
Ce qui est bloqué, concrètement #
| Fonctionnalité | Ce qui se passe |
|---|---|
| Invitation d’un utilisateur | Le message part dans le piège. Il faut y chercher le lien et le transmettre à la main |
| Réinitialisation de mot de passe | Idem — l’utilisateur attend un courriel qui n’arrivera jamais |
Vérification d’adresse (ACCOUNT_EMAIL_VERIFICATION=optional) |
Sans effet visible, puisqu’optionnelle |
| Notifications de fin de job | Perdues silencieusement |
ACCOUNT_EMAIL_VERIFICATION à mandatory en croyant durcir l’instance. Avec un piège à courriels, plus personne ne peut activer son compte — vous compris.
Pour une instance à poignée d’utilisateurs, tous connus, le compromis se défend : on relève les invitations dans l’interface de smtp4dev et on transmet le lien par le canal qu’on veut. À condition de le savoir, et de ne pas découvrir le jour J que l’invitation envoyée à un collègue n’est jamais partie.
La bascule, en cinq variables #
# .env
EMAIL_HOST="smtp.exemple.fr" # au lieu de smtp4dev
EMAIL_PORT=587 # au lieu de 25
EMAIL_USE_TLS=True # STARTTLS ; EMAIL_USE_SSL pour du 465 implicite
EMAIL_HOST_USER="…"
EMAIL_HOST_PASSWORD="…"
DEFAULT_FROM_EMAIL="qfieldcloud@votre-domaine.fr" # LA variable qu'on oublieAucune modification du sous-module. Deux confusions classiques :
EMAIL_PORT n’a rien à voir avec SMTP4DEV_SMTP_PORT. Le premier est le port que l’application compose sur le réseau Docker ; le second, le port publié sur l’hôte — déplacé sur 2525 à l’épisode 4 pour éviter le conflit avec l’agent de transport local. Changer l’un en croyant agir sur l’autre est le classique du genre.
DEFAULT_FROM_EMAIL doit appartenir à un domaine dont les enregistrements SPF autorisent le relais choisi. Faute de quoi les messages partent bien — et arrivent en indésirables, ce qui est plus difficile à diagnostiquer qu’un échec franc.
Une fois le relais en place, smtp4dev se retire exactement comme certbot :
# docker-compose.override.yml
smtp4dev:
profiles: ["never"]Avec la même réserve qu’en A01 : le profil exclut le service de la configuration, il ne supprime pas le conteneur qui tourne déjà. up -d --remove-orphans le laissera en place, invisible dans docker compose config et bien présent dans docker ps. Il faut le nommer, en réactivant le profil le temps de la commande :
$ cd src && docker compose --env-file ../.env --profile never rm -sf smtp4dev
Y accéder tant qu’il est là #
L’interface web écoute sur le port SMTP4DEV_WEB_PORT (8012 ici), et l’adresse sur laquelle elle est publiée décide de tout le reste.
Par défaut, 127.0.0.1 : elle n’est joignable que depuis la machine elle-même, donc par un tunnel SSH depuis votre poste.
$ ssh -N -L 8012:127.0.0.1:8012 utilisateur@10.0.0.10
# puis http://127.0.0.1:8012 dans le navigateur
C’est fastidieux pour une consultation quotidienne, et la tentation est grande de publier l’interface sur le LAN. Elle est légitime — à une condition près, qu’il faut avoir en tête.
Si vous la publiez, ne publiez qu’elle. Une seule variable pilote les trois ports dans le fichier upstream — web, SMTP et IMAP partent ensemble —, alors que l’IMAP donne accès aux mêmes messages que l’interface et que le SMTP accepterait n’importe quel envoi. Les dissocier dans l’override coûte trois lignes :
# docker-compose.override.yml
smtp4dev:
ports: !override
- "${SMTP4DEV_WEB_BIND_IP}:${SMTP4DEV_WEB_PORT}:80" # seule à sortir
- "127.0.0.1:${SMTP4DEV_SMTP_PORT}:25"
- "127.0.0.1:${SMTP4DEV_IMAP_PORT}:143"Une porte au lieu de trois, pour le même service rendu. Et un contrôle qui ne coûte rien, une fois le conteneur recréé :
$ docker port qfieldcloud-smtp4dev-1
25/tcp -> 127.0.0.1:2525
80/tcp -> 10.0.0.10:8012
143/tcp -> 127.0.0.1:143
Notez qu’après cette bascule, http://127.0.0.1:8012 ne répond plus depuis la machine elle-même : le port n’y est plus publié. C’est la même URL pour tout le monde, ce qui est plutôt une bonne chose.
Vérifier que ça marche vraiment #
$ cd src && docker compose --env-file ../.env exec app python manage.py sendtestemail vous@votre-domaine.fr
Puis regardez la boîte de réception, pas les journaux : Django considère l’envoi réussi dès que le relais a accepté le message. Le reste — SPF, DKIM, réputation — se joue après, et hors de son champ de vision.
Ce qu’il faut retenir #
- Fermer les inscriptions fait du courriel le seul chemin d’entrée en libre-service. L’administrateur garde la création à la main (
/admin/oumanage.py createuser), mais elle ne dispense pas d’un relais : la réinitialisation de mot de passe, elle, n’a pas d’équivalent manuel. -
DEFAULT_FROM_EMAILpar défaut vautwebmaster@localhost, refusé par n’importe quel relais. -
EMAIL_PORTetSMTP4DEV_SMTP_PORTdésignent deux choses différentes. -
ACCOUNT_EMAIL_VERIFICATION=mandatoryavec un piège à courriels verrouille l’instance entière.
Image de couverture : Scott Blake sur Unsplash.