Plateforme auto-hébergée de distribution d'IPAs pour SideStore. Interface d'admin FastAPI + Jinja2, stockage MySQL/MariaDB, génération dynamique de source.json.
- Backend : FastAPI + SQLAlchemy 2.0 + Uvicorn (Python 3.13)
- Base de données : MySQL ou MariaDB (externe, pas embarqué dans le conteneur). La connexion est saisie via l'UI à la première connexion.
- Stockage : binaires IPA + icônes + screenshots sur le disque hôte (
/srv/store-prod/), montés en volume dans le conteneur. - Déploiement : un conteneur Docker par VM.
Chaque VM héberge un seul environnement. Dev et prod vivent sur des machines séparées, pas côte-à-côte :
| VM | Branche Git | Mode de mise à jour |
|---|---|---|
| Dev (maison / lab) | dev |
rolling — git pull origin dev + rebuild à la demande |
| Prod (cloud / serveur public) | main |
release-based — git checkout <tag> + rebuild sur release GitHub |
Les deux VM utilisent la même configuration infra (chemins, nom de conteneur, port 8000, units systemd). La seule différence est la branche qui est clonée. Le script de management détecte automatiquement le mode (prod vs dev) en lisant la branche courante.
Tout développement sur
devuniquement. Ne jamais committer ni pusher directement surmain.
Publier une release prod :
- Merger
dev→main(PR ou fast-forward) - Créer une release GitHub avec un tag semver (ex
v1.2.0) via l'UI ough release create v1.2.0 - L'UI admin détecte la MAJ → bouton "Appliquer" → le bootstrap de la VM prod checkout le tag et rebuild
Un seul script bootstrap auto-suffisant (curl | bash) : il installe Docker, clone le repo, checkout la dernière release GitHub (HEAD détaché), configure systemd et démarre le conteneur.
# En root ou via sudo. Tout est auto : Docker, clone, systemd, conteneur.
curl -sSL https://raw.githubusercontent.com/MattTen/sideserver_website/refs/heads/main/deploy/bootstrap.sh | sudo bashLa VM démarre toujours en env prod. Pour basculer en dev après coup (ou revenir en prod), on utilise le script de management :
website-management switch-dev # passe sur la branche dev (rolling, pas de release)
website-management switch-prod # revient sur la derniere release (= update prod)| Variable | Défaut | Rôle |
|---|---|---|
BASE_URL |
(vide) | URL publique forcée. Si absent, l'app dérive l'URL depuis les headers HTTP (X-Forwarded-* via --proxy-headers) — c'est recommandé : changer d'IP ou ajouter un domaine ne nécessite pas de re-bootstrap. |
GITHUB_USER |
MattTen |
Utilisateur Git pour l'auth du clone. |
GITHUB_TOKEN |
(vide) | PAT GitHub seulement si le repo est privé. Le repo actuel étant public, laisser vide. |
HOST_PORT |
8000 |
Port HTTP hôte. |
Exemple avec URL forcée :
curl -sSL https://raw.githubusercontent.com/MattTen/sideserver_website/refs/heads/main/deploy/bootstrap.sh \
| sudo BASE_URL=https://store.mon-domaine.com bash- Ouvrir
http://<IP_VM>:8000/→ redirection automatique vers/setup/databasepour saisir la connexion MySQL/MariaDB (host/port/user/password/database) - Puis
/setuppour créer le compte admin
Note SSH : le bootstrap ajoute le user applicatif au groupe
docker, mais les sessions SSH ouvertes avant le bootstrap ne voient pas le nouveau groupe. Reconnecte-toi (exit+ssh) ou lancenewgrp dockerpour utiliserdockersans sudo.
Le bootstrap détecte uname -m et installe qemu-user-static + binfmt-support sur les hôtes ARM64 (Oracle Ampere, Raspberry Pi…). Le builder SCInsta (toolchain iOS L1ghtmann + ipapatch, distribués uniquement en amd64) tourne alors en émulation x86_64 transparente. Sur amd64 le bootstrap saute cet install (qemu inutile). Aucune intervention manuelle.
Le conteneur écoute sur HOST_PORT (défaut 8000). Trois options pour le rendre accessible en HTTPS :
cloudflared se connecte en sortant vers Cloudflare (pas de port entrant à ouvrir, IP serveur masquée, pas de limite d'upload contrairement au proxy DNS classique limité à 100 Mo sur le plan Free). Setup :
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-<arch>.deb -o cf.deb && sudo dpkg -i cf.debcloudflared tunnel login→ autorise ton domainecloudflared tunnel create ipastore- Config
/etc/cloudflared/config.yml:tunnel: <UUID> credentials-file: /etc/cloudflared/<UUID>.json ingress: - hostname: ipastore.<ton-domaine> service: http://127.0.0.1:8000 # ou via HAProxy:80 si tu en utilises un originRequest: httpHostHeader: ipastore.<ton-domaine> - service: http_status:404
cloudflared tunnel route dns ipastore ipastore.<ton-domaine>→ crée le CNAME automatiquementsudo cloudflared service install && sudo systemctl enable --now cloudflared
Oracle Cloud / autres clouds avec firewall strict : ouvrir l'egress UDP/TCP vers
7844(et idéalement all protocols en sortie). cloudflared négocie en QUIC (UDP) avec fallback HTTP/2 (TCP). Sans ouverture, le tunnel échoue silencieusement.
Bind le proxy sur 127.0.0.1:80, route le vhost vers 127.0.0.1:8000 (le conteneur). Healthcheck applicatif via GET /healthz. Si tu mets cloudflared devant ton reverse proxy, garde le bind interne (127.0.0.1) — pas d'exposition publique nécessaire.
Ouvre HOST_PORT sur ton firewall + cert SSL géré par un reverse proxy local. Expose ton IP publique au scraping.
Dans SideStore (iOS) → Sources → Ajouter :
https://ipastore.<ton-domaine>/source.json
Le feed source.json utilise l'URL publique de la requête (ou IPASTORE_BASE_URL si défini) pour générer les downloadURL, iconURL etc. — SideStore effectuant des requêtes HTTP indépendantes depuis l'app iOS, des chemins relatifs ne suffisent pas.
Deux options dans l'UI admin :
- Drag-and-drop dans la dropzone du dashboard → upload XHR direct depuis le navigateur. Sujet à la limite Cloudflare 100 Mo si proxy DNS orange (passe sans souci via Tunnel).
- Champ URL (à côté de la dropzone) → le serveur télécharge depuis l'URL fournie (litterbox.catbox.moe, 0x0.st, n'importe quel CDN HTTPS direct). Évite la limite CF côté upload client. Polling temps réel des Mo reçus.
Idem dans l'onglet SCInsta pour l'IPA Instagram source — utile parce que les IPAs Instagram font 250-300 Mo (au-dessus de la limite CF Free).
Par défaut, source.json est public : n'importe qui connaissant l'URL du serveur peut récupérer la liste des IPAs (et les télécharger). Pour limiter l'accès aux personnes à qui vous donnez le lien (et bloquer les bots de scraping), activez la protection par jeton dans Réglages → Sécurité → "Protéger l'accès au dépôt d'IPA".
Une fois activé :
- Un jeton aléatoire de 256 caractères alphanumériques est généré
GET /source.jsonetGET /qr.svgexigent?t=<jeton>. Sans ce jeton, le serveur répond404(volontairement opaque pour les bots de scraping)- L'URL du dépôt affichée sur le dashboard et le QR code intègrent automatiquement le jeton
- Le bouton Régénérer crée un nouveau jeton (avec confirmation). Les anciens liens cessent immédiatement de fonctionner
URL à coller dans SideStore quand la protection est activée :
http://<IP_VM>/source.json?t=<jeton-256-caracteres>
C'est un secret long plutôt qu'une authentification standard car SideStore ne sait pas envoyer de header custom : seul un GET avec query string est utilisable côté client iOS.
Le script tools/website-management.sh est exposé via le symlink /usr/local/bin/website-management.
website-management # menu interactif
website-management --help # aide complète
# Conteneur
website-management start / stop / restart / logs / status
# Code
website-management update # prod : release-based / dev : rolling (auto-detect branche)
website-management check # machine-readable : current / latest / update_available
website-management pull # force pull HEAD de la branche courante (dev uniquement)
website-management self-update # pull ce script (git pull de /opt/ipaserver)
# Bascule d'environnement
website-management switch-dev # passe la VM en env dev (branche dev, rolling)
website-management switch-prod # revient en env prod (checkout derniere release)
# Admin
website-management reset-users # supprime tous les admins + en crée un nouveau (prompt login/mdp)- Merger
dev→main gh release create v1.2.0- Sur la VM prod, l'UI
/settingsdétecte la MAJ toutes les 6 h et affiche le bouton "Appliquer" - Le bouton écrit un flag-file →
ipastore-update@prod.path(systemd) →website-management prod-update
Le conteneur lit sa config depuis /etc/ipastore/prod.env, écrit par le bootstrap. Voir .env.example pour le détail.
| Variable | Rôle |
|---|---|
IPASTORE_BASE_URL |
(optionnel) URL publique forcée. Si absent, l'app dérive depuis les headers HTTP. |
IPASTORE_STORE_DIR |
Racine des binaires dans le conteneur (toujours /srv/store, monté depuis /srv/store-prod de l'hôte). |
IPASTORE_SECRET_FILE |
Chemin vers la clé de signature des cookies. |
IPASTORE_ENV |
prod (toujours prod dans le modèle mono-env, même sur la VM dev). |
IPASTORE_GITHUB_REPO |
Repo GitHub pour le check de releases (MattTen/sideserver_website). |
La connexion BDD (host/user/mdp/nom de base) n'est pas fournie en env var : elle est saisie via l'UI au premier démarrage (/setup/database) puis persistée dans /etc/ipastore/db.json (mode 600).
Voir documentation/server.md pour les détails complets.
app/ # code Python (FastAPI)
templates/ # Jinja2
static/ # CSS + JS
patch/ # scripts de patch IPA (fix_ipa.py, etc.)
deploy/ # bootstrap.sh unique (curl | bash auto-suffisant, clone + release)
tools/ # website-management.sh + scinsta-builder/
documentation/ # doc serveur + credentials (exclu du déploiement serveur via sparse-checkout)
Dockerfile
docker-compose.yml