flfeders d7382e7319
All checks were successful
Build and Release FEDEO / verify (push) Successful in 56s
Build and Release FEDEO / build-and-release (push) Successful in 5m58s
feat(documents): prepare inbox suggestions and add entity assignments
2026-10-02 16:09:04 +02:00
2026-03-16 20:46:26 +01:00
2026-03-16 20:46:26 +01:00

FEDEO Hosting Guide

Diese Anleitung beschreibt ein produktionsnahes Self-Hosting von FEDEO mit Docker Compose, Traefik, PostgreSQL und optionalem S3-kompatiblem Objektspeicher via MinIO.

Versionen und Releases

Ein Push auf main veröffentlicht nach erfolgreichen Prüfungen und Builds automatisch die nächste FEDEO-Version. Die Versionsnummer und Release Notes entstehen aus Conventional Commits (fix:, feat:, Breaking Changes). dev bleibt der Entwicklungskanal.

Für feste Selfhost-Versionen FEDEO_VERSION in .env auf eine veröffentlichte Version wie 3.0.0 setzen und die Betriebsdateien aus demselben Release verwenden. Ohne gesetzte Version bleibt der bisherige dev-Kanal aktiv. Die folgenden raw/branch/dev-Beispiele beschreiben diesen Entwicklungskanal.

Release-Freigabe, Einrichtung der CI-Tokens, versionierte Installation, Updates und Wiederaufnahme sind unter Versionierung und Releases beschrieben. Die laufende Backend-Version liefert /backend/version; im Frontend steht sie unten in der Seitenleiste.

Architektur

Der Stack besteht aus:

  • frontend: Nuxt-Frontend auf Port 3000
  • backend: Node/Fastify-API auf Port 3100
  • db: PostgreSQL
  • traefik: Reverse Proxy mit automatischen Let's-Encrypt-Zertifikaten
  • optional minio: S3-kompatibler Objektspeicher fur Dateiuploads

Die Konfiguration erfolgt uber Umgebungsvariablen beziehungsweise eine .env-Datei im Deploy-Verzeichnis.

Voraussetzungen

Vor dem Deployment sollten folgende Punkte erfullt sein:

  • Ein Linux-Server oder VPS mit offentlichen Ports 80 und 443
  • Docker Engine inkl. Compose Plugin
  • Eine Domain, die auf den Server zeigt, z. B. app.example.com
  • Optional: SMTP-Zugang fur E-Mails
  • Optional: S3-Bucket oder MinIO fur Dateispeicher

Empfohlen:

  • mindestens 2 vCPU
  • mindestens 4 GB RAM
  • SSD-Speicher fur PostgreSQL und Dateiuploads

DNS und Netzwerk

Lege mindestens einen A- oder AAAA-Record an:

  • app.example.com -> <SERVER-IP>

Traefik terminiert TLS direkt im Compose-Stack. Es ist kein zusatzlicher Reverse Proxy davor erforderlich.

Benotigte Backend-Umgebungsvariablen

Das Backend erwartet mindestens diese Umgebungsvariablen:

  • COOKIE_SECRET
  • JWT_SECRET
  • PORT
  • HOST
  • DATABASE_URL
  • S3_BUCKET
  • ENCRYPTION_KEY
  • MAILER_SMTP_HOST
  • MAILER_SMTP_PORT
  • MAILER_SMTP_SSL
  • MAILER_SMTP_USER
  • MAILER_SMTP_PASS
  • MAILER_FROM
  • S3_ENDPOINT
  • S3_REGION
  • S3_ACCESS_KEY
  • S3_SECRET_KEY
  • M2M_API_KEY
  • API_BASE_URL
  • GOCARDLESS_BASE_URL
  • GOCARDLESS_SECRET_ID
  • GOCARDLESS_SECRET_KEY
  • DOKUBOX_IMAP_HOST
  • DOKUBOX_IMAP_PORT
  • DOKUBOX_IMAP_SECURE
  • DOKUBOX_IMAP_USER
  • DOKUBOX_IMAP_PASSWORD
  • OPENAI_API_KEY
  • STIRLING_API_KEY

