- Schéma Hostinger recopié automatiquement depuis PG Debian (colonnes, PK, index) : plus d'init.sql maintenu à la main (corrige articles.artcentrale, mvtart.no_id et les 12 tables manquantes) - Stratégies : full transactionnel (DELETE + INSERT, garde-fou de volume) et window (signature mensuelle + 10 derniers jours) pour les grosses tables - Attente de la fin de la sync Debian (_run), pg_try_advisory_lock, cron 07:30 Europe/Paris, force honoré, alertes webhook - Dashboard : basic auth, CSP sans script inline, plus d'innerHTML, port 3002 non publié (accès via Nginx Proxy Manager) - Secrets hors du compose (.env), frps épinglé 0.71.0 avec TLS forcé, allowPorts et additionalScopes - Suppression de sync.sh, tables.js, utils.js, init.sql ; README de déploiement L'ancien mot de passe de la base `post` figure dans l'historique git : il doit être changé sur le serveur (voir README). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gateway Hostinger — frps + miroir PostgreSQL
Côté VPS Hostinger. Reçoit le tunnel frp du serveur Debian du magasin et recopie chaque jour
la base PostgreSQL Debian dans le PostgreSQL Hostinger (postgres_db).
Architecture
Serveur Debian (magasin) VPS Hostinger (réseau Docker externe nginx_default)
┌──────────────────────────┐ tunnel frp ┌──────────────────────────────────────────────────┐
│ frpc 0.71.0 ─────────────┼── :7000 (TLS) ──▶│ ff_frps (snowdreamtech/frps:0.71.0-alpine) │
│ API Debian :3000 ─────┼──────────────────│ → ff_frps:13000 (API Debian) │
│ PostgreSQL :5432 ─────┼──────────────────│ → ff_frps:15432 (PostgreSQL Debian) │
└──────────────────────────┘ │ │
│ ff_sync_hostinger (Node 22) │
│ lit frps:15432 (source, lecture seule) │
│ écrit postgres_db:5432 (destination) │
│ dashboard :3002 ◀── Nginx Proxy Manager (HTTPS)│
└──────────────────────────────────────────────────┘
- Seul le port 7000 est publié sur l'hôte (connexion frpc). Les ports 13000/15432 et le
dashboard 3002 ne sont joignables que depuis le réseau
nginx_default. - frps n'accepte que les ports distants 13000 et 15432 (
allowPorts) et des connexions TLS.
Fichiers
| Fichier | Rôle |
|---|---|
docker-compose.yml |
services frps et sync (aucun secret) |
frps.toml.example |
modèle de config frps → copier en frps.toml (ignoré par git) |
.env.example |
modèle des variables → copier en .env (ignoré par git) |
sync/ |
service Node du miroir PostgreSQL + dashboard |
sync/test/ |
tests des fonctions SQL pures (npm test) |
L'ancien init.sql (schéma écrit à la main) et sync/tables.js (liste de tables) ont été
supprimés : ils avaient dérivé de la source (colonnes et tables manquantes). Le schéma de la
destination est désormais recopié automatiquement depuis la source à chaque exécution.
sync/sync.sh (pg_dump, jamais utilisé) a aussi été supprimé.
Variables d'environnement (.env)
| Variable | Défaut | Description |
|---|---|---|
SRC_HOST / SRC_PORT |
frps / 15432 |
PostgreSQL Debian via le tunnel |
SRC_DB / SRC_USER / SRC_PASS |
— (obligatoire) | identifiants source (idéalement rôle lecture seule) |
DST_HOST / DST_PORT |
postgres_db / 5432 |
PostgreSQL Hostinger |
DST_DB / DST_USER / DST_PASS |
— (obligatoire) | identifiants destination |
SYNC_CRON |
30 7 * * * |
planification (cron 5 champs) |
SYNC_TZ |
Europe/Paris |
fuseau du cron |
EXCLUDE_TABLES |
vide | tables source à ne pas répliquer (a,b,c) |
WINDOW_TABLES |
liste du code | surcharge des tables « fenêtre » (table:colonne_date,…) |
WINDOW_RECENT_DAYS |
10 |
jours toujours rechargés pour les tables « fenêtre » |
DEBIAN_WAIT_POLL_MIN / DEBIAN_WAIT_MAX_MIN |
2 / 180 |
attente de la fin de la sync Debian |
DASHBOARD_USER / DASHBOARD_PASSWORD |
vide | Basic auth ; vide = dashboard désactivé (503) |
ALERT_WEBHOOK_URL |
vide | webhook Slack/Discord/Mattermost (POST {text, content}) |
Fonctionnement du miroir
Une exécution (cron, bouton du dashboard ou CLI) :
- Verrou
pg_try_advisory_locksur la destination : deux exécutions ne peuvent jamais se chevaucher, même depuis deux process (service +docker exec). La ligne_rundesync_log(destination) passe àrunning. - Attente de la sync Debian : tant que
sync_logsource atable_name='_run'etstatus='running', on attend (contrôle toutes les 2 min, 3 h max). Si la ligne n'existe pas, on continue. Après 3 h : alerte, puis copie quand même. - Lecture du catalogue source (
pg_catalog) : toutes les tables de base du schémapublic(saufEXCLUDE_TABLES), colonnes dans l'ordre avec leur type exact (format_type), NOT NULL, clé primaire, index. - Pour chaque table (chacune dans son propre try/catch, résultat écrit dans
sync_log) :- Schéma :
CREATE TABLE IF NOT EXISTS(mêmes colonnes et PK),ADD COLUMN IF NOT EXISTSpour les colonnes manquantes (ajoutées NULLables). Un type différent est seulement signalé. Rien n'est jamais supprimé côté destination (tables ou colonnes en trop conservées). - Données (voir stratégies ci-dessous), en lisant la source par curseur serveur
(
FETCH 5000) et en insérant parINSERTmulti-lignes paramétrés (floor(60000 / nb_colonnes)lignes par requête). Les valeurs transitent en texte brut : aucune perte de précision (timestamps, numeric, bytea…). - Index après la copie : index source recréés avec le même nom (
IF NOT EXISTS), clé primaire ajoutée si la table destination n'en avait pas. Si un index utilisepg_trgm, on tenteCREATE EXTENSION IF NOT EXISTS pg_trgm; en cas d'échec, ces index sont ignorés (avertissement).
- Schéma :
sync_logsource → table destinationsource_sync_log(la tablesync_logde la destination est celle du miroir et n'est jamais écrasée).- Fin :
_run=okouerror, verrou libéré, alerte webhook si au moins une table a échoué.
Stratégies de copie
full (toutes les tables sauf les tables « fenêtre ») :
BEGIN; DELETE FROM; INSERT de toutes les lignes; COMMIT dans une seule transaction. Grâce au
MVCC, les lecteurs continuent de lire l'ancienne version sans être bloqués (pas de verrou ACCESS
EXCLUSIVE comme avec TRUNCATE) et ne voient jamais une table vide ou partielle ; l'autovacuum
récupère l'espace des anciennes lignes. lock_timeout de 60 s par sécurité.
Garde-fou volume : si la source a moins de 50 % des lignes de la destination (et la
destination plus de 100 lignes), la table n'est pas copiée et passe en erreur (alerte).
window (tables de mouvements, configurées dans sync/config.js : mvtart, mvtreg,
mvtart_motif, histo_art_gamme, demarques, meteo, fidelite_jour ; une table absente de la
source est ignorée) :
- Signature par mois des deux côtés :
date_trunc('month', col), count(*), sum(hashtext(ROW(colonnes source)::text)::bigint). - Garde-fou volume sur le total des lignes.
- Les 10 derniers jours sont toujours rechargés (DELETE + INSERT dans une transaction).
- Chaque mois dont la signature diffère (ou présent d'un seul côté) est rechargé dans sa propre
transaction (
DELETEdu mois + réinsertion depuis la source).
Résultat : coût incrémental chaque nuit, propagation des corrections et suppressions anciennes,
et remplissage automatique d'une nouvelle colonne (tous les mois diffèrent une fois, puis plus).
Les deux connexions utilisent les mêmes paramètres de session (TimeZone=UTC, DateStyle=ISO,
extra_float_digits=3…) pour que les signatures soient comparables. Si un type de colonne diffère
entre source et destination, les mois concernés seront rechargés à chaque exécution : corriger le
type à la main côté destination.
Mode forcé (bouton « Forcer » avec confirmation, ou --force) : garde-fou volume ignoré et
tables « fenêtre » rechargées entièrement (stratégie full).
Dashboard
- Exposé uniquement sur
nginx_default: dans Nginx Proxy Manager, créer un proxy host versff_sync_hostinger, port3002, avec certificat SSL et « Force SSL » (Basic auth en clair sinon). - HTTP Basic auth (
DASHBOARD_USER/DASHBOARD_PASSWORD). Sans ces variables : 503. GET /healthzsans authentification (healthcheck Docker).- Bouton « Lancer la sync » = exécution normale ; « Forcer » = mode forcé (confirmation).
- Par table : stratégie, dernière sync, lignes, durée, statut, détail ou erreur.
Déploiement (VPS Hostinger)
cd ~/gateway # emplacement du dépôt sur le VPS
git pull
# 1. Secrets (une seule fois, puis à chaque rotation)
cp -n .env.example .env # compléter DST_*, DASHBOARD_*, ALERT_WEBHOOK_URL…
cp -n frps.toml.example frps.toml # si frps.toml n'existe pas encore
# → dans frps.toml existant, ajouter allowPorts / additionalScopes / tls.force (voir l'exemple)
chmod 600 .env frps.toml
# 2. Changer le mot de passe PostgreSQL destination (voir ci-dessous) et le reporter dans .env
# 3. Construire et démarrer
docker compose up -d --build
docker compose ps # ff_sync_hostinger doit passer "healthy"
docker logs -f ff_sync_hostinger
# 4. Première exécution manuelle (facultatif, sinon cron 07:30 Paris)
docker exec ff_sync_hostinger node index.js --once
Le premier passage crée les tables/colonnes/index manquants et recharge tous les mois des tables « fenêtre » : il est plus long que les suivants.
Commandes utiles :
docker exec ff_sync_hostinger node index.js --once # exécution normale
docker exec ff_sync_hostinger node index.js --force # exécution forcée
docker exec -it postgres_db psql -U post -d post -c "SELECT * FROM sync_log ORDER BY last_sync DESC"
Rotation des secrets
Token frp
openssl rand -hex 32
- Mettre le nouveau token dans
frps.toml(Hostinger) et dansfrpc.toml(Debian). docker compose restart frpscôté Hostinger, puis redémarrer frpc côté Debian.- Vérifier :
docker logs ff_frps(connexion du client, proxys 13000 et 15432 acceptés).
frpc doit être en 0.71.0 (snowdreamtech/frpc:0.71.0-alpine) avec
auth.additionalScopes = ["HeartBeats", "NewWorkConns"] et transport.tls.enable = true
(valeur par défaut), sinon frps refusera la connexion.
Mot de passe PostgreSQL post (destination)
L'ancien mot de passe de post a été commité dans l'historique git de ce dépôt : il doit être
considéré comme public et changé sur le serveur.
NEWPASS=$(openssl rand -hex 24)
docker exec -it postgres_db psql -U post -d post -c "ALTER USER post WITH PASSWORD '$NEWPASS';"
# Reporter $NEWPASS dans .env (DST_PASS) ET dans la config de tout autre service qui utilise ce
# compte (API Hostinger, outils…), puis :
docker compose up -d sync
Réécrire l'historique git ne suffit pas (copies existantes) : seule la rotation protège.
Dashboard
Changer DASHBOARD_PASSWORD dans .env puis docker compose up -d sync.
Tests
cd sync && npm ci && npm test # fonctions SQL pures, sans base de données