Aller au contenu
Bastide Connexion Créer un compte
Appuyez sur / pour chercher

Migrer depuis un VPS ou un serveur maison

EnglishMarkdownv0.3.0

Cette page déplace une application qui tourne sur votre propre serveur (un VPS, une machine dédiée, un serveur de bureau) vers Bastide : le processus devient un conteneur, la base PostgreSQL passe dans le backend du projet, les fichiers locaux vont au stockage objet, puis le domaine bascule sans coupure. Il n'existe pas d'importeur automatique pour ce cas : chaque étape se mène avec les outils ordinaires.

Faire l'inventaire

Avant de commencer, notez ce que le serveur fait vraiment :

  • les processus qui tournent (le service web, des ouvriers, un crontab), leur commande de démarrage et le port qu'ils écoutent ;
  • la base de données, sa version et ses extensions ;
  • les fichiers que l'application écrit sur le disque (téléversements, caches, exports) ;
  • les variables d'environnement et les fichiers de configuration qui portent des secrets ;
  • les domaines, et qui gère leur DNS.

Ce qui ne tient pas dans cette liste (un démon maison, un port TCP autre que HTTP, un accès SSH aux machines) n'a pas d'équivalent sur Bastide : lisez la section « Ce qui ne passe pas » avant d'aller plus loin.

1. Conteneuriser l'application

Bastide construit trois sortes de dépôts, dans cet ordre : un Dockerfile à la racine, un cadriciel reconnu (voir Cadriciels), ou à défaut un repli qui détecte lui-même le langage. Pour une application Node, Python ou Go ordinaire, ne fournissez pas de Dockerfile : la plateforme compose l'image. Fournissez-en un si votre serveur installe des paquets système ou lance une commande particulière.

Le contrat d'exécution, que l'image doit tenir :

  • Le port. L'application écoute sur 0.0.0.0 et sur le port que donne la variable PORT, posée par la plateforme. Une image composée par la plateforme écoute sur 3000 (8080 pour un site statique). Avec votre propre Dockerfile, le port vaut 3000 sauf si le dépôt le déclare : un EXPOSE ne suffit pas, déclarez port dans glowsoft.json.
  • Le démarrage. L'image déclare ENTRYPOINT ou CMD. L'application répond à la sonde de démarrage dans le délai accordé ; le chemin de la sonde et ce délai se déclarent dans glowsoft.json (probe.path, probe.startupBudgetSeconds).
  • L'arrêt. Le conteneur reçoit SIGTERM, puis ses requêtes en cours ont 30 secondes pour finir. STOPSIGNAL n'admet que des signaux nommés de la liste servie.
  • Le disque. Le système de fichiers de l'image est en lecture seule. Seul /tmp s'écrit, borné par le palier, et il est perdu au redémarrage. VOLUME est refusé : rien de ce qui doit durer ne vit sur le disque du conteneur.
  • L'architecture. L'image vise linux/amd64.
  • Les sources. Les images de base viennent d'un registre admis (docker.io, ghcr.io, gcr.io, mcr.microsoft.com, quay.io, public.ecr.aws, registry.k8s.io) ; apt et apk ne joignent que les dépôts officiels d'Alpine, Debian et Ubuntu. La directive # syntax=, ADD d'une adresse distante et les options privilégiées de RUN sont refusées.

Un Dockerfile minimal pour une application Node qui tournait sous pm2 ou systemd :

FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node", "server.js"]

Un processus de fond (une file de travaux) devient un service worker déclaré dans glowsoft.json, dans la limite des services du palier ; ce découpage n'est servi que lorsque la plateforme compose l'image Node elle-même. Un dépôt qui fournit son Dockerfile reste servi en un seul service. Les lignes du crontab deviennent des tâches planifiées (voir Tâches planifiées).

Chaque refus de construction nomme sa cause, le lieu à corriger et l'action suivante ; la liste des causes fréquentes est dans Dépannage.

2. Créer le projet et son backend

Créez le projet avec son backend, depuis Nouveau projet ou par l'outil project_create_with_backend, en donnant l'adresse du dépôt. Le backend porte une base PostgreSQL 17. La plateforme pose d'office deux variables sur chaque environnement :

  • DATABASE_URL, le mode transaction du mutualiseur, pour l'application ;
  • DIRECT_URL, le mode session, pour les outils de migration qui tiennent un verrou de session (Prisma, par exemple).

Si vous posez vous-même DATABASE_URL (pour garder une base ailleurs pendant la transition), la plateforme n'y touche pas.

3. Déplacer la base PostgreSQL

La base d'un projet n'est pas joignable depuis l'extérieur : seul le mutualiseur de la plateforme y mène, et la connexion directe d'un outil d'administration n'est pas encore ouverte. pg_restore ne se lance donc pas contre Bastide depuis votre poste. La donnée passe par du SQL, que la plateforme exécute pour vous.

Sur le serveur, exportez la base au format personnalisé, puis tirez-en le schéma en SQL :

pg_dump --format=custom --no-owner --no-privileges --file=base.dump "$SOURCE_DATABASE_URL"
pg_restore --schema-only --no-owner --no-privileges --file=schema.sql base.dump

Relisez schema.sql avant de l'appliquer :

  • Les extensions doivent être admises par le palier : 20 en Découverte et 50 en Standard, sur liste blanche ; arbitraires à partir de Pro.
  • Les rôles de l'API portent le nom de la base en préfixe : un grant … to authenticated écrit tel quel est refusé (LIM-11). Retirez les grant qui visent des rôles de votre serveur.
  • Une table n'est exposée à l'API REST que si elle porte la sécurité au niveau des lignes et au moins une politique.