Minimal wichtige Werte fur den ersten Start:

  • HOST=0.0.0.0
  • PORT=3100
  • DATABASE_URL=postgres://fedeo:<starkes-passwort>@db:5432/fedeo
  • API_BASE_URL=https://app.example.com/backend

Wenn du MinIO verwendest, setze zusatzlich:

  • S3_ENDPOINT=http://minio:9000
  • S3_REGION=eu-central-1
  • S3_ACCESS_KEY=<MINIO_ROOT_USER>
  • S3_SECRET_KEY=<MINIO_ROOT_PASSWORD>
  • S3_BUCKET=fedeo

Deploy-Struktur

Deploye den Stack in einem eigenen Betriebsverzeichnis. Der Selfhost-Installer lädt dafür nur die benötigten Betriebsdateien und klont nicht das komplette Repository.

Beispiel für die manuelle Vorbereitung:

mkdir -p /opt/fedeo/scripts
curl -fsSL https://git.federspiel.tech/flfeders/FEDEO/raw/branch/dev/docker-compose.selfhost.yml -o /opt/fedeo/docker-compose.yml
curl -fsSL https://git.federspiel.tech/flfeders/FEDEO/raw/branch/dev/.env.example -o /opt/fedeo/.env.example
curl -fsSL https://git.federspiel.tech/flfeders/FEDEO/raw/branch/dev/scripts/selfhost-setup.sh -o /opt/fedeo/scripts/selfhost-setup.sh
chmod +x /opt/fedeo/scripts/selfhost-setup.sh

Die Verzeichnisstruktur sollte dann mindestens so aussehen:

/opt/fedeo/
  docker-compose.yml
  .env
  scripts/
  traefik/
    letsencrypt/
    logs/
  postgres/
  minio/

Danach:

mkdir -p /opt/fedeo/traefik/letsencrypt
mkdir -p /opt/fedeo/traefik/logs
mkdir -p /opt/fedeo/postgres
mkdir -p /opt/fedeo/minio
touch /opt/fedeo/traefik/letsencrypt/acme.json
chmod 600 /opt/fedeo/traefik/letsencrypt/acme.json

Als Startpunkt kannst du die Beispielumgebung kopieren:

cp .env.example .env

Ersetze anschließend alle Platzhalter und passe mindestens DOMAIN, CONTACT_EMAIL, Datenbank-, Secret-, SMTP- und S3-Werte an. Seafile ist kein Teil des Standard-Stacks; wenn FEDEO später Seafile als File-Backend nutzen soll, zeigst du die Seafile-Variablen auf einen externen Seafile-Dienst.

Alternativ kannst du die Konfiguration geführt erzeugen lassen:

bash scripts/selfhost-setup.sh

Auf einem frischen Server kannst du die Betriebsdateien und die Konfiguration direkt per One-Liner vorbereiten:

curl -fsSL https://git.federspiel.tech/flfeders/FEDEO/raw/branch/dev/scripts/selfhost-install.sh | bash

Der One-Liner von der Webseite nutzt den schnellen Standardpfad und startet den Stack direkt:

curl -fsSL https://git.federspiel.tech/flfeders/FEDEO/raw/branch/dev/scripts/selfhost-install.sh | bash -s -- --simple --start

Der Installer prüft Basispakete, installiert Docker auf Wunsch über das offizielle Docker-Installationsscript, lädt nur die Selfhost-Dateien nach /opt/fedeo und startet anschließend den geführten Setup-Assistenten.

Für den schnellen Standardpfad:

bash scripts/selfhost-setup.sh --simple

Für mehr Rückfragen zu SMTP, API-Schlüsseln und optionalen Diensten:

bash scripts/selfhost-setup.sh --advanced

Der Assistent erklärt zuerst die Selfhost-Verzeichnisstruktur, schreibt anschließend .env, legt persistente Verzeichnisse inklusive traefik/letsencrypt/acme.json an und kann den Stack optional direkt starten.

Beispiel .env

Diese Datei liegt neben der docker-compose.yml:

DOMAIN=app.example.com
CONTACT_EMAIL=admin@deine-domain.de

DB_NAME=fedeo
DB_USER=fedeo
DB_PASSWORD=change-this-db-password
DATABASE_URL=postgres://fedeo:change-this-db-password@db:5432/fedeo

MINIO_ROOT_USER=fedeo-minio
MINIO_ROOT_PASSWORD=change-this-minio-password
MINIO_BUCKET=fedeo

HOST=0.0.0.0
PORT=3100
COOKIE_SECRET=change-this-cookie-secret
JWT_SECRET=change-this-jwt-secret
ENCRYPTION_KEY=change-this-encryption-key

MAILER_SMTP_HOST=smtp.example.com
MAILER_SMTP_PORT=587
MAILER_SMTP_SSL=false
MAILER_SMTP_USER=mailer@example.com
MAILER_SMTP_PASS=change-this-mail-password
MAILER_FROM=FEDEO <no-reply@example.com>

S3_ENDPOINT=http://minio:9000
S3_REGION=eu-central-1
S3_ACCESS_KEY=fedeo-minio
S3_SECRET_KEY=change-this-minio-password
S3_BUCKET=fedeo

FEDEO_FILE_BACKEND=s3
SEAFILE_BASE_URL=https://files.example.com
SEAFILE_INTERNAL_URL=https://files.example.com
SEAFILE_ADMIN_EMAIL=admin@example.com
SEAFILE_ADMIN_PASSWORD=change-this-seafile-admin-password

M2M_API_KEY=change-this-m2m-key
API_BASE_URL=https://app.example.com/backend
GOCARDLESS_BASE_URL=https://api.gocardless.com
GOCARDLESS_SECRET_ID=replace-this
GOCARDLESS_SECRET_KEY=replace-this

DOKUBOX_IMAP_HOST=imap.example.com
DOKUBOX_IMAP_PORT=993
DOKUBOX_IMAP_SECURE=true
DOKUBOX_IMAP_USER=dokubox@example.com
DOKUBOX_IMAP_PASSWORD=change-this-imap-password

OPENAI_API_KEY=replace-this
STIRLING_API_KEY=replace-this

# Optional: zentrale, durch den FEDEO-Betreiber bereitgestellte Dienste
FEDEO_CENTRAL_SERVICES_ENABLED=false
PUSH_SERVER_URL=https://services.fedeo.de
PUSH_SERVER_INSTANCE_ID=
PUSH_SERVER_SECRET=

NUXT_PUBLIC_PDF_LICENSE=replace-with-your-pdf-license

FEDEO_BOOTSTRAP_ADMIN_EMAIL=admin@example.com
FEDEO_BOOTSTRAP_ADMIN_PASSWORD=change-this-admin-password
FEDEO_BOOTSTRAP_ADMIN_FIRST_NAME=Admin
FEDEO_BOOTSTRAP_ADMIN_LAST_NAME=Benutzer
FEDEO_BOOTSTRAP_TENANT_NAME=Mein Unternehmen
FEDEO_BOOTSTRAP_TENANT_SHORT=MEIN

Die FEDEO_BOOTSTRAP_*-Werte sind für den ersten Start gedacht. Wenn FEDEO_BOOTSTRAP_ADMIN_EMAIL und FEDEO_BOOTSTRAP_ADMIN_PASSWORD gesetzt sind, legt das Backend idempotent einen Admin-Benutzer, einen ersten Mandanten, eine Administrator-Rolle und grundlegende Stammdaten an. Nach erfolgreichem Erstzugriff solltest du das Bootstrap-Passwort aus der .env entfernen oder ändern.

Docker Compose mit optionalem S3

Die Selfhost-Konfiguration wird im Betriebsverzeichnis als docker-compose.yml abgelegt. Sie startet MinIO standardmäßig mit. Wenn du stattdessen AWS S3, Hetzner Object Storage, Backblaze B2 S3 oder einen anderen externen S3-Dienst nutzen willst, kannst du die Services minio und createbuckets entfernen und nur die entsprechenden S3-Umgebungsvariablen auf den externen Anbieter zeigen lassen.

