Architektur


Gemeinsame Dienste#

Ein einziger Satz Dienst-Container bedient jedes Projekt auf dem Rechner.

Redis, Memcached und der Mail-Catcher laufen immer. Die Datenbankserver nicht: Jeder startet, sobald ein Projekt auf dem Rechner ihn zum ersten Mal braucht, und bleibt dann an. Ein Rechner, auf dem nur Postgres-Projekte liegen, betreibt nie MariaDB oder MongoDB.

Das ist Zeltros zentrale Designentscheidung, und sie ist ein Kompromiss, kein Gewinn ohne Preis – es lohnt sich, sie zu verstehen, bevor Sie einen Workflow darauf aufbauen.

Was Sie gewinnen. Die meisten Docker-Setups pro Projekt starten einen Datenbank-Container pro Projekt. Zwanzig laufende Projekte bedeuten zwanzig Datenbank-Container. Zeltro betreibt einen. Auf einem Entwicklerrechner ist das oft der Unterschied zwischen „Ich kann alle meine Kunden-Websites laufen lassen“ und „Ich kann drei laufen lassen“. Deshalb braucht die .env eines Projekts auch keine eigene Datenbank-Verdrahtung: Die Hostnamen unten sind in jedem Projekt gleich, für immer.

Was Sie aufgeben. Das folgt direkt aus derselben Entscheidung:

  • Eine Version pro Engine, rechnerweit. Die gemeinsamen Server laufen mit MariaDB 12, PostgreSQL 17 und MongoDB 8. Ein Projekt, das MySQL 5.7 oder PostgreSQL 15 braucht, bekommt das von ihnen nicht. Legen Sie die Version pro Projekt fest, sind Sie wieder bei einem Container pro Projekt.
  • Gemeinsamer Lebenszyklus. zeltro down <project> stoppt nur dieses Projekt, aber zeltro stop-services stoppt die Dienste für alle Projekte, nicht nur für das, an dem Sie gerade gearbeitet haben.
  • Gemeinsamer Schadensradius. Ein beschädigtes Daten-Volume, eine außer Kontrolle geratene Migration oder ein DROP DATABASE trifft einen Server, den auch alles andere nutzt. Projektdatenbanken sind über ihren Namen getrennt, nicht über eigene Prozesse.
  • Gemeinsame Zugangsdaten. Jedes Projekt verbindet sich als root. Das ist ein Werkzeug für die lokale Entwicklung, und die Datenbankserver veröffentlichen keine Ports außerhalb des Docker-Netzwerks (nur die Web-Oberfläche des Mail-Catchers, Port 8025, ist auf dem Host veröffentlicht), aber als Vorbild für die Produktion taugt es nicht.

Wann Zeltro die falsche Wahl ist: wenn Sie Datenbankversionen pro Projekt brauchen oder Ihre lokale Umgebung die Topologie der Produktion exakt abbilden muss. Werkzeuge, die pro Projekt einen kompletten Stack betreiben – DDEV, Lando oder einfaches Docker Compose –, passen für beides besser, um den Preis des oben beschriebenen Ressourcenverbrauchs.

Wann es die richtige ist: wenn Sie viele kleine bis mittlere Projekte auf einem Rechner betreiben und den Speicher lieber den Projekten geben als zwanzig Kopien von MariaDB.

Immer an:

Dienst Hostname Port Benutzer Passwort
Redis 8 zeltro-redis 6379 — (keins)
Memcached 1.6 zeltro-memcached 11211 — (keins)
Mail-Catcher (Mailpit) zeltro-mailhog SMTP 1025 / UI 8025 — (keins)

Der Mail-Catcher ist Mailpit, ein Ersatz für MailHog. Er behält den Hostnamen zeltro-mailhog, damit bestehende Projekte nicht geändert werden müssen. Seine Web-Oberfläche ist außerdem auf dem Host unter http://localhost:8025 veröffentlicht.

Datenbanken, gestartet, wenn ein Projekt eine braucht:

Dienst Hostname Port Benutzer Passwort
MariaDB 12 (MySQL-kompatibel) zeltro-mariadb 3306 root (leer)
PostgreSQL 17 zeltro-postgres 5432 root password
MongoDB 8 zeltro-mongo 27017 root password

zeltro new, clone, setup, install und up ermitteln, welche Datenbank ein Projekt nutzt, und schalten diesen Server ein, falls er noch nicht läuft. Sie müssen nichts tun. Um einen von Hand einzuschalten, nutzen Sie zeltro enable-service mysql, postgres oder mongo.

Nutzen Sie diese Hostnamen und Zugangsdaten direkt, wenn Sie ein Projekt konfigurieren – Sie müssen keine Container untersuchen, um sie herauszufinden. Zeltro schreibt sie automatisch in die .env jedes Projekts.

Optionale gemeinsame Dienste#