Appliquez le schéma par database.migrate (jusqu'à 1 000 000 caractères par appel) : il est d'abord répété sur une base fantôme, et un effet destructif est refusé.

bastide database migrate --project-id <prj_…> --sql "$(cat schema.sql)"

Chargez ensuite les données, table par table, en instructions INSERT :

pg_dump --data-only --inserts --rows-per-insert=200 --table=public.clients --file=clients.sql "$SOURCE_DATABASE_URL"

Chaque lot passe par database.sql.write, en deux temps : le premier appel exécute, mesure l'effet, l'annule et rend un nonce ; le second, avec --confirmation-nonce, valide si l'effet est le même. Un appel porte au plus 100 000 caractères : découpez les gros fichiers. Ce va-et-vient est ce qu'un agent fait bien, par l'outil database_sql_write ; une personne le relit à l'audit.

Ce chemin convient à une base de quelques centaines de mégaoctets. Pour une base plus grosse, écrivez au support avant de commencer.

Vérifiez enfin le compte des lignes de chaque table des deux côtés, par database.sql.read ou l'Éditeur SQL.

4. Déplacer les fichiers vers le stockage

Les fichiers que l'application écrivait sur le disque vont dans un seau du stockage objet du projet. Le point compatible S3 (/storage/v1/s3) n'est pas servi (LIM-07) : un client aws s3 ou rclone en mode S3 ne fonctionne pas. Le stockage se sert par l'API de storage-js, sous /storage/v1/bucket et /storage/v1/object.

Un script de copie, à lancer depuis le serveur avec une clé secrète du projet (sb_secret_…, créée par backend.key.create) :

import { createClient } from "@supabase/supabase-js";
import { readFile, readdir } from "node:fs/promises";
import { join } from "node:path";

const bastide = createClient(process.env.BASTIDE_URL, process.env.BASTIDE_SECRET_KEY);
await bastide.storage.createBucket("televersements", { public: false });
for (const nom of await readdir("/var/www/app/uploads")) {
  const octets = await readFile(join("/var/www/app/uploads", nom));
  const { error } = await bastide.storage.from("televersements").upload(nom, octets);
  if (error) console.error(nom, error.message);
}

Un fichier dépasse-t-il la taille maximale du palier (50 Mo en Découverte, 5 Go en Standard, 50 Go en Pro), le téléversement est refusé en le nommant. Adaptez ensuite le code de l'application pour lire et écrire dans le seau plutôt que sur le disque. Voir Stockage.

5. Reporter les variables

Ouvrez Variables et utilisez « Importer un .env » en collant le fichier .env du serveur, ou écrivez chaque variable par l'outil env_write_production. Les valeurs sont sensibles par défaut : elles ne se relisent plus en clair. Si le dépôt porte un .env.example, une construction est refusée tant qu'une variable qu'il déclare manque (variables_requises_manquantes) ; une variable posée vide suffit si l'application s'en accommode. Voir Environnements.

6. Déployer et éprouver

Déployez par deployment_create, suivez le travail par task_read, lisez le journal. L'application tourne alors sur son adresse de déploiement, à côté du serveur qui sert toujours la production. Rejouez vos parcours : connexion, lecture, écriture, téléversement.

7. Basculer sans coupure

Il n'existe pas de réplication continue depuis votre serveur : ce qui s'écrit à la source après la copie ne suit pas. La bascule se fait donc en deux temps.

  1. Rattachez le domaine (Domaines, ou domain.add) et posez le TXT de preuve, valable sept jours. Posez aussi le CNAME du champ acmeChallenge : le certificat est émis avant la bascule.
  2. Baissez la durée de vie (TTL) des enregistrements DNS actuels quelques heures avant.
  3. À l'heure choisie, passez l'application du serveur en lecture seule ou en maintenance.
  4. Rejouez la copie des données et des fichiers écrits depuis la première copie, puis comparez les comptes.
  5. Remplacez les enregistrements DNS par ceux que rend domain.add : A et AAAA pour un apex, CNAME pour un sous-domaine.
  6. Gardez le serveur en lecture seule quelques jours, le temps que les anciens caches DNS expirent, puis arrêtez-le.

En cas de problème avant l'étape 5, rien n'a changé pour vos visiteurs : levez la maintenance du serveur.

Combien de temps compter

  • La conteneurisation : de quelques minutes pour une application que la plateforme reconnaît, à une journée pour un serveur qui installait beaucoup de choses à la main.
  • Une construction prend, au 95e centile mesuré, de quelques secondes à un peu plus de deux minutes selon le cadriciel.
  • La base : le temps de l'export, puis celui des allers-retours de database.sql.write, proportionnel au volume.
  • La fenêtre de bascule : le temps de rejouer les écritures récentes, plus la propagation DNS, de quelques minutes si la durée de vie a été baissée.

Ce qui ne passe pas

  • Un disque persistant attaché au conteneur : les fichiers vont au stockage objet, les données à la base.
  • Un port entrant autre que HTTP et HTTPS : le trafic passe par l'edge de la plateforme, et aucun port n'est ouvert sur les serveurs d'exécution.
  • Un accès SSH au conteneur, ou une commande lancée à la main sur la machine.
  • Une image linux/arm64.
  • Le SMTP sortant depuis le projet gratuit ; pour les courriels, utilisez les courriels transactionnels du backend (voir Courriels).

L'offre de migration (50 % pendant 3 mois) vaut aussi pour un projet venu d'un serveur, sur présentation d'une facture de l'ancien hébergement datée de moins de 3 mois ; voir Facturation.

Voir aussi : Migrer depuis Vercel, Migrer depuis Supabase, Base de données, Domaines, Sortir de Bastide.

Signaler une erreur sur cette page