Seafile wird bewusst nicht im Standard-Compose-Stack gestartet. FEDEO kann später gegen einen extern betriebenen Seafile-Dienst sprechen; dafür bleiben SEAFILE_BASE_URL, SEAFILE_INTERNAL_URL, SEAFILE_ADMIN_EMAIL und SEAFILE_ADMIN_PASSWORD als generische Anbindungswerte vorgesehen. FEDEO_FILE_BACKEND=s3 bleibt der Standard, bis die Backend-Integration für Seafile vollständig umgesetzt ist.

Das Backend führt beim Containerstart standardmäßig npm run migrate aus. Setze FEDEO_RUN_MIGRATIONS=false, wenn du Migrationen bewusst manuell ausführen möchtest.

services:
  traefik:
    image: traefik:v2.11
    container_name: fedeo-traefik
    restart: unless-stopped
    command:
      - --api.insecure=false
      - --api.dashboard=false
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --entrypoints.web.http.redirections.entrypoint.to=websecure
      - --entrypoints.web.http.redirections.entrypoint.scheme=https
      - --certificatesresolvers.letsencrypt.acme.tlschallenge=true
      - --certificatesresolvers.letsencrypt.acme.email=${CONTACT_EMAIL}
      - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
      - --accesslog=true
      - --accesslog.filepath=/logs/access.log
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./traefik/letsencrypt:/letsencrypt
      - ./traefik/logs:/logs
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - web

  db:
    image: postgres:16
    container_name: fedeo-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - ./postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks:
      - internal

  minio:
    image: minio/minio:latest
    container_name: fedeo-minio
    restart: unless-stopped
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    volumes:
      - ./minio:/data
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks:
      - internal

  createbuckets:
    image: minio/mc:latest
    container_name: fedeo-minio-init
    depends_on:
      minio:
        condition: service_healthy
    entrypoint: >
      /bin/sh -c "
      mc alias set local http://minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD};
      mc mb --ignore-existing local/${MINIO_BUCKET};
      mc anonymous set private local/${MINIO_BUCKET};
      exit 0;
      "
    restart: "no"
    networks:
      - internal

  backend:
    image: git.federspiel.tech/flfeders/fedeo/backend:dev
    container_name: fedeo-backend
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
      minio:
        condition: service_healthy
      createbuckets:
        condition: service_completed_successfully
    environment:
      NODE_ENV: production
      HOST: ${HOST}
      PORT: ${PORT}
      COOKIE_SECRET: ${COOKIE_SECRET}
      JWT_SECRET: ${JWT_SECRET}
      ENCRYPTION_KEY: ${ENCRYPTION_KEY}
      DATABASE_URL: ${DATABASE_URL}
      MAILER_SMTP_HOST: ${MAILER_SMTP_HOST}
      MAILER_SMTP_PORT: ${MAILER_SMTP_PORT}
      MAILER_SMTP_SSL: ${MAILER_SMTP_SSL}
      MAILER_SMTP_USER: ${MAILER_SMTP_USER}
      MAILER_SMTP_PASS: ${MAILER_SMTP_PASS}
      MAILER_FROM: ${MAILER_FROM}
      S3_ENDPOINT: ${S3_ENDPOINT}
      S3_REGION: ${S3_REGION}
      S3_ACCESS_KEY: ${S3_ACCESS_KEY}
      S3_SECRET_KEY: ${S3_SECRET_KEY}
      S3_BUCKET: ${S3_BUCKET}
      M2M_API_KEY: ${M2M_API_KEY}
      API_BASE_URL: ${API_BASE_URL}
      GOCARDLESS_BASE_URL: ${GOCARDLESS_BASE_URL}
      GOCARDLESS_SECRET_ID: ${GOCARDLESS_SECRET_ID}
      GOCARDLESS_SECRET_KEY: ${GOCARDLESS_SECRET_KEY}
      DOKUBOX_IMAP_HOST: ${DOKUBOX_IMAP_HOST}
      DOKUBOX_IMAP_PORT: ${DOKUBOX_IMAP_PORT}
      DOKUBOX_IMAP_SECURE: ${DOKUBOX_IMAP_SECURE}
      DOKUBOX_IMAP_USER: ${DOKUBOX_IMAP_USER}
      DOKUBOX_IMAP_PASSWORD: ${DOKUBOX_IMAP_PASSWORD}
      OPENAI_API_KEY: ${OPENAI_API_KEY}
      STIRLING_API_KEY: ${STIRLING_API_KEY}
    labels:
      - traefik.enable=true
      - traefik.http.routers.fedeo-backend.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)
      - traefik.http.routers.fedeo-backend.entrypoints=websecure
      - traefik.http.routers.fedeo-backend.tls.certresolver=letsencrypt
      - traefik.http.middlewares.fedeo-backend-strip.stripprefix.prefixes=/backend
      - traefik.http.routers.fedeo-backend.middlewares=fedeo-backend-strip
      - traefik.http.services.fedeo-backend.loadbalancer.server.port=3100
      - traefik.docker.network=fedeo_web
    networks:
      - web
      - internal

  frontend:
    image: git.federspiel.tech/flfeders/fedeo/frontend:dev
    container_name: fedeo-frontend
    restart: unless-stopped
    depends_on:
      - backend
    environment:
      NODE_ENV: production
      NUXT_PUBLIC_API_BASE: https://${DOMAIN}/backend
      NUXT_PUBLIC_PDF_LICENSE: ${NUXT_PUBLIC_PDF_LICENSE}
    labels:
      - traefik.enable=true
      - traefik.http.routers.fedeo-frontend.rule=Host(`${DOMAIN}`)
      - traefik.http.routers.fedeo-frontend.entrypoints=websecure
      - traefik.http.routers.fedeo-frontend.tls.certresolver=letsencrypt
      - traefik.http.services.fedeo-frontend.loadbalancer.server.port=3000
      - traefik.docker.network=fedeo_web
    networks:
      - web