Alles außer Redis, Memcached und dem Mail-Catcher ist optional: die drei Datenbanken oben, dazu Objektspeicher, eine Suchmaschine und Web-Oberflächen zur Verwaltung. Sie liegen hinter Docker-Compose-Profilen und bleiben aus, bis ein Rechner sie anfordert, sodass ein Rechner keinen RAM für Dienste bezahlt, die keines seiner Projekte nutzt.

zeltro enable-service minio
zeltro enable-service adminer
zeltro disable-service minio      # data volume is kept
zeltro enable-service --help      # list every optional service

Einmal eingeschaltet, starten sie bei jedem zeltro up mit und sind aus jedem Projekt-Container über ihren Hostnamen erreichbar, genau wie die Dienste, die immer laufen. Die Liste der eingeschalteten Dienste steht dauerhaft in OPTIONAL_SERVICES in /etc/zeltro-cli/.env.

Dienst Slug Host Port Zugangsdaten
MinIO-kompatibler S3-Speicher (Silo, der gepflegte MinIO-Fork) minio zeltro-minio API 9000 / Konsole 9001 root / password
Meilisearch (Volltextsuche) meilisearch zeltro-meilisearch 7700 Master-Key zeltro-dev-master-key
phpMyAdmin (MariaDB) phpmyadmin zeltro-phpmyadmin 80 root, kein Passwort
Adminer (MariaDB, PostgreSQL, SQLite, MongoDB) adminer zeltro-adminer 8080 root und das Passwort der jeweiligen Datenbank
Mongo Express mongo-express zeltro-mongo-express 8081 keine
RedisInsight redisinsight zeltro-redisinsight 5540 keine

Die Verwaltungsoberflächen sind bereits mit den gemeinsamen Servern verbunden. RedisInsight zeigt das gemeinsame Redis erst, nachdem Sie beim ersten Start seine Nutzungsbedingungen akzeptiert haben.


Wie ein Projekt erreichbar ist#

Zwei verschiedene Mechanismen, und es lohnt sich, sie auseinanderzuhalten:

  • Container → Container: über den Namen. Dockers eingebautes DNS löst Container-Namen im gemeinsamen Netzwerk auf. Code in einem Projekt kann psql -h zeltro-postgres oder fetch('http://other-project/') ohne weitere Konfiguration nutzen. So teilen sich Projekte Datenbanken und rufen einander auf.
  • Host oder LAN → Projekt: über die Adresse. Ihr Browser hängt nicht im Docker-Netzwerk, also nutzt er eine Adresse. zeltro status zeigt für jedes Projekt zwei an:
Von Adresse Warum
Dem Rechner, auf dem Zeltro läuft http://10.x.x.219 Die eigene IP des Containers. Kein Port – sonst ist nichts auf dieser Adresse.
Einem anderen Gerät im LAN http://192.168.1.20:219 Die IP dieses Rechners plus der veröffentlichte Port des Projekts, also die letzte Zahl seiner Container-IP.

Unter macOS betreibt Docker Desktop die Container in einer virtuellen Maschine und kann vom Host aus keinen Verkehr an Container-IPs leiten. Deshalb zeigt zeltro status dort http://localhost:<port> als lokale Adresse.

Unter Windows läuft Zeltro in WSL2. Die lokale Adresse funktioniert innerhalb von WSL. In einem Windows-Browser nutzen Sie die LAN-Adresse; das ist die IP der WSL-VM, und sie ändert sich, wenn WSL neu startet.

Zeltro schreibt nicht in /etc/hosts, daher funktioniert http://my-api/ in einem Browser auf dem Host nicht. Nutzen Sie die Adressen, die zeltro status anzeigt. Frühere Versionen haben für jedes Projekt einen Host-Eintrag angelegt; das ist weggefallen, und Anlegen oder Starten eines Projekts braucht dafür kein sudo mehr.


VPC und IP-Vergabe#

Alle Zeltro-Container hängen am Docker-Netzwerk zeltro-cli_vpc (${VPC_SUBNET}.0/24, pro Rechner in /etc/zeltro-cli/.env festgelegt). Der Adressraum ist so aufgeteilt, dass statische IPs nie mit dynamischen kollidieren:

Bereich Zweck Vergabe
.2–.8 Dauerhaft laufende Dienste, Datenbanken und phpMyAdmin Statisch
.32–.63 Hilfs-Container (Worker, Scheduler) Dynamisch
.100–.239 Einstiegspunkte der Projekte Statisch, pro Projekt zufällig gewählt
.250–.254 MinIO, Meilisearch, Adminer, Mongo Express, RedisInsight Statisch

.240–.249 bleibt als Reserve frei. Der Web-Port jedes Projekts wird auf dem Host unter seinem letzten Oktett veröffentlicht; ein Projekt auf .219 antwortet also auf Port 219.

Wenn Sie eine eigene Compose-Datei schreiben: Geben Sie dem Dienst, der nach außen antwortet, eine statische IP in .100–.239, lassen Sie Hilfsdienste ohne ipv4_address und fassen Sie .2–.8 und .250–.254 nie an.


