❯ apps · Running

Éditeur visuel no-code avec Silex (GrapesJS) No-Code Visual Editor with Silex (GrapesJS)

Déploiement de Silex sur le cluster k3s — l'image upstream est en amd64 uniquement, j'ai donc dû builder ma propre image ARM64 via GitHub Actions, débusquer un défaut qui bind sur localhost à l'intérieur du conteneur, et basculer un package GHCR en public. Une petite histoire de trois pièges enchaînés. Deploying Silex on the k3s cluster — the upstream image is amd64-only, so I had to build my own ARM64 image with GitHub Actions, fight a default that binds to localhost inside the container, and toggle a GHCR package public. A short story of three traps in a row.

· 9 min de lecture 9 min read ·
k3s-clustersilexgrapesjsnocodek8sselfhost
🇬🇧 Read this article in English

Pourquoi un éditeur visuel auto-hébergé

Le besoin initial était simple : pouvoir importer des sites HTML/CSS/JS existants et les retravailler en drag & drop, sans dépendre d’un Webflow ou d’un Wix. Trois candidats sérieux dans l’écosystème open source :

  • GrapesJS — bibliothèque JS d’édition visuelle, embarquable. Mais ce n’est pas un serveur autonome ; il faut écrire le wrapper soi-même.
  • Webstudio — alternative moderne à Webflow, plus orientée greenfield. Import HTML existant limité.
  • Silex — projet officiel construit sur GrapesJS, distribué sous forme de paquet npm prêt à l’emploi avec backend de persistance, gestion de projets et import HTML natif.

Silex coche toutes les cases : self-hostable, basé sur GrapesJS, AGPL, et le paquet @silexlabs/silex expose un binaire CLI directement utilisable.

Architecture cible

silex.olig-homelab-2025.win
        │  (Cloudflare Access policy *.olig-homelab-2025.win)
        ▼
Cloudflare Tunnel (cloudflared, mode token)
        │
        ▼
Traefik Ingress  ──▶  Service silex:6805  ──▶  Deployment silex (1 replica, Recreate)
                                                      │
                                                      ▼
                                         PVC silex-data (10Gi, Longhorn)
                                         /silex-data/projects  (sources éditables)
                                         /silex-data/hosting   (sites publiés)

Single replica + strategy: Recreate parce que Silex écrit ses projets sur le système de fichiers local, donc PVC ReadWriteOnce et pas de rolling update. Même pattern que code-server.

Les manifests — pattern ArgoCD habituel

Six fichiers, copiés sur le pattern existant (code-server, n8n, homepage) :

silex/
  00-namespace.yaml
  pvc-data.yaml         # 10Gi, standard (Longhorn)
  deployment.yaml
  service.yaml          # ClusterIP :6805
  ingress.yaml          # traefik, host silex.olig-homelab-2025.win
argocd/apps/silex.yaml  # Application CRD pointant sur silex/

L’Application racine d’ArgoCD scanne argocd/apps/ automatiquement, donc un simple git push sur actual-state déclenche le déploiement. Pas d’action manuelle côté cluster — c’est le confort d’un GitOps bien posé.

Trap n°1 : exec format error

Premier déploiement, pod en CrashLoopBackOff. Logs :

exec /usr/local/bin/docker-entrypoint.sh: exec format error

Diagnostic instantané : l’image ne correspond pas à l’architecture du nœud. Vérification :

$ kubectl get nodes -o custom-columns=NAME:.metadata.name,ARCH:.status.nodeInfo.architecture
NAME       ARCH
master-1   arm64
master-2   arm64
master-3   arm64
worker-1   arm64
worker-2   arm64
worker-3   arm64

Cluster 100 % ARM64 (six Raspberry Pi). Et côté Docker Hub, une rapide inspection des manifests silexlabs/silex :

$ curl -s "https://hub.docker.com/v2/repositories/silexlabs/silex/tags/?page_size=20" \
  | jq '.results[] | {name, archs: [.images[].architecture]}'
{"name":"latest", "archs":["amd64"]}
{"name":"canary", "archs":["amd64"]}
{"name":"3.6.4",  "archs":["amd64"]}
...

Tous les tags publiés sont amd64-only. Pas de build ARM64 sur le registry officiel. Première leçon retenue : avant de référencer une image upstream, vérifier ses architectures publiées — un seul appel docker manifest inspect aurait évité le faux départ.

Trap n°2 : pas de Docker buildx local, GHA à la rescousse

Solution évidente : builder soi-même une image multi-arch. Sauf que Docker Desktop n’était pas démarré sur la machine locale, et le token gh n’avait pas le scope write:packages pour pousser sur GHCR.

Plutôt que de tout configurer en local, j’ai suivi le pattern déjà utilisé pour le blog : GitHub Actions construit et publie. Le runner GHA a buildx + QEMU prêts à l’emploi, et le GITHUB_TOKEN natif a write:packages sans configuration supplémentaire.

Le Dockerfile est minimal — Silex est distribué comme paquet npm, donc on s’appuie sur node:18-bullseye-slim qui est nativement multi-arch :

FROM node:18-bullseye-slim
RUN npm install -g @silexlabs/[email protected] \
 && npm cache clean --force
WORKDIR /silex
EXPOSE 6805
CMD ["silex"]

Le workflow est un calque de celui du blog :

name: Build and push silex image
on:
  workflow_dispatch:
  push:
    branches: [actual-state]
    paths:
      - 'silex/Dockerfile'
      - '.github/workflows/silex.yml'
jobs:
  build-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-qemu-action@v3
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: ./silex
          platforms: linux/arm64
          push: true
          tags: |
            ghcr.io/yellowbeanie/silex:latest
            ghcr.io/yellowbeanie/silex:${{ github.sha }}

