Quand vous déployez une application Laravel sur sh0, elle obtient automatiquement un sous-domaine comme my-laravel.sh0.app. Chaque service avec un port HTTP en obtient un : my-laravel-phpmyadmin.sh0.app, my-laravel-redis.sh0.app. Pas de configuration DNS, pas de configuration de proxy, pas de gestion de certificats SSL. Cela fonctionne tout simplement.
Mais le stockage de fichiers était différent. Quand nous avons livré MinIO géré lors de la session précédente, l'API S3 et la console web n'étaient accessibles que via des mappages de ports localhost. Vous pouviez voir http://localhost:55519 dans le dashboard, mais c'est inutile si vous êtes sur votre téléphone, partagez un lien avec un collègue ou déployez un frontend qui a besoin d'un endpoint S3 public.
La question était simple : pourquoi les services d'infrastructure ne reçoivent-ils pas le même traitement que les applications ?
Le patron existant
sh0 possédait déjà un système mature de sous-domaines automatiques pour les services d'applications. Quand un template se déploie, le pipeline dans templates.rs exécute une étape appelée "6b: Auto-subdomain for secondary HTTP services" qui fait trois choses :
- Génère le sous-domaine --
{app_name}-{service_name}.{base_domain} - Configure Caddy -- crée une route de proxy inverse depuis ce domaine vers l'IP et le port du conteneur sur le réseau Docker
sh0-net - Crée un enregistrement DNS -- appelle l'API Cloudflare pour pointer le sous-domaine vers l'IP publique du serveur
Le certificat SSL est géré automatiquement par Caddy via Let's Encrypt. Le tout prend environ deux secondes.
Le stockage de fichiers avait déjà son propre système de domaines -- les utilisateurs pouvaient ajouter manuellement des domaines personnalisés via un onglet "Domaines", et le backend configurait Caddy et Cloudflare. Mais la partie automatique manquait.
La décision de conception
Nous avions deux choix pour le moment où les domaines automatiques sont attribués :
Option A : Uniquement quand l'utilisateur clique sur un bouton. Sûr, explicite, pas de surprises.
Option B : Automatiquement à la création, plus un bouton pour les instances existantes. Zéro friction pour les nouveaux déploiements, avec un recours pour les instances créées avant cette fonctionnalité.
Nous avons opté pour l'Option B. Tout l'intérêt de sh0 est que l'infrastructure devrait être invisible. Si vous créez une instance de stockage, vous voulez probablement y accéder depuis l'extérieur de localhost.
Le patron de nommage suit la convention des applications :
- API S3 : {instance-name}-s3.{base_domain} (ex. : system-storage-s3.sh0.app)
- Console web : {instance-name}-console.{base_domain} (ex. : system-storage-console.sh0.app)
L'implémentation
Un helper partagé, pas de la logique dupliquée
Le handler existant add_storage_domain faisait déjà tout le nécessaire : valider le domaine, vérifier l'unicité inter-tables, insérer un enregistrement FileStorageDomain, configurer Caddy et créer un enregistrement DNS. Nous avons extrait les parties réutilisables dans une nouvelle fonction :
rustasync fn auto_assign_storage_domains(
state: &AppState,
storage_id: &str,
instance_name: &str,
) -> Result<Vec<FileStorageDomainResponse>> {
let base_domain = match &state.base_domain {
Some(bd) => bd.clone(),
None => return Ok(vec![]), // Pas de domaine de base configuré -- on passe silencieusement
};
let candidates = [
(format!("{}-s3.{}", instance_name, base_domain), "api"),
(format!("{}-console.{}", instance_name, base_domain), "console"),
];
// Pour chaque candidat : vérifier l'unicité, insérer, DNS, puis mettre à jour Caddy
// ...
}Décisions de conception clés dans cette fonction :
- Retour anticipé si pas de
base_domain-- les instances auto-hébergées sans domaine enregistré passent l'attribution automatique silencieusement. Pas d'erreur, pas de bruit dans les logs.
- Idempotent -- chaque sous-domaine est vérifié à la fois dans la table
domainsdes applications et la tablefile_storage_domainsavant insertion. Appeler la fonction deux fois est sans danger.
- Le routage Caddy est groupé --
update_proxy_for_storageest appelé une seule fois à la fin, pas par domaine. Il regroupe tous les domaines API dans une route Caddy (port upstream 9000) et tous les domaines console dans une autre (port upstream 9001).
Non-fatal à la création
Le détail critique : l'attribution automatique de domaines pendant la création d'instance ne doit pas bloquer la création elle-même.
rust// Dans create_instance() :
if let Err(e) = auto_assign_storage_domains(&state, &created.id, &created.name).await {
tracing::warn!(instance = %created.name, error = %e,
"Failed to auto-assign storage domains");
}Si Cloudflare est en panne, si le domaine existe déjà, si quoi que ce soit tourne mal -- l'instance de stockage est quand même créée. L'utilisateur obtient son MinIO, et il peut attribuer des domaines plus tard via le bouton.
L'endpoint du bouton
Pour les instances existantes créées avant cette fonctionnalité, nous avons ajouté un endpoint simple :
POST /api/v1/file-storage/{id}/auto-domainIl appelle le même helper auto_assign_storage_domains. Le dashboard affiche une carte "Activer l'accès externe" dans l'onglet vue d'ensemble quand aucun domaine n'existe, suivant le patron utilisé pour les services d'applications.
Ce que Caddy voit
Quand la fonction termine, la configuration de Caddy contient deux nouveaux blocs de routes. Simplifié :
json{
"match": [{"host": ["system-storage-s3.sh0.app"]}],
"handle": [{
"handler": "reverse_proxy",
"upstreams": [{"dial": "172.18.0.5:9000"}]
}]
}Le port 9000 est l'API S3 de MinIO. Le port 9001 est la console web. Caddy gère la terminaison TLS et le provisionnement des certificats automatiquement.
La dimension i18n
Cette session a aussi révélé une lacune : des dizaines de chaînes anglaises codées en dur dans les pages de stockage de fichiers. "Console Username", "Enable subdomain", "Browse", "DNS Active", descriptions de cartes de fonctionnalités -- tout en anglais brut.
Nous avons ajouté 25 nouvelles clés de traduction dans les cinq langues (anglais, français, espagnol, portugais, swahili) et remplacé chaque chaîne codée en dur par des appels t(). C'est important car sh0 cible les développeurs africains, dont beaucoup préfèrent des interfaces en français ou en portugais.
La page de domaines (/domains) avait le même problème -- les en-têtes de tableau comme "Service", "Status", "Instance", "Target" étaient tous codés en dur. Corrigés aussi.
Le patron à retenir
La leçon plus large ici : quand vous construisez une fonctionnalité pour une catégorie de ressources (les applications), concevez le système sous-jacent (intégration Caddy + Cloudflare) pour être réutilisable. Quand nous avons construit le système de domaines pour le stockage de fichiers un jour plus tôt, nous l'avons modelé d'après le système de domaines des applications -- même patron de schéma de base de données, même logique de configuration de proxy, même intégration DNS. Cela a fait des sous-domaines automatiques une question d'appeler des fonctions existantes dans le bon ordre, pas de construire une nouvelle infrastructure.
L'implémentation totale était une fonction helper Rust, un endpoint API, un enregistrement de route et une poignée de changements frontend. Le travail difficile était déjà fait.