networks:
  web:
    name: fedeo_web
    driver: bridge
  internal:
    name: fedeo_internal
    driver: bridge

Externe S3-Provider statt MinIO

Wenn du keinen lokalen MinIO-Container betreiben willst:

  1. Entferne die Services minio und createbuckets aus der Compose-Datei.
  2. Entferne im Backend depends_on fur minio und createbuckets.
  3. Trage in .env die Zugangsdaten des externen S3-Dienstes ein.

Beispiel fur die relevanten Werte:

S3_ENDPOINT=https://s3.eu-central-1.amazonaws.com
S3_REGION=eu-central-1
S3_ACCESS_KEY=...
S3_SECRET_KEY=...
S3_BUCKET=fedeo

Hinweis: Das Backend nutzt forcePathStyle: true. Das funktioniert sauber mit MinIO und vielen S3-kompatiblen Providern. Bei reinem AWS S3 kann je nach Endpoint-Setup ein abweichendes Verhalten sinnvoll sein. Falls du AWS S3 einsetzen willst, sollte die S3-Initialisierung im Backend gegen den konkreten Zielprovider getestet werden.

Start des Stacks

Im Deploy-Verzeichnis:

docker compose --env-file /opt/fedeo/.env -f /opt/fedeo/docker-compose.yml up -d

Danach Status prufen:

docker compose --env-file /opt/fedeo/.env -f /opt/fedeo/docker-compose.yml ps
docker compose --env-file /opt/fedeo/.env -f /opt/fedeo/docker-compose.yml logs -f traefik
docker compose --env-file /opt/fedeo/.env -f /opt/fedeo/docker-compose.yml logs -f backend

Wenn du Migrationen manuell ausführen möchtest:

docker compose --env-file /opt/fedeo/.env -f /opt/fedeo/docker-compose.yml run --rm backend npm run migrate

Funktionsprufung

Nach dem ersten Start sollten mindestens diese Checks erfolgreich sein:

curl -I https://app.example.com
curl https://app.example.com/backend/health

Erwartung:

  • Frontend liefert 200 oder 302
  • Backend liefert JSON wie {"status":"ok"}