Build ARM64 émulé via QEMU sur runner ubuntu-latest : 3 min 19 s pour cette image, pleinement acceptable.

Trap n°3 : GHCR met les nouveaux paquets en privé

Image construite, deployment mis à jour vers ghcr.io/yellowbeanie/silex:latest, ArgoCD synchronise — et le pod retombe en erreur :

Failed to pull image "ghcr.io/yellowbeanie/silex:latest":
failed to authorize: failed to fetch anonymous token:
401 Unauthorized

Contrairement à Docker Hub, GHCR met tous les nouveaux paquets en private par défaut, même quand on les pousse depuis un workflow d’un repo public. Sans imagePullSecret configuré, le kubelet ne peut pas les pull.

Deux options :

  1. Créer un imagePullSecret dans le namespace avec un PAT en lecture, scellé via Sealed Secrets — propre mais 15 minutes de plomberie.
  2. Basculer le paquet en Public via les settings de l’utilisateur GitHub — un clic.

Ce paquet contient exactement ce que n’importe qui obtiendrait avec npm install -g @silexlabs/[email protected] sur une base Node officielle. Aucun secret, aucun code propriétaire, aucune config interne. Donc visibilité publique, sans hésitation. Le précédent était déjà là : homelab-blog est aussi public sur GHCR.

Toggle effectué dans Settings → Danger Zone → Change package visibility → Public, puis force d’un nouveau pull :

kubectl delete pod -n silex -l app=silex

Et là, dans les logs :

███████ ██ ██      ███████ ██   ██     ██    ██ ██████
██      ██ ██      ██       ██ ██      ██    ██      ██
███████ ██ ██      █████     ███       ██    ██  █████
     ██ ██ ██      ██       ██ ██       ██  ██       ██
███████ ██ ███████ ███████ ██   ██       ████   ██████ .0.0
> [FsHosting] Created folders assets/ and css/ in /silex-data/hosting
> [FsStorage] Created default in /silex-data/projects
Silex is ready, open http://localhost:6805 in your browser

PVC monté et writable, deux dossiers initialisés, application up.

La variable d’environnement qui pourrait piéger

En lisant le .env.default de Silex pendant l’investigation, un défaut mérite d’être souligné :

SILEX_HOST=localhost

Dans un conteneur, bind sur localhost = bind sur loopback uniquement. Le Service Kubernetes route vers l’IP du pod, pas vers 127.0.0.1. Sans surcharge, le pod démarrerait, la readiness probe HTTP passerait (kubelet probe sur 127.0.0.1 qui marche), et pourtant le Service recevrait des connexions refusées.

D’où la surcharge dans le deployment :

env:
- name: SILEX_HOST
  value: "0.0.0.0"
- name: SILEX_PORT
  value: "6805"
- name: SILEX_PROTOCOL
  value: "https"
- name: SILEX_URL
  value: "https://silex.olig-homelab-2025.win"
- name: SILEX_FORCE_HTTPS_TRUST_XFP_HEADER
  value: "true"
- name: SILEX_FS_ROOT
  value: "/silex-data/projects"
- name: SILEX_FS_HOSTING_ROOT
  value: "/silex-data/hosting"

SILEX_PROTOCOL=https + SILEX_FORCE_HTTPS_TRUST_XFP_HEADER=true permettent à Silex de faire confiance au header X-Forwarded-Proto injecté par Traefik et de générer ses redirections et cookies en HTTPS, alors que la connexion interne pod-Service-Ingress reste en HTTP.

Validation end-to-end

$ kubectl get pods -n silex
NAME                    READY   STATUS    RESTARTS   AGE
silex-785dc667b-jskmw   1/1     Running   0          81s

$ kubectl get pvc -n silex
NAME         STATUS   CAPACITY   ACCESS MODES   STORAGECLASS
silex-data   Bound    10Gi       RWO            standard

$ kubectl run -n silex curl-test --rm -it --restart=Never \
    --image=curlimages/curl:latest -- \
    curl -sS -o /dev/null -w 'http_code=%{http_code} time=%{time_total}s\n' \
    http://silex.silex.svc.cluster.local:6805/
http_code=200 time=0.017409s

Service interne joignable en 17 ms, pod healthy, PVC bound. Reste à ajouter la route publique dans Cloudflare Zero Trust (silex.olig-homelab-2025.win → http://silex.silex.svc.cluster.local:6805), ce qui est manuel sur ce homelab parce que le tunnel tourne en mode token avec configuration côté dashboard.

Leçons retenues

Trois pièges, trois habitudes à graver pour la prochaine app :

  1. Vérifier les architectures publiées avant de pull. Une seconde de docker manifest inspect silexlabs/silex:latest aurait remplacé une heure de debug.
  2. GitHub Actions est souvent la voie la plus rapide pour builder une image custom multi-arch. Pas de Docker local à débugger, pas de PAT à gérer, le GITHUB_TOKEN couvre déjà GHCR.
  3. GHCR met les nouveaux paquets en privé par défaut. Soit on bascule public quand le contenu le permet, soit on prévoit un imagePullSecret.

Et un piège bonus à garder en tête sur n’importe quelle app conteneurisée : localhost dans un container, c’est 127.0.0.1 du namespace réseau du pod. Toujours forcer le bind sur 0.0.0.0 quand l’app expose un port à un Service.

Silex tourne désormais derrière silex.olig-homelab-2025.win, prêt pour importer des sites HTML existants en drag & drop. Le pipeline GitOps tient sa promesse : une nouvelle app, six fichiers YAML et un workflow, pas une commande tapée sur le cluster.

k3s-clustersilexgrapesjsnocodek8sselfhost