Basis-Images#

Neue Projekte werden aus einem von drei Images auf Docker Hub unter canebaycomputers/cbc gebaut. Jedes betreibt nginx + supervisor auf Ubuntu Noble. Die Quellen liegen in CaneBayComputers/docker-images, ein Verzeichnis pro Image.

Mit --image <ref> bei new, clone, setup oder install ersetzen Sie das Standard-Image.

cbc:nginx-php8 – PHP-Projekte#

  • nginx mit FastCGI zu php-fpm8.3 über einen Unix-Socket; Web-Root /usr/share/nginx/html/public
  • PHP 8.3 mit pdo_mysql, pdo_pgsql, pdo_sqlite, redis, mongodb, gd, intl, mbstring, imagick, zip, soap, xdebug, dazu php-codesniffer und phpmd
  • Composer, WP-CLI und Node 22 (für Builds mit Vite/npm) sind enthalten
  • Supervisor betreibt nginx, php-fpm und einen Laravel-Queue-Worker (4 Prozesse, Autostart)

cbc:nginx-python3 – Python-Projekte#

  • nginx als Reverse-Proxy, Port 80 → localhost:8000, mit WebSocket-Upgrade-Headern
  • Vorinstalliert: fastapi, uvicorn, django, flask, gunicorn, python-dotenv, sqlalchemy, psycopg2-binary, PyMySQL, pymongo, redis, pymemcache
  • Supervisor startet PYTHON_APP_COMMAND; die App muss an Port 8000 lauschen
  • python3 ist verfügbar, python nicht

cbc:nginx-node – Node-Projekte#

  • nginx als Reverse-Proxy, Port 80 → localhost:3000, mit WebSocket-Upgrade-Headern
  • Node 22 LTS
  • Supervisor startet NODE_APP_COMMAND; die App muss an Port 3000 lauschen. Setzen Sie PORT=3000 in der .env für Frameworks, die standardmäßig einen anderen Port nutzen.

Projekt-Container werden bei jedem zeltro up aus dem Basis-Image neu erstellt. Alles, was ins Dateisystem eines laufenden Containers installiert wurde, geht verloren – nur das per Bind-Mount eingebundene Projektverzeichnis bleibt. Deshalb steckt die Laufzeit jedes Frameworks fest im Image, und deshalb muss eine SQLite-Datenbank im Projektverzeichnis liegen.


Compose-Anpassung#

Trifft zeltro clone oder zeltro setup auf eine docker-compose.yaml mit mehr als einem Dienst oder einem Image, das nicht von cbc stammt, passt Zeltro sie an:

  • Mitgelieferte Dienste postgres / mysql / mariadb / redis / valkey / mongodb werden entfernt, und Umgebungsvariablen, die auf sie verweisen, werden auf die gemeinsamen Hostnamen umgeleitet
  • Der nach außen antwortende Dienst bekommt eine statische VPC-IP und einen container_name, der zum Projekt passt
  • Andere Dienste hängen ohne feste IP am Netzwerk
  • Das Original bleibt als docker-compose.upstream.yaml erhalten (nur beim ersten Lauf, damit das echte Original weitere Läufe übersteht)
  • docker-compose.yaml wird in .gitignore eingetragen, damit die von Zeltro verwaltete Datei nicht in ein gemeinsames Repository zurück committet wird

Der Image-Typ betrifft nur diese Anpassung. Die Framework-Schritte – Installation der Abhängigkeiten, .env-Verdrahtung, Storage-Symlink, Migrationen – richten sich nach der Framework-Erkennung und laufen auch für angepasste Projekte.

Nützliche Optionen: --no-startup, um die angepasste Compose-Datei vor dem Start zu prüfen, --overwrite-env, um die .env einer bestehenden App auf die gemeinsamen Dienste umzuleiten und dabei APP_KEY zu behalten, --db-name <name>, um die Datenbank zu wählen, --no-migration, um Migrationen zu überspringen.


Projektstruktur#

~/zeltro-projects/
├── my-api/
│   ├── docker-compose.yaml          # Zeltro-managed (gitignored)
│   ├── docker-compose.upstream.yaml # the original, if there was one
│   ├── AGENTS.md                    # hand-off context for AI agents
│   ├── .env
│   └── [your project files]
└── my-shop/

~/zeltro-projects ist der Standard. zeltro projects-dir zeigt, welches Verzeichnis dieser Rechner nutzt. Das Projektverzeichnis bekommt außerdem eine eigene AGENTS.md mit den Dienstnamen dieses Rechners.

Die Laufzeitkonfiguration liegt in /etc/zeltro-cli/.env. Ein Rechner, der vor der Umbenennung in Zeltro installiert wurde, nutzt womöglich noch ältere Container- und Netzwerknamen; zeltro migrate-names stellt ihn ohne Datenverlust auf die zeltro-*-Namen um.

Einen Fehler in dieser Übersetzung gefunden? Sagen Sie es uns auf GitHub.