Wenn der Bootstrap aktiviert ist, kannst du dich danach mit FEDEO_BOOTSTRAP_ADMIN_EMAIL und FEDEO_BOOTSTRAP_ADMIN_PASSWORD anmelden. Die Mandantensperre wird über locked gesteuert; hasActiveLicense wird nicht mehr für den Selfhost-Zugriff ausgewertet.

Updates

Vollständige Release-Updates einschließlich Compose-Datei werden mit dem Host-Skript vorbereitet und ausgeführt:

sudo python3 /opt/fedeo/scripts/selfhost-update.py check
sudo python3 /opt/fedeo/scripts/selfhost-update.py apply --version 3.1.0
sudo python3 /opt/fedeo/scripts/selfhost-update.py status

3.1.0 durch den gewünschten veröffentlichten Release ersetzen. Das Skript benötigt Python ab 3.10 und Docker Compose ab 2.24. Es sichert die lokale Datenbank, MinIO, Konfiguration und Zertifikate vor dem Austausch, erhält .env und prüft die gestarteten Versionen. Während Backup und Aktualisierung gibt es eine Wartungsunterbrechung.

Beim ersten Update die erzeugte Compose-Differenz prüfen, eigene Anpassungen in eine Override-Datei übertragen und den Austausch mit --accept-compose-replacement bestätigen. Bestehende Installationen müssen das Update-Skript zunächst aus einem passenden Release übernehmen.

Unter Administration → Systemstatus → FEDEO-Updates können Systemadministratoren neue Releases prüfen und nach Bestätigung installieren. Dazu führt fedeo-updater.service die Aufträge unabhängig von Browser und Backend-Neustarts aus. Nach Bereitstellung der zugehörigen Dienstdateien installiert sudo bash /opt/fedeo/scripts/selfhost-update-service-install.sh den systemd-Dienst. Die aktuelle Selfhost-Compose-Datei bindet dessen lokales Socket-Verzeichnis im Backend ein. Bestehende Installationen benötigen einmalig die aktuellen Hostskripte und ein CLI-Update auf einen Release mit der Oberfläche. Einrichtung und Schritte stehen in der folgenden Anleitung.

Voraussetzungen, unterstützte Konfigurationen und Wiederherstellung stehen in Vollständige Selfhost-Updates. Für ältere Releases steht der manuelle Ablauf in Versionierung und Releases.

Backup-Empfehlung

Regelmassig sichern:

  • PostgreSQL mit einem konsistenten Datenbankbackup, beispielsweise pg_dump
  • ./minio falls MinIO lokal genutzt wird
  • ./traefik/letsencrypt/acme.json
  • deine .env
  • deine dokumentierten Secret-Werte aus der .env oder deinem Secret-Management

Bekannte Betriebsbesonderheiten

  • Das Backend startet nur sauber, wenn alle Pflichtvariablen gesetzt sind.
  • Ohne korrekt gesetzte S3-Secrets funktionieren Dateiuploads und dateibasierte Funktionen nicht.
  • Fur die Frontend-PDF-Funktion wird eine gueltige NUXT_PUBLIC_PDF_LICENSE benotigt.
  • PostgreSQL ist im Projekt vorgesehen; andere SQL-Datenbanken sind in dieser Compose-Datei nicht berucksichtigt.

Optional: Nur mit bestehender externer Infrastruktur

Wenn bereits vorhanden:

  • externer Reverse Proxy
  • externer PostgreSQL-Server
  • externer S3-Speicher
  • externe Zertifikatsverwaltung

dann konnen traefik, db und minio aus dem Stack entfernt werden. In diesem Fall mussen die zugehorigen Hostnamen und Zugangsdaten in der .env beziehungsweise im Frontend-Environment auf die externe Infrastruktur zeigen.

Description
No description provided
https://fedeo.de
Readme 44 MiB
FEDEO 3.17.0 Latest
2026-10-29 17:28:20 +00:00
Languages
TypeScript 53.8%
Vue 31.1%
JavaScript 8.1%
Python 5.3%
Shell 0.8%
Other 0.9%