[{"data":1,"prerenderedAt":2079},["ShallowReactive",2],{"update-\u002Fupdates\u002Fmobile-performance":3,"updates-index":318},{"id":4,"title":5,"body":6,"date":308,"description":309,"extension":310,"meta":311,"navigation":312,"path":313,"seo":314,"stem":315,"tag":316,"__hash__":317},"updates\u002Fupdates\u002Fmobile-performance.md","Von 5 Sekunden auf unter 0,5",{"type":7,"value":8,"toc":289},"minimark",[9,18,23,26,33,41,45,71,74,106,109,113,122,125,135,138,169,181,185,188,200,204,207,213,216,219,223,232,257,261,264,276,282,286],[10,11,12,13,17],"p",{},"Am Desktop hat die Landingpage sofort geladen. Am Handy war sie furchtbar langsam, über fünf Sekunden bis sie da war. Schuld war am Ende ein einziger CSS-Effekt: ",[14,15,16],"code",{},"blur()",". Hier der ganze Weg von der Diagnose bis zu dem, was unter der Haube wirklich passiert.",[19,20,22],"h2",{"id":21},"das-problem","Das Problem",[10,24,25],{},"Am Handy war die Seite einfach langsam. Das Laden dauerte spürbar lang, am Desktop war es blitzschnell. Die üblichen Verdächtigen, zu große Bilder, zu viel JavaScript, langsamer Server, waren es nicht.",[10,27,28,29,32],{},"Am Ende war es der Hintergrund. Dort liegen ein paar große, weiche Lichtflecken (Glows), umgesetzt mit ",[14,30,31],{},"filter: blur()",". Ein Element war über 1000px breit, mit einem Blur-Radius von 100px.",[34,35,38],"callout",{"color":36,"icon":37},"primary","i-lucide-zap-off",[10,39,40],{},"Über fünf Sekunden bis zum ersten Frame ist für eine Landingpage tödlich. Genau dort entscheidet sich, ob jemand bleibt.",[19,42,44],{"id":43},"warum-blur-so-teuer-ist","Warum Blur so teuer ist",[10,46,47,49,50,57,58,62,63,66,67,70],{},[14,48,16],{}," ist eine ",[51,52,56],"a",{"href":53,"rel":54},"https:\u002F\u002Fdeveloper.chrome.com\u002Fblog\u002Fanimated-blur",[55],"nofollow","Convolution",": Für jeden Output-Pixel werden die umliegenden Input-Pixel gewichtet zusammengerechnet. Die Kosten skalieren mit Fläche ",[59,60,61],"strong",{},"und"," Radius, ein ",[14,64,65],{},"blur(100px)"," ist also drastisch teurer als ein ",[14,68,69],{},"blur(5px)",".",[10,72,73],{},"Im Detail läuft pro Frame ungefähr das ab:",[75,76,78,83,86,90,93,97],"steps",{"level":77},"3",[79,80,82],"h3",{"id":81},"render-to-texture","Render-to-Texture",[10,84,85],{},"Das Element wird zuerst in eine eigene Off-Screen-Textur gerendert. Allein das promotet es zu einer eigenen Compositing-Layer und kostet GPU-Speicher.",[79,87,89],{"id":88},"separable-gaussian","Separable Gaussian",[10,91,92],{},"Ein 2D-Gaussian lässt sich in zwei 1D-Passes zerlegen (horizontal, dann vertikal). Das senkt die Kosten von O(n²) auf O(n) pro Pixel, ist also schon die optimierte Variante.",[79,94,96],{"id":95},"downsampling","Downsampling",[10,98,99,100,105],{},"Bei großen Radien rechnet die Engine nicht in voller Auflösung. Skia (Chrome) ",[51,101,104],{"href":102,"rel":103},"https:\u002F\u002Fapi.skia.org\u002FclassSkImageFilters.html",[55],"skaliert ab einem Sigma > 4 herunter",", blurrt auf der kleineren Buffer-Größe und skaliert wieder hoch.",[10,107,108],{},"Trotz all dieser Optimierungen bleibt eine riesige Fläche übrig, die vor dem ersten Paint fertig sein muss. Auf dem Desktop fällt das nicht auf. Eine Mobile-GPU stemmt das deutlich langsamer, und genau das hat den ersten Frame um Sekunden verzögert.",[19,110,112],{"id":111},"warum-es-mobile-gpus-besonders-trifft","Warum es Mobile-GPUs besonders trifft",[10,114,115,116,121],{},"Mobile-GPUs arbeiten fast alle ",[51,117,120],{"href":118,"rel":119},"https:\u002F\u002Fdeveloper.samsung.com\u002Fgalaxy-gamedev\u002Fresources\u002Farticles\u002Fgpu-framebuffer.html",[55],"Tile-Based",": Der Framebuffer wird in kleine Tiles zerlegt, die im schnellen On-Chip-Speicher gerendert werden. Das spart enorm Memory-Bandwidth, funktioniert aber nur, solange ein Tile für sich allein berechenbar ist.",[10,123,124],{},"Ein Blur bricht genau diese Annahme. Die Convolution muss Pixel über die Tile-Grenze hinaus lesen. Die GPU kann den Effekt also nicht tile-lokal halten, sondern muss in den langsamen System-Memory schreiben und wieder lesen. Auf Hardware mit ohnehin knapper Fill-Rate und Bandwidth ist das der teuerste Fall.",[19,126,128,131,132],{"id":127},"filter-ist-nicht-backdrop-filter",[14,129,130],{},"filter"," ist nicht ",[14,133,134],{},"backdrop-filter",[10,136,137],{},"Im Header steckte noch ein zweiter Blur, und der ist eine ganz andere Baustelle. Die beiden werden oft verwechselt:",[139,140,141,153],"ul",{},[142,143,144,148,149,152],"li",{},[59,145,146],{},[14,147,31],{}," blurrt die ",[59,150,151],{},"eigenen Pixel"," des Elements. Inhalt dahinter ist egal.",[142,154,155,164,165,168],{},[59,156,157],{},[51,158,161],{"href":159,"rel":160},"https:\u002F\u002Fwww.w3tweaks.com\u002Fcss\u002Fcss-filter-backdrop-filter\u002F",[55],[14,162,163],{},"backdrop-filter: blur()"," blurrt ",[59,166,167],{},"alles, was dahinter liegt",", und liest diese Pixel bei jedem Frame neu.",[10,170,171,172,177,178,180],{},"Letzteres ist beim Scrollen brutal: Der Backdrop ändert sich ständig, also wird der Blur permanent neu berechnet. ",[51,173,176],{"href":174,"rel":175},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fvitepress\u002Fissues\u002F1049",[55],"Genau das ruckelt sichtbar",", bei älteren Firefox-Builds unter Linux. Ein fixer, halbtransparenter Header mit ",[14,179,134],{}," ist einer der häufigsten Jank-Verursacher überhaupt.",[19,182,184],{"id":183},"und-blur-animieren","Und Blur animieren?",[10,186,187],{},"Kurz: nicht den Radius animieren. Jeder Frame triggert die volle Convolution auf der GPU und sprengt das 16ms-Frame-Budget, das Ergebnis liegt weit unter 60fps.",[10,189,190,191,195,196,199],{},"Der ",[51,192,194],{"href":53,"rel":193},[55],"Trick von Chrome",": ein paar vorberechnete Kopien mit exponentiell steigendem Blur stapeln und per ",[14,197,198],{},"opacity"," zwischen ihnen cross-faden. Opacity ist compositor-only und damit billig. Man animiert also nicht den Blur, sondern täuscht ihn vor.",[19,201,203],{"id":202},"die-lösung","Die Lösung",[10,205,206],{},"Im konkreten Fall war sie fast enttäuschend simpel: den Blur ganz weglassen.",[10,208,209,210,212],{},"Statt einen scharfen Kreis teuer zu blurren, sind die Glows jetzt mehrstufige Radial-Gradients, die von Haus aus weich sind. Optisch praktisch identisch, aber ohne Filter, ohne extra Layer, ohne Convolution. Den ",[14,211,134],{}," im Header habe ich gleich mit rausgeworfen.",[10,214,215],{},"Andere valide Wege, je nach Fall: ein vorgeblurtes Bild als Asset ausliefern, den Radius klein halten, oder bewusst auf einem kleineren Element blurren.",[10,217,218],{},"Das Ergebnis: Statt über fünf Sekunden ist die Seite jetzt in unter einer halben Sekunde da. Zehnmal schneller, ohne dass man dem Design den Unterschied ansieht.",[19,220,222],{"id":221},"wie-man-so-etwas-findet","Wie man so etwas findet",[10,224,225,226,231],{},"In den Chrome DevTools führt der ",[51,227,230],{"href":228,"rel":229},"https:\u002F\u002Fdeveloper.chrome.com\u002Fdocs\u002Fdevtools\u002Frendering\u002Fperformance",[55],"Rendering-Tab"," schnell zur Ursache:",[139,233,234,240,250],{},[142,235,236,239],{},[59,237,238],{},"Paint Flashing"," zeigt grün, was neu gezeichnet wird. Ein Header, der beim Scrollen dauergrün blinkt, ist ein eindeutiges Signal.",[142,241,242,245,246,249],{},[59,243,244],{},"Layer Borders"," und der ",[59,247,248],{},"Layers-Tab"," zeigen, welche Elemente eine eigene Compositing-Layer bekommen, dort taucht jeder Blur auf.",[142,251,252,253,256],{},"Im ",[59,254,255],{},"Performance-Panel"," sieht man die langen Paint- und GPU-Tasks direkt im Flame-Chart.",[19,258,260],{"id":259},"was-ältere-browser-damit-zu-tun-haben","Was ältere Browser damit zu tun haben",[10,262,263],{},"Mein erster Reflex war: \"Blur wird doch überall unterstützt.\" Stimmt laut Caniuse, sagt aber nichts über Performance.",[10,265,266,269,270,275],{},[59,267,268],{},"GPU-Beschleunigung kann ganz wegfallen."," Firefox rendert über WebRender, fällt aber auf vielen Linux-Setups (alte Intel-GPUs, proprietäre Nvidia-Treiber) auf ",[51,271,274],{"href":272,"rel":273},"https:\u002F\u002Fwiki.archlinux.org\u002Ftitle\u002FFirefox\u002FTweaks",[55],"Software-Rendering"," zurück. Dann läuft die Blur-Convolution auf der CPU, und aus \"unmerklich\" wird \"ruckelt\".",[34,277,279],{"color":36,"icon":278},"i-lucide-lightbulb",[10,280,281],{},"Ein grüner Caniuse-Balken heißt \"der Browser kennt das Feature\", nicht \"er macht es schnell, auch ohne GPU-Beschleunigung\".",[19,283,285],{"id":284},"was-ich-mitnehme","Was ich mitnehme",[10,287,288],{},"Der schönste Effekt taugt nichts, wenn die Seite dadurch langsam wird. Blur ist teuer in der Berechnung, Layer-erzeugend, mit Nebenwirkungen auf Stacking und Positionierung, und besonders fragil auf Mobile und ohne GPU-Beschleunigung. Und immer auf echten, eher schwachen Geräten testen, nicht nur auf der dicken Entwickler-Maschine.",{"title":290,"searchDepth":291,"depth":291,"links":292},"",2,[293,294,300,301,303,304,305,306,307],{"id":21,"depth":291,"text":22},{"id":43,"depth":291,"text":44,"children":295},[296,298,299],{"id":81,"depth":297,"text":82},3,{"id":88,"depth":297,"text":89},{"id":95,"depth":297,"text":96},{"id":111,"depth":291,"text":112},{"id":127,"depth":291,"text":302},"filter ist nicht backdrop-filter",{"id":183,"depth":291,"text":184},{"id":202,"depth":291,"text":203},{"id":221,"depth":291,"text":222},{"id":259,"depth":291,"text":260},{"id":284,"depth":291,"text":285},"2026-06-12","Ein Deep Dive, warum CSS-Blur einer der teuersten Effekte im Browser ist und wie ein einziger Filter die Landingpage am Handy ausgebremst hat.","md",{},true,"\u002Fupdates\u002Fmobile-performance",{"title":5,"description":309},"updates\u002Fmobile-performance","Performance","kGrKA57XOvzqCZ9wyNjojvTa5NKeaJUvNWLoyhI1I1Q",[319,488,790,1007,1256,1543,1725,1884,1965],{"id":320,"title":321,"body":322,"date":480,"description":481,"extension":310,"meta":482,"navigation":312,"path":483,"seo":484,"stem":485,"tag":486,"__hash__":487},"updates\u002Fupdates\u002Fweb-terminal-vscode.md","Ein Terminal und VS Code für jeden Run",{"type":7,"value":323,"toc":473},[324,332,336,339,344,348,351,365,371,375,378,443,446,450,459,463,466,469],[10,325,326,327,331],{},"Ein Run ist eine einzelne Ausführung eines ",[51,328,330],{"href":329},"\u002Fupdates\u002Fworkflow-engine","Workflows"," auf einem Projekt, mit eigener Umgebung auf dem Server. Bisher kam man an diese Umgebung nur über das Dashboard heran. Man konnte dem Agenten Anweisungen schicken und die Preview anschauen, aber nicht selbst hinein. Wer nur kurz ein Log lesen oder einen Befehl ausprobieren wollte, musste dafür den Agenten bemühen. Jetzt gibt es auf der Run-Seite drei direkte Wege hinein.",[19,333,335],{"id":334},"das-terminal","Das Terminal",[10,337,338],{},"Der direkteste Weg in einen Run ist eine Shell. Es gibt sie in zwei Varianten, im Browser für jedes Dashboard-Mitglied und über SSH für alle, die ohnehin auf den Server dürfen. Das Video zeigt beide.",[340,341],"update-video",{"caption":342,"src":343},"Das Terminal im Browser und der fertige SSH-Befehl","\u002Fassets\u002Fknecht-ssh-showcase.webm",[79,345,347],{"id":346},"im-browser","Im Browser",[10,349,350],{},"Ein Klick auf \"Terminal\" öffnet eine Shell im Container des Runs. Sie geht als Fenster direkt auf der Run-Seite auf, man tippt also im selben Browser-Tab, in dem man den Run gerade anschaut. Ein Terminal-Programm oder ein SSH-Client auf dem eigenen Rechner ist dafür nicht nötig.",[139,352,353,356,359,362],{},[142,354,355],{},"Funktioniert für jedes Dashboard-Mitglied, ganz ohne Zugang zum Server.",[142,357,358],{},"Die Shell läuft im selben Container wie der Agent. Composer, npm und Git sind da und verhalten sich wie im echten Projekt.",[142,360,361],{},"Es geht kein zusätzlicher Port auf, die Verbindung läuft über dieselbe HTTPS-Verbindung wie das Dashboard.",[142,363,364],{},"Läuft ein Run mit mehreren Containern, etwa einer eigenen Datenbank, hat das Terminal einen Tab pro Service.",[366,367,368],"note",{},[10,369,370],{},"Knecht stoppt die Umgebung eines Runs automatisch, wenn sie eine Weile niemand benutzt, damit sie keinen RAM belegt. Arbeit im Terminal zählt dabei als Benutzung, die Umgebung bleibt also an, solange man darin tippt.",[79,372,374],{"id":373},"über-ssh","Über SSH",[10,376,377],{},"Wer lieber im eigenen Terminal arbeitet, findet im Terminal-Fenster einen fertigen SSH-Befehl zum Kopieren. Auf der eigenen Maschine eingefügt, landet er in genau demselben Container.",[379,380,384],"pre",{"className":381,"code":382,"language":383,"meta":290,"style":290},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","ssh -t knecht@my-server.com docker exec -it -u 1000:1000 -w \u002Fvar\u002Fwww\u002Fhtml -e HOME=\u002Fhome\u002Fnode -e USER=node ddev-knecht-run-4-web bash -l\n","bash",[14,385,386],{"__ignoreMap":290},[387,388,391,395,399,402,405,408,411,414,417,420,423,426,429,431,434,437,440],"span",{"class":389,"line":390},"line",1,[387,392,394],{"class":393},"sBMFI","ssh",[387,396,398],{"class":397},"sfazB"," -t",[387,400,401],{"class":397}," knecht@my-server.com",[387,403,404],{"class":397}," docker",[387,406,407],{"class":397}," exec",[387,409,410],{"class":397}," -it",[387,412,413],{"class":397}," -u",[387,415,416],{"class":397}," 1000:1000",[387,418,419],{"class":397}," -w",[387,421,422],{"class":397}," \u002Fvar\u002Fwww\u002Fhtml",[387,424,425],{"class":397}," -e",[387,427,428],{"class":397}," HOME=\u002Fhome\u002Fnode",[387,430,425],{"class":397},[387,432,433],{"class":397}," USER=node",[387,435,436],{"class":397}," ddev-knecht-run-4-web",[387,438,439],{"class":397}," bash",[387,441,442],{"class":397}," -l\n",[10,444,445],{},"Knecht betreibt dafür keinen eigenen SSH-Server und verwaltet keine Keys. Der Befehl nutzt den SSH-Zugang zum Server, den es sowieso schon gibt. In den Einstellungen wird dafür einmal die SSH-Adresse des Servers hinterlegt, mehr nicht. Dieser Weg ist damit denen vorbehalten, die ohnehin auf den Server dürfen. Für alle anderen bleibt das Web-Terminal.",[19,447,449],{"id":448},"vs-code-im-browser","VS Code im Browser",[10,451,452,453,458],{},"VS Code läuft direkt im Run. Ein Klick auf \"Open in VS Code\" auf der Run-Seite öffnet die IDE als neuen Browser-Tab. Dahinter steckt ",[51,454,457],{"href":455,"rel":456},"https:\u002F\u002Fgithub.com\u002Fgitpod-io\u002Fopenvscode-server",[55],"openvscode-server",", der Open-Source-Build von VS Code für den Browser.",[340,460],{"caption":461,"src":462},"VS Code öffnet den Run im Browser","\u002Fassets\u002Fknecht-vs-code.webm",[10,464,465],{},"Die IDE arbeitet dabei direkt am Projekt im Run, nicht an einer Kopie. Eine gespeicherte Änderung liegt sofort im laufenden Projekt, es ist derselbe Code, den auch die Preview und der Agent sehen. Und weil jeder Run ein vollwertiger Git-Clone ist, funktionieren Commits und Branches in IDE und Terminal ganz normal.",[10,467,468],{},"Ein lokales Setup braucht es nicht. Die IDE läuft für jedes Dashboard-Mitglied im Browser und ist wie die Previews nur mit Knecht-Login erreichbar.",[470,471,472],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":290,"searchDepth":291,"depth":291,"links":474},[475,479],{"id":334,"depth":291,"text":335,"children":476},[477,478],{"id":346,"depth":297,"text":347},{"id":373,"depth":297,"text":374},{"id":448,"depth":291,"text":449},"2026-07-27","Ein Terminal im Browser, ein fertiger SSH-Befehl und VS Code ohne lokales Setup. Jeder Run lässt sich jetzt direkt öffnen und weiterbearbeiten.",{},"\u002Fupdates\u002Fweb-terminal-vscode",{"title":321,"description":481},"updates\u002Fweb-terminal-vscode","Dashboard","8NUQlo_9Gh0OZNW07t1c2nlkm2T0CHTBOqgI14NvSZ8",{"id":489,"title":490,"body":491,"date":782,"description":783,"extension":310,"meta":784,"navigation":312,"path":785,"seo":786,"stem":787,"tag":788,"__hash__":789},"updates\u002Fupdates\u002Fsandbox-rollback.md","Zurück zum geteilten Daemon, eine Preview für 180 MB",{"type":7,"value":492,"toc":771},[493,503,518,522,537,545,548,552,555,575,578,582,586,589,602,606,615,630,633,637,652,661,665,668,745,748,761,764,768],[366,494,495],{},[10,496,497,498,502],{},"Dieser Post ist die Fortsetzung von ",[51,499,501],{"href":500},"\u002Fupdates\u002Fdashboard-architecture","Vom geteilten Docker Daemon zur Sandbox pro Run",". Dort steht, warum wir uns für eine eigene Sandbox pro Run entschieden haben. Hier steht, warum wir sie wieder ausgebaut haben.",[10,504,505,506,511,512,517],{},"Der letzte Architektur-Post endete mit einem klaren Plan. Jeder Run bekommt seine eigene ",[51,507,510],{"href":508,"rel":509},"https:\u002F\u002Fgithub.com\u002Fnestybox\u002Fsysbox",[55],"Sysbox","-Sandbox mit eigenem ",[51,513,516],{"href":514,"rel":515},"https:\u002F\u002Fdocs.docker.com\u002Fget-started\u002Fdocker-overview\u002F#docker-architecture",[55],"Docker Daemon",". Wir haben das gebaut, und es lief. Trotzdem haben wir die Sandbox wieder ausgebaut, denn zwei Probleme, die wir bewusst aufgeschoben hatten, ließen sich nicht mehr aufschieben.",[19,519,521],{"id":520},"die-rechnung","Die Rechnung",[10,523,524,525,530,531,536],{},"Eine Sysbox-Sandbox ist im Grunde ein kleines Betriebssystem mit ",[51,526,529],{"href":527,"rel":528},"https:\u002F\u002Fsystemd.io",[55],"systemd",", eigenem Docker Daemon und darüber dem kompletten ",[51,532,535],{"href":533,"rel":534},"https:\u002F\u002Fddev.com",[55],"DDEV","-Stack. Dass das Ressourcen kostet, war uns von Anfang an klar. Wir dachten nur, wir bekommen das später in den Griff, dem war aber leider nicht so.",[139,538,539,542],{},[142,540,541],{},"Eine laufende Preview kostete gemessen rund 3 GB RAM. Bei 20 parallelen Previews wären das 60 GB, nur für Sandboxes.",[142,543,544],{},"Jede Sandbox braucht alle DDEV-Images einmal für sich, das sind 1-2 GB Disk pro Preview. Ein Registry-Cache beschleunigte die Downloads, am Platz änderte er nichts.",[10,546,547],{},"Das Ziel von Knecht ist aber, dass eine Agentur alle ihre Projekte auf einem eigenen, bezahlbaren Server laufen lässt. Dafür ist eine Architektur mit 3 GB pro Preview das falsche Fundament, egal wie sauber sie isoliert.",[19,549,551],{"id":550},"was-die-sandbox-löste","Was die Sandbox löste",[10,553,554],{},"Zur Erinnerung, der letzte Post hatte zwei Gründe für die Sandbox.",[139,556,557,566],{},[142,558,559,560,565],{},"Auf dem geteilten Daemon gab es keine Isolation zwischen Runs. Die festen Ports des ",[51,561,564],{"href":562,"rel":563},"https:\u002F\u002Fdocs.ddev.com\u002Fen\u002Fstable\u002Fusers\u002Fusage\u002Farchitecture\u002F",[55],"DDEV-Routers"," kollidierten, deshalb mussten wir ihn weglassen und in die DDEV-Config eingreifen.",[142,567,568,569,574],{},"Der Agent führt fremden Code aus, und wer den ",[51,570,573],{"href":571,"rel":572},"https:\u002F\u002Fdocs.docker.com\u002Fengine\u002Fsecurity\u002F#docker-daemon-attack-surface",[55],"Docker-Socket"," des Hosts erreicht, kontrolliert den ganzen Server.",[10,576,577],{},"Die Sandbox löste beides mit einem Schlag. Aber sie war die gründlichste Lösung, nicht die einzige. Der Rückbau war erst möglich, als wir beide Probleme noch einmal einzeln angeschaut haben.",[19,579,581],{"id":580},"die-neuen-lösungen","Die neuen Lösungen",[79,583,585],{"id":584},"kein-socket-im-run","Kein Socket im Run",[10,587,588],{},"Das Problem ist der Docker-Socket. Der Agent führt fremden Code aus, und sobald der Socket in seiner Umgebung liegt, kontrolliert ein gekaperter Agent den ganzen Server. Die Sandbox hat deshalb jedem Run einen eigenen Daemon gegeben, damit der Socket des Hosts außer Reichweite bleibt.",[10,590,591,592,595,596,601],{},"Unsere Lösung setzt früher an, der Run bekommt überhaupt keinen Docker-Zugriff mehr. Knecht fährt DDEV host-seitig hoch. Der Agent und alle Projekt-Befehle wie ",[14,593,594],{},"composer install"," laufen ausschließlich im Web-Container des Projekts. Ein Agent, der per ",[51,597,600],{"href":598,"rel":599},"https:\u002F\u002Fsimonwillison.net\u002Fseries\u002Fprompt-injection\u002F",[55],"Prompt Injection"," gekapert wird, steht damit in einem ganz normalen Container mit eigenem Netz und begrenzten Ressourcen, und er findet dort weder Socket noch Daemon vor.",[79,603,605],{"id":604},"umbenennen-statt-einsperren","Umbenennen statt einsperren",[10,607,608,609,614],{},"Die Port-Kollisionen kamen allein vom Router, dem Reverse Proxy, den DDEV vor alle Projekte stellt und der die Ports 80 und 443 auf dem Host bindet. Der bleibt weiter weg. Stattdessen spricht der Preview-Proxy den Web-Container jedes Runs direkt über das ",[51,610,613],{"href":611,"rel":612},"https:\u002F\u002Fdocs.docker.com\u002Fengine\u002Fnetwork\u002F",[55],"Docker-Netzwerk"," an.",[10,616,617,618,623,624,629],{},"Eine Sache muss der Proxy dabei mitliefern, den Hostnamen. Projekte mit mehreren Domains, etwa ein ",[51,619,622],{"href":620,"rel":621},"https:\u002F\u002Fcraftcms.com\u002Fdocs\u002F5.x\u002Fsystem\u002Fsites.html",[55],"Craft-Multisite",", entscheiden anhand der aufgerufenen Domain, welche Site sie ausliefern. Die Preview läuft aber unter einer Knecht-URL, nicht unter der echten Domain des Projekts. Deshalb schickt der Proxy die echte Domain im ",[51,625,628],{"href":626,"rel":627},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FHTTP\u002FReference\u002FHeaders\u002FHost",[55],"Host-Header"," mit, und für das Projekt sieht jede Anfrage so aus, als wäre sie über die normale URL gekommen.",[10,631,632],{},"Damit parallele Runs desselben Projekts sich nicht in die Quere kommen, registriert Knecht jeden Run als eigenes DDEV-Projekt mit eindeutigem Namen. Der Eingriff in die DDEV-Config, im letzten Post noch ein Minuspunkt, ist geblieben, aber er ist für das Projekt unsichtbar. Der Code im Repo bleibt unangetastet, umgeschrieben wird nur die generierte Laufzeit-Config.",[19,634,636],{"id":635},"der-tradeoff","Der Tradeoff",[10,638,639,640,645,646,651],{},"Sicherheitstechnisch ist das ein Rückschritt gegenüber Sysbox. Sysbox mappte Root im Container über ",[51,641,644],{"href":642,"rel":643},"https:\u002F\u002Fman7.org\u002Flinux\u002Fman-pages\u002Fman7\u002Fuser_namespaces.7.html",[55],"User Namespaces"," auf einen unprivilegierten Host-User. Jetzt laufen die Container wieder mit ",[51,647,650],{"href":648,"rel":649},"https:\u002F\u002Fgithub.com\u002Fopencontainers\u002Frunc",[55],"runc",", der normalen Docker-Runtime, die Grenze zum Host ist dünner. Aufgefangen wird das durch genau die Punkte von oben, denn der Run hat keinen Socket, ein eigenes Netz und feste Limits.",[10,653,654,655,660],{},"Ob dieser Rückschritt vertretbar ist, entscheidet wie schon im letzten Post das ",[51,656,659],{"href":657,"rel":658},"https:\u002F\u002Fowasp.org\u002Fwww-community\u002FThreat_Modeling",[55],"Threat Model",". Knecht läuft bei einer Agentur, für ihre eigenen Projekte, auf ihrem eigenen Server. Der Angreifer, vor dem die Sandbox zusätzlich geschützt hätte, braucht einen Kernel-Exploit und landet dann auf einem Server, auf dem ohnehin nur die eigenen Projekte liegen. Für dieses Risiko 3 GB statt 180 MB pro Preview zu bezahlen, ist der falsche Tausch.",[19,662,664],{"id":663},"das-ergebnis","Das Ergebnis",[10,666,667],{},"Beide Varianten liefen auf derselben Dev-Maschine, gemessen im Juli 2026:",[669,670,671,686],"table",{},[672,673,674],"thead",{},[675,676,677,680,683],"tr",{},[678,679],"th",{},[678,681,682],{},"Sandbox pro Run (Sysbox)",[678,684,685],{},"DDEV auf dem Host (heute)",[687,688,689,701,712,723,734],"tbody",{},[675,690,691,695,698],{},[692,693,694],"td",{},"RAM pro laufender Preview",[692,696,697],{},"~3 GB",[692,699,700],{},"~180 MB",[675,702,703,706,709],{},[692,704,705],{},"DDEV-Images",[692,707,708],{},"pro Sandbox, 1-2 GB extra",[692,710,711],{},"einmal pro Host",[675,713,714,717,720],{},[692,715,716],{},"Gestoppte Preview wecken",[692,718,719],{},"Minuten, kompletter Sandbox-Boot",[692,721,722],{},"Sekunden",[675,724,725,728,731],{},[692,726,727],{},"Docker-Zugriff im Run",[692,729,730],{},"eigener Daemon in der Sandbox",[692,732,733],{},"keiner, Knecht steuert von außen",[675,735,736,739,742],{},[692,737,738],{},"Isolationsgrenze",[692,740,741],{},"eigener Daemon plus User Namespaces",[692,743,744],{},"eigenes Netz, Limits, kein Socket",[10,746,747],{},"Dazu kommen noch zwei kleinere Optimierungen.",[139,749,750,758],{},[142,751,752,753,70],{},"Die Datenbank läuft für die Preview mit einer sparsameren Konfiguration. Die MySQL-Config, die DDEV mitliefert, erlaubt jeder Datenbank rund 1 GB ",[51,754,757],{"href":755,"rel":756},"https:\u002F\u002Fmariadb.com\u002Fkb\u002Fen\u002Finnodb-buffer-pool\u002F",[55],"Puffer",[142,759,760],{},"Eine gestoppte Preview kostet 0 RAM, übrig bleiben nur Datenbank-Volume und Code.",[10,762,763],{},"Aus \"ein paar Previews pro Server\" wurde damit \"praktisch alle Projekte einer Agentur gleichzeitig\".",[19,765,767],{"id":766},"was-wir-mitnehmen","Was wir mitnehmen",[10,769,770],{},"Der letzte Post begann mit dem Satz, dass wir die riskanteste Annahme zuerst testen. Das war richtig, und es hat funktioniert, die Sandbox lief. Nur war \"läuft es?\" die falsche letzte Frage. Die vollständige lautet \"läuft es zu Kosten, die zum Ziel passen?\", und die lässt sich erst beantworten, wenn das System unter echter, paralleler Last steht. Diese Messung machen wir jetzt früher.",{"title":290,"searchDepth":291,"depth":291,"links":772},[773,774,775,779,780,781],{"id":520,"depth":291,"text":521},{"id":550,"depth":291,"text":551},{"id":580,"depth":291,"text":581,"children":776},[777,778],{"id":584,"depth":297,"text":585},{"id":604,"depth":297,"text":605},{"id":635,"depth":291,"text":636},{"id":663,"depth":291,"text":664},{"id":766,"depth":291,"text":767},"2026-07-26","Die Sandbox pro Run hat funktioniert und kostete 3 GB RAM pro Preview. Warum wir sie wieder ausgebaut haben und wie die alten Probleme heute gelöst sind.",{},"\u002Fupdates\u002Fsandbox-rollback",{"title":490,"description":783},"updates\u002Fsandbox-rollback","Architektur","JbZXDKJUzyiwV9FB1n6k0GV-6CWeyKheBZA-zRFd_fk",{"id":791,"title":792,"body":793,"date":1000,"description":1001,"extension":310,"meta":1002,"navigation":312,"path":329,"seo":1003,"stem":1004,"tag":1005,"__hash__":1006},"updates\u002Fupdates\u002Fworkflow-engine.md","DDEV-Projekte automatisieren mit Workflows",{"type":7,"value":794,"toc":989},[795,798,805,809,827,830,838,845,849,852,878,885,889,898,904,911,915,918,926,930,933,953,956,960,963,969,972,976,979,982,986],[10,796,797],{},"Ein Workflow ist eine Liste von Schritten, die Knecht automatisch für ein Projekt ausführt. Zum Beispiel Projekt starten, Composer-Pakete updaten, Tests laufen lassen und einen Pull Request öffnen. Die letzten Wochen sind fast komplett in diese Engine geflossen. Hier steht, was sie kann, was die AI-Action von einem Chat unterscheidet und was noch fehlt.",[10,799,800],{},[801,802],"img",{"alt":803,"src":804},"Der Workflow-Editor mit Trigger, Schritten und der Action-Bibliothek","\u002Fassets\u002Fworkflow-detail.png",[19,806,808],{"id":807},"wie-ein-workflow-läuft","Wie ein Workflow läuft",[10,810,811,812,815,816,819,820,823,824,70],{},"Wird ein Workflow gestartet, entsteht ein ",[59,813,814],{},"Run",", eine einzelne Ausführung auf einem Projekt. Jeder Run bekommt eine eigene, abgeschottete Umgebung, die ",[59,817,818],{},"Sandbox",". Darin läuft das Projekt komplett, der Code frisch von GitHub, die Datenbank eingespielt, ",[51,821,535],{"href":533,"rel":822},[55]," gestartet. Das Live-System und andere Runs bekommen davon nichts mit. Warum jeder Run seine eigene Sandbox bekommt, steht im ",[51,825,826],{"href":500},"Architektur-Post",[10,828,829],{},"Die Schritte laufen nacheinander, und jeder hinterlässt Ergebnisse, die spätere Schritte über Platzhalter einsetzen.",[379,831,836],{"className":832,"code":834,"language":835},[833],"language-text","{{ steps.run_tests.stdout }}      die Ausgabe des Schritts \"Run tests\"\n{{ inputs.title }}                der Titel des Tickets, das den Workflow gestartet hat\n","text",[14,837,834],{"__ignoreMap":290},[10,839,840,841,844],{},"Auswendig wissen muss man das nicht. Wer ",[14,842,843],{},"{{ "," tippt, bekommt eine Liste mit allem, was an dieser Stelle verfügbar ist. Und nach jedem Schritt speichert Knecht, was passiert ist. Deshalb sieht man live, wo ein Run steht, und ein fehlgeschlagener Run lässt sich genau ab dem Schritt fortsetzen, an dem er hängen blieb.",[19,846,848],{"id":847},"die-actions","Die Actions",[10,850,851],{},"Jeder Schritt führt eine Action aus. Grob gibt es drei Sorten.",[853,854,855,866,872],"field-group",{},[856,857,859],"field",{"name":858},"Deterministische Actions",[10,860,861,862,865],{},"Erledigen genau eine Sache, immer gleich. Sie booten das Projekt, führen einen Shell-Befehl wie ",[14,863,864],{},"composer update"," aus, lassen eigenen JavaScript-Code laufen, rufen eine externe URL auf, prüfen die ganze Sitemap auf kaputte Links oder bringen die Änderungen als Pull Request zurück nach GitHub.",[856,867,869],{"name":868},"Kontrollfluss-Actions",[10,870,871],{},"If\u002Felse führt Schritte nur unter einer Bedingung aus, zum Beispiel \"wenn die Tests fehlgeschlagen sind\". Loop wiederholt Schritte für jeden Eintrag einer Liste.",[856,873,875],{"name":874},"AI-Action",[10,876,877],{},"Lässt einen Agenten im laufenden Projekt arbeiten, dazu gleich mehr.",[10,879,880,881,884],{},"Weil nicht jeder Fehler ein echter Fehler ist, hat jeder Schritt seine eigene Fehler-Policy. Wackelige Befehle bekommen Retries mit Backoff, und mit ",[14,882,883],{},"continueOnError"," läuft der Run trotz Fehler weiter. Die Bibliothek wächst gerade noch, einen teil der aktuellen Actions können im oberen Screenshot angesehen werden.",[79,886,888],{"id":887},"ein-agent-kein-chat","Ein Agent, kein Chat",[10,890,891,892,897],{},"Die AI-Action startet ",[51,893,896],{"href":894,"rel":895},"https:\u002F\u002Fopencode.ai",[55],"opencode"," mit eurem eigenen API-Key direkt in der Sandbox. Der Agent arbeitet im echten, laufenden Projekt, er liest Code, ändert Dateien und führt Befehle aus. Ein Chat sagt dir, wie ein Fix aussehen könnte. Der Agent wendet ihn an und probiert aus, ob er funktioniert.",[10,899,900],{},[801,901],{"alt":902,"src":903},"Die AI-Action mit Prompt, festem Output-Format und den verfügbaren Variablen","\u002Fassets\u002Fworkflow-ai-action.png",[10,905,906,907,910],{},"Statt Fließtext kann der Agent feste Output-Felder wie ",[14,908,909],{},"prTitle"," liefern, als geprüftes JSON für spätere Schritte. Und er ist nach dem Run nicht weg. Ein Chat auf der Run-Seite setzt dieselbe Session fort, \"Mach den Button noch blau\" braucht keinen neuen Run.",[79,912,914],{"id":913},"sicher-eingesperrt","Sicher eingesperrt",[10,916,917],{},"Alles, was ein Workflow ausführt (Shell-Befehle, eigener Code, der Agent), läuft ausschließlich in der Sandbox, nie auf dem Server selbst.",[10,919,920,921,925],{},"Auch Projekt-Secrets bleiben, wo sie hingehören. API-Keys und Tokens liegen ",[51,922,924],{"href":923},"\u002Fupdates\u002Fgh-auth-evolution","verschlüsselt"," in der Datenbank. Die Preview-URLs sind ebenfalls nicht öffentlich, denn wer eine öffnet, braucht einen Knecht-Login.",[19,927,929],{"id":928},"die-trigger","Die Trigger",[10,931,932],{},"Ein Trigger legt fest, wann ein Workflow von selbst startet. Das ist der Punkt, an dem aus einem Werkzeug Automatisierung wird, denn niemand muss mehr daran denken, den Ablauf anzustoßen. Der Workflow läuft, sobald seine Bedingung eintritt, auch nachts und am Wochenende. Aktuell gibt es drei Arten.",[853,934,935,941,947],{},[856,936,938],{"name":937},"Manuell",[10,939,940],{},"Per Klick im Dashboard oder als Test-Run direkt im Editor.",[856,942,944],{"name":943},"Integrationen",[10,945,946],{},"Knecht reagiert auf Events aus GitHub und Jira. Bekommt ein Jira-Ticket zum Beispiel das Label \"knecht\", startet der Workflow, und der fertige PR-Link landet als Kommentar auf dem Ticket.",[856,948,950],{"name":949},"Cron",[10,951,952],{},"Nach Zeitplan, zum Beispiel jeden Montag um 6 Uhr die Security-Updates prüfen.",[10,954,955],{},"Ein Trigger kann mehrere Projekte anhängen und startet für jedes einen eigenen Run.",[19,957,959],{"id":958},"nach-dem-start","Nach dem Start",[10,961,962],{},"Bevor ein Trigger scharf geschaltet wird, lässt sich der Workflow direkt im Editor testen, gegen ein echtes Projekt in einer eigenen Sandbox. Für Test-Runs gilt dabei dasselbe wie für echte.",[10,964,965],{},[801,966],{"alt":967,"src":968},"Ein Test-Run direkt im Editor, mit Live-Log und laufendem Schritt","\u002Fassets\u002Fworkflow-test-run.png",[10,970,971],{},"Innerhalb eines Runs läuft alles bewusst nacheinander, parallele Zweige gibt es nicht. Ein Run bleibt eine lineare, nachvollziehbare Kette. Wie viele Runs gleichzeitig laufen, ist konfigurierbar, alles darüber wartet in der Queue.",[19,973,975],{"id":974},"workflows-als-code","Workflows als Code",[10,977,978],{},"Jeder Workflow lässt sich als YAML oder JSON exportieren und wieder importieren. Damit wandert ein Workflow ins Git-Repo, bekommt Reviews wie jeder andere Code und lässt sich zwischen Projekten oder Instanzen kopieren. Wer lieber im Texteditor arbeitet, schreibt die Datei direkt und importiert sie fertig.",[10,980,981],{},"Das Format öffnet auch die Tür für später. Ein Workflow, der sich bei einer Agentur bewährt hat, funktioniert bei der nächsten genauso, weil er nur aus Schritten und Platzhaltern besteht. Denkbar ist ein Workflow-Store, aus dem man sich fertige Abläufe wie Security-Updates oder Link-Checks direkt installiert, statt sie selbst zusammenzuklicken.",[19,983,985],{"id":984},"was-noch-fehlt","Was noch fehlt",[10,987,988],{},"Ein Benachrichtigungssystem gibt es noch nicht. Schlägt ein Run fehl, sieht man das bisher nur im Dashboard. Wer es sofort wissen will, ruft mit dem HTTP-Schritt am Ende des Workflows einen Slack-Webhook auf. Auch ein hartes Zeitlimit pro Run fehlt noch, ein hängender Schritt läuft, bis man ihn abbricht.",{"title":290,"searchDepth":291,"depth":291,"links":990},[991,992,996,997,998,999],{"id":807,"depth":291,"text":808},{"id":847,"depth":291,"text":848,"children":993},[994,995],{"id":887,"depth":297,"text":888},{"id":913,"depth":297,"text":914},{"id":928,"depth":291,"text":929},{"id":958,"depth":291,"text":959},{"id":974,"depth":291,"text":975},{"id":984,"depth":291,"text":985},"2026-07-13","Automatisierung für DDEV-Projekte. Knecht führt Composer-Updates und Tests in einer Sandbox aus und öffnet Pull Requests. Mit AI-Agent, der Bugs im Projekt fixt.",{},{"title":792,"description":1001},"updates\u002Fworkflow-engine","Engine","Qhlo7ao_IaLkW6HlMao5yttvQWI5aT9rDw8v5Jl7EyM",{"id":1008,"title":1009,"body":1010,"date":1249,"description":1250,"extension":310,"meta":1251,"navigation":312,"path":923,"seo":1252,"stem":1253,"tag":1254,"__hash__":1255},"updates\u002Fupdates\u002Fgh-auth-evolution.md","GitHub Auth Evolution",{"type":7,"value":1011,"toc":1241},[1012,1015,1018,1022,1043,1046,1050,1056,1073,1077,1090,1119,1125,1135,1139,1142,1154,1163,1166,1169,1173,1184,1193,1208,1213,1220,1223,1236,1238],[10,1013,1014],{},"Der GitHub-Login funktionierte vom ersten Tag an. Trotzdem haben wir ihn dreimal umgebaut.",[10,1016,1017],{},"Der Grund: Hinter \"mit GitHub verbinden\" stecken drei getrennte Fragen, die gerne vermischt werden. Wer sitzt vor dem Dashboard? Womit klont der Server Repos und öffnet PRs? Und wer darf diese Instanz überhaupt benutzen? Am Anfang haben wir alle drei mit einem einzigen Token beantwortet. Hier der Weg von dort bis zum heutigen Stand, inklusive der GitHub-Eigenheiten, über die wir dabei gestolpert sind.",[19,1019,1021],{"id":1020},"was-ist-eigentlich-eine-oauth-app","Was ist eigentlich eine OAuth App?",[10,1023,1024,1025,1030,1031,1034,1035,1038,1039,1042],{},"Eine OAuth App ist der klassische \"Sign in with GitHub\"-Button. Der User wird zu GitHub geschickt, bestätigt dort den Zugriff und kommt mit einem Access Token zurück. Was dieser Token darf, bestimmen die ",[51,1026,1029],{"href":1027,"rel":1028},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fapps\u002Foauth-apps\u002Fbuilding-oauth-apps\u002Fscopes-for-oauth-apps",[55],"Scopes",", die die App anfragt. ",[14,1032,1033],{},"read:user"," heißt: nur das Profil lesen. ",[14,1036,1037],{},"repo"," heißt: voller Lese- und Schreibzugriff auf ",[59,1040,1041],{},"alle"," Repos, die der User erreichen kann.",[10,1044,1045],{},"Und genau da fing unser Problem an.",[19,1047,1049],{"id":1048},"stufe-1-ein-token-für-alles","Stufe 1: Ein Token für alles",[10,1051,1052,1053,1055],{},"Die erste Version von Knecht war eine OAuth App mit ",[14,1054,1037],{},"-Scope. Der Token, der den Login bestätigt hat, war gleichzeitig das Credential für alle Git-Operationen. Kein zweites Secret, nichts einzurichten. Praktisch, aber zweifach falsch:",[139,1057,1058,1067],{},[142,1059,1060,1063,1064,1066],{},[59,1061,1062],{},"Der Scope ist zu grob."," Ein ",[14,1065,1037],{},"-Token kann alles, was der User kann. In jedem seiner Repos, nicht nur in den Projekten, die Knecht betreut. Feiner geht es bei OAuth-Scopes nicht.",[142,1068,1069,1072],{},[59,1070,1071],{},"Das Credential hängt an einer Person."," Rotiert der User seinen Token oder verlässt die Agentur, kommt der Server an kein Repo mehr. Server-Infrastruktur, die auf dem Account eines Mitarbeiters steht, ist eine tickende Uhr.",[19,1074,1076],{"id":1075},"was-eine-github-app-anders-macht","Was eine GitHub App anders macht",[10,1078,1079,1080,1083,1084,1089],{},"Für Stufe 2 haben wir den Repo-Zugriff auf eine ",[59,1081,1082],{},"GitHub App"," umgestellt. Der ",[51,1085,1088],{"href":1086,"rel":1087},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fapps\u002Foauth-apps\u002Fbuilding-oauth-apps\u002Fdifferences-between-github-apps-and-oauth-apps",[55],"Unterschied zur OAuth App"," ist grundlegend:",[139,1091,1092,1099,1106],{},[142,1093,1094,1095,1098],{},"Eine GitHub App ist ein ",[59,1096,1097],{},"eigener Akteur"," mit eigener Identität, nicht ein User, der seine Rechte verleiht.",[142,1100,1101,1102,1105],{},"Sie wird auf ausgewählten Repos ",[59,1103,1104],{},"installiert",". Der Zugriff ist auf genau diese Repos begrenzt.",[142,1107,1108,1109,1112,1113,1118],{},"Sie authentifiziert sich mit ihrem ",[59,1110,1111],{},"Private Key"," und erzeugt daraus ",[51,1114,1117],{"href":1115,"rel":1116},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fapps\u002Fcreating-github-apps\u002Fauthenticating-with-a-github-app\u002Fgenerating-an-installation-access-token-for-a-github-app",[55],"Installation Tokens",": gültig für etwa eine Stunde, gescoped auf die installierten Repos. Läuft einer ab, wird einfach der nächste erzeugt.",[10,1120,1121,1122,1124],{},"Der Login blieb OAuth, aber mit geschrumpftem Scope: ",[14,1123,1033],{},". Der Token wird einmal benutzt, um das Profil zu laden, und danach verworfen. Er wird nie gespeichert. Identität und Repo-Zugriff waren damit sauber getrennt.",[10,1126,1127,1128,1130,1131,1134],{},"Nur das Setup war eine Zumutung. Der Betreiber musste eine OAuth App ",[59,1129,61],{}," eine GitHub App von Hand anlegen, vier Secrets in die ",[14,1132,1133],{},".env"," kopieren, den Private Key base64-encoden und Callback-URLs eintragen. Bevor irgendetwas lief.",[19,1136,1138],{"id":1137},"stufe-3-der-manifest-flow","Stufe 3: Der Manifest Flow",[10,1140,1141],{},"Dann fiel uns auf: Eine GitHub App bringt ihren eigenen OAuth-Client schon mit. Eine einzige App kann also beides, Login und Repo-Zugriff. Die separate OAuth App war überflüssig.",[10,1143,1144,1145,1150,1151,1153],{},"Und GitHub hat für genau diesen Fall den ",[51,1146,1149],{"href":1147,"rel":1148},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fapps\u002Fsharing-github-apps\u002Fregistering-a-github-app-from-a-manifest",[55],"App Manifest Flow",": Eine Software beschreibt die gewünschte App als JSON (Name, Callback-URLs, Permissions), schickt das an GitHub, GitHub legt die App an und gibt ",[59,1152,1041],{}," Credentials zurück.",[10,1155,1156,1157,1162],{},"Bei Knecht sieht das so aus: Beim ersten Start zeigt das Dashboard einen Button. Ein Klick, GitHub erstellt die App, Knecht speichert die Credentials verschlüsselt in der eigenen Datenbank. AES-256-GCM, der Schlüssel wird per ",[51,1158,1161],{"href":1159,"rel":1160},"https:\u002F\u002Fdatatracker.ietf.org\u002Fdoc\u002Fhtml\u002Frfc5869",[55],"HKDF"," aus dem Session-Passwort abgeleitet, das ohnehin gesetzt sein muss. Die Verschlüsselung kostet also keine neue Env-Variable.",[10,1164,1165],{},"Übrig bleiben genau zwei Dinge, die der Betreiber liefert: das Session-Passwort und die Base URL.",[10,1167,1168],{},"Ein Trade-off gehört dazu: Wer das Session-Passwort rotiert, macht die gespeicherten Credentials unlesbar. Die Lösung ist dann nicht Entschlüsseln, sondern das Setup einmal neu durchlaufen.",[19,1170,1172],{"id":1171},"stufe-4-wer-darf-überhaupt-rein","Stufe 4: Wer darf überhaupt rein?",[10,1174,1175,1176,1179,1180,1183],{},"Ein Loch blieb bis zuletzt offen: Der Login prüfte ",[59,1177,1178],{},"Identität",", aber keine ",[59,1181,1182],{},"Erlaubnis",". Jeder GitHub-Account, der den OAuth-Flow abschließt, bekam eine Session.",[10,1185,1186,1187,1192],{},"Ob das ausnutzbar ist, hängt an einer GitHub-Eigenheit, die man einmal verstanden haben muss. Eine App ist entweder ",[51,1188,1191],{"href":1189,"rel":1190},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fapps\u002Fcreating-github-apps\u002Fsetting-up-a-github-app\u002Fmaking-a-github-app-public-or-private",[55],"privat oder öffentlich",":",[139,1194,1195,1202],{},[142,1196,1197,1198,1201],{},"Eine ",[59,1199,1200],{},"private"," GitHub App zeigt die Authorize-Seite nur ihrem Besitzer (oder Mitgliedern der besitzenden Org). Alle anderen sehen einen 404. Das wirkt wie eingebaute Zugriffskontrolle, macht aber das Einladen externer Teammitglieder unmöglich. Sie kommen nicht einmal bis zur Zustimmungsseite.",[142,1203,1197,1204,1207],{},[59,1205,1206],{},"öffentliche"," App lässt jeden GitHub-User durch den Identity-Flow. Nur so lassen sich beliebige Teammitglieder onboarden. Dafür gated GitHub dann gar nichts mehr.",[34,1209,1210],{"color":36,"icon":278},[10,1211,1212],{},"Die Sichtbarkeit einer GitHub App ist keine Zugriffskontrolle. Privat sperrt die Falschen aus, öffentlich lässt alle rein. Wer entscheiden will, wer sich einloggen darf, muss das selbst tun.",[10,1214,1215,1216,1219],{},"Also ist Knechts App öffentlich, und die Erlaubnis liegt bei Knecht selbst: eine ",[59,1217,1218],{},"Allowlist",". Eine simple Members-Tabelle mit GitHub-Logins. Beim Setup wird der Ersteller automatisch als Owner eingetragen, alle weiteren werden über die Settings eingeladen. Wer nicht auf der Liste steht, wird nach dem GitHub-Login abgewiesen.",[10,1221,1222],{},"\"Öffentlich\" heißt übrigens nur, dass Fremde die App auf ihren eigenen Repos installieren könnten. Das ist harmlos: Knecht benutzt ausschließlich Installationen, von denen er weiß.",[10,1224,1225,1226,1231,1232,1235],{},"Ein Detail noch, weil es oft übersehen wird: Eine Session ist typischerweise ein Cookie, das bis zu seinem Ablauf gültig bleibt. Wer nur beim Login prüft, hat entfernte Mitglieder also noch tagelang im System, die ",[51,1227,1230],{"href":1228,"rel":1229},"https:\u002F\u002Fcheatsheetseries.owasp.org\u002Fcheatsheets\u002FSession_Management_Cheat_Sheet.html",[55],"OWASP nennt das fehlende serverseitige Invalidierung",". Knecht prüft die Mitgliedschaft deshalb bei ",[59,1233,1234],{},"jedem"," Request neu. Wird jemand entfernt, ist er beim nächsten Klick draußen.",[19,1237,985],{"id":984},[10,1239,1240],{},"Rollen. Aktuell hat jedes Mitglied vollen Zugriff, inklusive Einladen. Für die kleinen Teams, für die Knecht gerade gebaut wird, reicht das. Feinere Rechte kommen, wenn echte Rollen gebraucht werden.",{"title":290,"searchDepth":291,"depth":291,"links":1242},[1243,1244,1245,1246,1247,1248],{"id":1020,"depth":291,"text":1021},{"id":1048,"depth":291,"text":1049},{"id":1075,"depth":291,"text":1076},{"id":1137,"depth":291,"text":1138},{"id":1171,"depth":291,"text":1172},{"id":984,"depth":291,"text":985},"2026-07-06","Ein Deep Dive in Knechts GitHub-Anbindung. Warum ein OAuth-Token nicht reicht, was eine GitHub App anders macht und wieso wir die Zugriffskontrolle am Ende selbst bauen mussten.",{},{"title":1009,"description":1250},"updates\u002Fgh-auth-evolution","Security","4RIxIoUrhvCeeUuytkSovF9UVCPEK_n7EkF2isGpmm8",{"id":1257,"title":501,"body":1258,"date":1537,"description":1538,"extension":310,"meta":1539,"navigation":312,"path":500,"seo":1540,"stem":1541,"tag":788,"__hash__":1542},"updates\u002Fupdates\u002Fdashboard-architecture.md",{"type":7,"value":1259,"toc":1516},[1260,1281,1284,1288,1299,1314,1318,1363,1366,1370,1373,1377,1389,1393,1400,1404,1407,1410,1414,1425,1429,1453,1457,1476,1480,1493,1497,1506,1510,1513],[366,1261,1262,1274],{},[10,1263,1264,1265,1269,1270,70],{},"Dieser Post ist ein Blick unter die Haube. Die Container-Architektur ist der Unterbau, damit parallele Previews mit DDEV-Projekten funktionieren, nicht das Produkt selbst. Was Knecht macht und für wen er gedacht ist, steht auf der ",[51,1266,1268],{"href":1267},"\u002F","Startseite"," und in ",[51,1271,1273],{"href":1272},"\u002Fupdates\u002Fwas-macht-knecht","Was macht der Knecht",[10,1275,1276,1277,1280],{},"Update, Juli 2026: Wir haben die Sysbox-Sandbox gebaut, und sie lief. Trotzdem haben wir sie wieder ausgebaut. Warum, steht in der Fortsetzung ",[51,1278,1279],{"href":785},"Zurück zum geteilten Daemon",". Der Weg zur Entscheidung unten bleibt trotzdem lesenswert, die Fortsetzung baut darauf auf.",[10,1282,1283],{},"Knecht läuft selbst als Docker Container, muss aber DDEV-Projekte hochfahren, die wieder aus Containern bestehen. Ob das überhaupt funktioniert, war die riskanteste Annahme der ganzen Architektur. Deshalb haben wir sie zuerst getestet, noch bevor der eigentliche Code entsteht.",[19,1285,1287],{"id":1286},"der-ansatz","Der Ansatz",[10,1289,1290,1291,1294,1295,1298],{},"Kurz zum Hintergrund: Docker besteht aus zwei Teilen. Der ",[51,1292,516],{"href":514,"rel":1293},[55]," ist der Dienst, der Container tatsächlich erstellt und verwaltet. Die Docker CLI ist nur ein Client, der über einen Unix-Socket (",[14,1296,1297],{},"\u002Fvar\u002Frun\u002Fdocker.sock",") mit dem Daemon spricht. Wer den Socket hat, steuert den Daemon.",[10,1300,1301,1302,1307,1308,1313],{},"Genau das nutzt Knecht: Der Container startet keinen eigenen Daemon, sondern mountet den Socket des Hosts und steuert damit den Host-Daemon. Das Muster heißt ",[51,1303,1306],{"href":1304,"rel":1305},"https:\u002F\u002Fwww.avonture.be\u002Fblog\u002Fdocker-out-of-docker-dood\u002F",[55],"Docker-out-of-Docker"," (DooD). Die DDEV-Container laufen dann als Geschwister neben dem Knecht-Container auf dem Host, nicht verschachtelt in ihm. ",[51,1309,1312],{"href":1310,"rel":1311},"https:\u002F\u002Fcoolify.io",[55],"Coolify"," und viele CI-Runner arbeiten genauso.",[19,1315,1317],{"id":1316},"der-test","Der Test",[75,1319,1320,1324,1330,1334,1341,1345,1348,1352],{"level":77},[79,1321,1323],{"id":1322},"host-daemon-erreichbar","Host-Daemon erreichbar",[10,1325,1326,1329],{},[14,1327,1328],{},"docker ps"," im Container zeigt die Host-Container. Der Socket-Mount funktioniert.",[79,1331,1333],{"id":1332},"ddev-projekt-bootet","DDEV-Projekt bootet",[10,1335,1336,1337,1340],{},"Ein minimales PHP-Projekt am festen Pfad, dann ",[14,1338,1339],{},"ddev start",". DDEV startet Webserver und Datenbank auf dem Host.",[79,1342,1344],{"id":1343},"container-sind-host-geschwister","Container sind Host-Geschwister",[10,1346,1347],{},"Auf dem Host tauchen die Projekt-Container als normale Nachbarn des Knecht-Containers auf.",[79,1349,1351],{"id":1350},"app-erreichbar","App erreichbar",[10,1353,1354,1355,1358,1359,1362],{},"Das gebootete Projekt antwortet. Wichtig dabei: ",[14,1356,1357],{},"*.ddev.site"," löst per DNS immer auf ",[14,1360,1361],{},"127.0.0.1"," auf, also auf den Container selbst. Der Preview-Proxy muss den Web-Container deshalb direkt über das Docker-Netzwerk ansprechen.",[10,1364,1365],{},"Damit ist die Basis bewiesen: Ein Prozess im Knecht-Container kann echte DDEV-Stacks booten, mit korrekt gemountetem Source Code.",[19,1367,1369],{"id":1368},"zwei-probleme-sind-geblieben","Zwei Probleme sind geblieben",[10,1371,1372],{},"Der Test hat aber auch die Grenzen des Ansatzes gezeigt.",[79,1374,1376],{"id":1375},"keine-isolation-zwischen-runs","Keine Isolation zwischen Runs",[10,1378,1379,1380,1384,1385,1388],{},"Alles läuft auf einem gemeinsamen Daemon. Knecht soll aber mehrere Previews parallel fahren, ohne dass sich Runs gegenseitig stören. Wir mussten sogar den ",[51,1381,1383],{"href":562,"rel":1382},[55],"DDEV-Router"," weglassen. Das ist der Reverse Proxy, den DDEV vor alle Projekte stellt und der die Ports 80 und 443 auf dem Host bindet. Feste Host-Ports gibt es nur einmal, bei vielen parallelen Projekten kollidiert das. Ohne Router muss Knecht in die ",[14,1386,1387],{},".ddev\u002Fconfig.yaml"," eingreifen und zum Beispiel dir Urls überschreiben.",[79,1390,1392],{"id":1391},"fremder-code-am-host-socket","Fremder Code am Host-Socket",[10,1394,1395,1396,1399],{},"Der Agent führt Code aus, und wir kontrollieren nicht vollständig, was er tut. Ein Agent lässt sich über ",[51,1397,600],{"href":598,"rel":1398},[55]," kapern, etwa durch manipulierte Inhalte im Repo oder in einer Datenquelle, und macht dann nicht mehr das, was wir wollten. Wer an dieser Stelle an den Host-Socket kommt, steuert den Daemon, und wer den Daemon steuert, kann jeden Container mit jedem Mount starten. Das ist faktisch Root auf dem Server.",[19,1401,1403],{"id":1402},"die-lösung-eine-sandbox-pro-run","Die Lösung: eine Sandbox pro Run",[10,1405,1406],{},"Beide Probleme haben dieselbe Antwort: Jeder Run bekommt seinen eigenen Docker Daemon in einer eigenen Sandbox. Damit verschwindet auch der Grund, den Router wegzulassen. Port 80 in Sandbox A kann nicht mit Sandbox B kollidieren, weil jede Sandbox ihren eigenen Netzwerk-Namespace hat. Projekte laufen also wieder mit allen ihren URLs und Settings.",[10,1408,1409],{},"Bleibt die Frage, was die Sandbox ist. Wir haben uns die üblichen Kandidaten angesehen.",[79,1411,1413],{"id":1412},"privileged-docker-in-docker","Privileged Docker-in-Docker",[10,1415,1416,1417,1424],{},"Ein zweiter Daemon in einem Container mit ",[51,1418,1421],{"href":1419,"rel":1420},"https:\u002F\u002Fdocs.docker.com\u002Fengine\u002Fcontainers\u002Frun\u002F#runtime-privilege-and-linux-capabilities",[55],[14,1422,1423],{},"--privileged",". Die Flag schaltet praktisch alle Schutzmechanismen ab und gibt dem Container Zugriff auf die Host-Geräte. Eine bösartige Dependency kommt damit ohne Exploit auf den Host. Fällt raus.",[79,1426,1428],{"id":1427},"kata-containers","Kata Containers",[10,1430,1431,1436,1437,1442,1443,1446,1447,1452],{},[51,1432,1435],{"href":1433,"rel":1434},"https:\u002F\u002Fkatacontainers.io",[55],"Kata"," startet jeden Container in einer leichtgewichtigen VM mit eigenem Kernel. Das ist die stärkste Grenze und der Ansatz hinter ",[51,1438,1441],{"href":1439,"rel":1440},"https:\u002F\u002Ffirecracker-microvm.github.io",[55],"Firecracker",", auf dem etwa AWS Lambda läuft. Kata braucht aber ",[14,1444,1445],{},"\u002Fdev\u002Fkvm",", und normale Cloud-VPS bieten ",[51,1448,1451],{"href":1449,"rel":1450},"https:\u002F\u002Flowendtalk.com\u002Fdiscussion\u002F154261\u002Fsearching-vps-for-nested-virtualization",[55],"keine Nested Virtualization",". Das würde Bare-Metal erzwingen und kollidiert mit dem Ziel, Knecht auf einem normalen VPS zu installieren. Fällt raus.",[79,1454,1456],{"id":1455},"gvisor","gVisor",[10,1458,1459,1463,1464,1469,1470,1475],{},[51,1460,1456],{"href":1461,"rel":1462},"https:\u002F\u002Fgvisor.dev",[55]," fängt Syscalls in einem Userspace-Kernel ab und braucht kein KVM. Es isoliert Anwendungsprozesse gut, tut sich aber schwer damit, selbst ein Docker-Host zu sein: ",[51,1465,1468],{"href":1466,"rel":1467},"https:\u002F\u002Fgithub.com\u002Fgoogle\u002Fgvisor\u002Fissues\u002F9917",[55],"iptables wird nur teilweise unterstützt"," und ",[51,1471,1474],{"href":1472,"rel":1473},"https:\u002F\u002Fgithub.com\u002Fgoogle\u002Fgvisor\u002Fissues\u002F12475",[55],"Overlayfs bricht mit Docker v29",". Genau ein Docker-Host ist aber, was DDEV braucht. Fällt raus.",[79,1477,1479],{"id":1478},"unsere-wahl-sysbox","Unsere Wahl: Sysbox",[10,1481,1482,1485,1486,1488,1489,1492],{},[51,1483,510],{"href":508,"rel":1484},[55]," ist eine Container-Runtime, die Docker-in-Docker ohne ",[14,1487,1423],{}," erlaubt. Sie nutzt dafür ",[51,1490,644],{"href":642,"rel":1491},[55],", Root im Container wird auf einen unprivilegierten User auf dem Host gemappt. Sysbox ist genau für diesen Fall gebaut, braucht kein KVM und läuft damit auf jedem normalen Linux-Server.",[19,1494,1496],{"id":1495},"das-threat-model-entscheidet","Das Threat Model entscheidet",[10,1498,1499,1500,1505],{},"Den Ausschlag gibt am Ende nicht die Runtime, sondern das Threat Model, also die Frage, wer hier eigentlich angreift und was er erreicht. Knecht ist ",[51,1501,1504],{"href":1502,"rel":1503},"https:\u002F\u002Fwww.geeksforgeeks.org\u002Fsystem-design\u002Fsingle-tenant-vs-multi-tenant-architecture\u002F",[55],"Single-Tenant",": eine Agentur, ihre Projekte, ihr Server. Die VM-Grenze von Kata schützt vor allem davor, dass ein Kunde aus seiner Umgebung in die eines anderen ausbricht. Diesen Fall gibt es hier nicht.",[19,1507,1509],{"id":1508},"wie-es-weitergeht","Wie es weitergeht",[10,1511,1512],{},"Als Nächstes bauen wir die Sysbox-Sandbox pro Run (eigener Daemon, DDEV inklusive Router) und einen zentralen Ingress mit Auth-Gate, der Preview-URLs in die richtige Sandbox routet. Der nächste Meilenstein: DDEV mit Router in einer Sysbox-Sandbox auf einem echten Linux-Host, erreichbar vom Ingress.",[10,1514,1515],{},"Vielleicht gibt es auch schon den ersten Showcase vom Prototypen.",{"title":290,"searchDepth":291,"depth":291,"links":1517},[1518,1519,1525,1529,1535,1536],{"id":1286,"depth":291,"text":1287},{"id":1316,"depth":291,"text":1317,"children":1520},[1521,1522,1523,1524],{"id":1322,"depth":297,"text":1323},{"id":1332,"depth":297,"text":1333},{"id":1343,"depth":297,"text":1344},{"id":1350,"depth":297,"text":1351},{"id":1368,"depth":291,"text":1369,"children":1526},[1527,1528],{"id":1375,"depth":297,"text":1376},{"id":1391,"depth":297,"text":1392},{"id":1402,"depth":291,"text":1403,"children":1530},[1531,1532,1533,1534],{"id":1412,"depth":297,"text":1413},{"id":1427,"depth":297,"text":1428},{"id":1455,"depth":297,"text":1456},{"id":1478,"depth":297,"text":1479},{"id":1495,"depth":291,"text":1496},{"id":1508,"depth":291,"text":1509},"2026-07-01","Knecht kann aus einem Container heraus echte DDEV-Projekte booten. Was der Test gezeigt hat und warum jeder Run künftig seine eigene Sandbox bekommt.",{},{"title":501,"description":1538},"updates\u002Fdashboard-architecture","zoLqq8tyF4M2RyALIpQoii80fV5PXI7WBMOZPwynGHQ",{"id":4,"title":5,"body":1544,"date":308,"description":309,"extension":310,"meta":1723,"navigation":312,"path":313,"seo":1724,"stem":315,"tag":316,"__hash__":317},{"type":7,"value":1545,"toc":1708},[1546,1550,1552,1554,1558,1562,1564,1577,1579,1596,1598,1600,1605,1607,1613,1615,1636,1643,1645,1647,1654,1656,1658,1662,1664,1666,1668,1673,1689,1691,1693,1700,1704,1706],[10,1547,12,1548,17],{},[14,1549,16],{},[19,1551,22],{"id":21},[10,1553,25],{},[10,1555,28,1556,32],{},[14,1557,31],{},[34,1559,1560],{"color":36,"icon":37},[10,1561,40],{},[19,1563,44],{"id":43},[10,1565,1566,49,1568,57,1571,62,1573,66,1575,70],{},[14,1567,16],{},[51,1569,56],{"href":53,"rel":1570},[55],[59,1572,61],{},[14,1574,65],{},[14,1576,69],{},[10,1578,73],{},[75,1580,1581,1583,1585,1587,1589,1591],{"level":77},[79,1582,82],{"id":81},[10,1584,85],{},[79,1586,89],{"id":88},[10,1588,92],{},[79,1590,96],{"id":95},[10,1592,99,1593,105],{},[51,1594,104],{"href":102,"rel":1595},[55],[10,1597,108],{},[19,1599,112],{"id":111},[10,1601,115,1602,121],{},[51,1603,120],{"href":118,"rel":1604},[55],[10,1606,124],{},[19,1608,1609,131,1611],{"id":127},[14,1610,130],{},[14,1612,134],{},[10,1614,137],{},[139,1616,1617,1625],{},[142,1618,1619,148,1623,152],{},[59,1620,1621],{},[14,1622,31],{},[59,1624,151],{},[142,1626,1627,164,1634,168],{},[59,1628,1629],{},[51,1630,1632],{"href":159,"rel":1631},[55],[14,1633,163],{},[59,1635,167],{},[10,1637,171,1638,177,1641,180],{},[51,1639,176],{"href":174,"rel":1640},[55],[14,1642,134],{},[19,1644,184],{"id":183},[10,1646,187],{},[10,1648,190,1649,195,1652,199],{},[51,1650,194],{"href":53,"rel":1651},[55],[14,1653,198],{},[19,1655,203],{"id":202},[10,1657,206],{},[10,1659,209,1660,212],{},[14,1661,134],{},[10,1663,215],{},[10,1665,218],{},[19,1667,222],{"id":221},[10,1669,225,1670,231],{},[51,1671,230],{"href":228,"rel":1672},[55],[139,1674,1675,1679,1685],{},[142,1676,1677,239],{},[59,1678,238],{},[142,1680,1681,245,1683,249],{},[59,1682,244],{},[59,1684,248],{},[142,1686,252,1687,256],{},[59,1688,255],{},[19,1690,260],{"id":259},[10,1692,263],{},[10,1694,1695,269,1697,275],{},[59,1696,268],{},[51,1698,274],{"href":272,"rel":1699},[55],[34,1701,1702],{"color":36,"icon":278},[10,1703,281],{},[19,1705,285],{"id":284},[10,1707,288],{},{"title":290,"searchDepth":291,"depth":291,"links":1709},[1710,1711,1716,1717,1718,1719,1720,1721,1722],{"id":21,"depth":291,"text":22},{"id":43,"depth":291,"text":44,"children":1712},[1713,1714,1715],{"id":81,"depth":297,"text":82},{"id":88,"depth":297,"text":89},{"id":95,"depth":297,"text":96},{"id":111,"depth":291,"text":112},{"id":127,"depth":291,"text":302},{"id":183,"depth":291,"text":184},{"id":202,"depth":291,"text":203},{"id":221,"depth":291,"text":222},{"id":259,"depth":291,"text":260},{"id":284,"depth":291,"text":285},{},{"title":5,"description":309},{"id":1726,"title":1727,"body":1728,"date":1876,"description":1877,"extension":310,"meta":1878,"navigation":312,"path":1879,"seo":1880,"stem":1881,"tag":1882,"__hash__":1883},"updates\u002Fupdates\u002Fbeta-tester.md","Beta Tester gesucht",{"type":7,"value":1729,"toc":1867},[1730,1733,1745,1749,1755,1772,1775,1779,1782,1802,1805,1809,1818,1821,1825,1828,1832,1835,1839,1847,1851,1854,1864],[10,1731,1732],{},"Knecht läuft. Jetzt braucht er das, was kein lokaler Test ersetzen kann: echte Projekte, echte Bugs und ehrliches Feedback. Dafür suchen wir eine Handvoll Agenturen, die Knecht als Erste einsetzen wollen.",[366,1734,1735],{},[10,1736,1737,1740,1741,1269,1743,70],{},[59,1738,1739],{},"Neu hier?"," Knecht ist ein Dashboard auf deinem eigenen Server: Er bootet deine DDEV-Projekte als komplett lauffähige Umgebungen, erledigt Aufgaben darin über Workflows und liefert fertige Pull Requests mit Preview. Mehr dazu auf der ",[51,1742,1268],{"href":1267},[51,1744,1273],{"href":1272},[19,1746,1748],{"id":1747},"was-knecht-konkret-macht","Was Knecht konkret macht",[10,1750,1751,1752,1754],{},"Ein Beispiel aus dem Agentur-Alltag: Für ein Craft-CMS-Projekt erscheint ein Security-Patch. Um ihn einzuspielen, muss die Datenbank laufen, denn nach dem ",[14,1753,864],{}," laufen Migrations, die Files schreiben, und die müssen mit in den Commit. Dependabot kann das nicht, er hat kein laufendes Projekt. Knecht schon:",[1756,1757,1758,1761,1764,1769],"ol",{},[142,1759,1760],{},"Der Patch erscheint, ein Trigger startet deinen Update-Workflow.",[142,1762,1763],{},"Knecht bootet das Projekt per DDEV, inklusive Datenbank.",[142,1765,1766,1768],{},[14,1767,864],{}," läuft im echten Projekt, Migrations laufen, Tests laufen.",[142,1770,1771],{},"Du bekommst einen Pull Request mit Preview-Link. Prüfen, mergen, fertig.",[10,1773,1774],{},"Diesen Workflow baust du einmal. Danach läuft er für jedes deiner Projekte, bei jedem Patch.",[19,1776,1778],{"id":1777},"wen-wir-suchen","Wen wir suchen",[10,1780,1781],{},"Knecht entfaltet seinen Nutzen dort, wo viele Projekte gewartet werden müssen. Genau dafür ist das ideale Profil:",[139,1783,1784,1790,1796],{},[142,1785,1786,1789],{},[59,1787,1788],{},"Ihr arbeitet mit DDEV."," Eure Projekte sind bereits dafür konfiguriert oder lassen sich leicht darauf umstellen.",[142,1791,1792,1795],{},[59,1793,1794],{},"Ihr betreut mehrere Projekte."," Security-Updates und Bugfixes sind bei euch wiederkehrende Arbeit, kein Einzelfall.",[142,1797,1798,1801],{},[59,1799,1800],{},"Ihr habt eine verantwortliche Person."," Jemand, der Knecht ausprobiert und uns regelmäßig zurückspielt, was funktioniert und was nicht.",[10,1803,1804],{},"Wenn das auf euch passt, seid ihr genau richtig.",[19,1806,1808],{"id":1807},"unser-angebot","Unser Angebot",[366,1810,1811],{},[10,1812,1813,1814,1817],{},"Wir setzen Knecht für euch auf und übernehmen ",[59,1815,1816],{},"100 € OpenCode-Guthaben",", damit die AI Agents direkt loslegen können. Oben drauf wird das erste Jahr nach dem Release für euch kostenlos sein und euer Logo inkl. Link würden wir auf der Home platzieren, so dass es jeder sieht.",[10,1819,1820],{},"Ihr müsst euch also nicht durch die Einrichtung kämpfen und geht kein finanzielles Risiko ein. Ihr bekommt einen lauffähigen Knecht auf eurem eigenen Server, eingerichtet auf eure Projekte, plus das Guthaben, um ihn wirklich zu nutzen.",[19,1822,1824],{"id":1823},"was-wir-uns-wünschen","Was wir uns wünschen",[10,1826,1827],{},"Im Gegenzug brauchen wir ehrliches Feedback. Was hilft euch im Alltag? Wo hakt es? Was fehlt? Diese Rückmeldungen aus echten Agentur-Projekten sind für uns mehr wert als jeder Test.",[19,1829,1831],{"id":1830},"mehr-als-testen-mitgestalten","Mehr als testen: mitgestalten",[10,1833,1834],{},"Das ist die Chance, Knecht früh mitzuentwickeln. Euer Feedback fließt direkt in die Roadmap. Ihr seht nicht nur, wohin sich das Produkt entwickelt, ihr bestimmt die Richtung mit, bevor sich vieles verfestigt.",[19,1836,1838],{"id":1837},"interesse","Interesse?",[10,1840,1841,1842,1846],{},"Schreib uns einfach kurz an ",[51,1843,1845],{"href":1844},"mailto:hallo@knecht.works","hallo@knecht.works",". Erzähl uns von eurer Agentur und euren Projekten, dann finden wir gemeinsam heraus, ob es passt.",[19,1848,1850],{"id":1849},"weitersagen","Weitersagen",[10,1852,1853],{},"Du kennst eine Agentur, für die das passt? Dann beschreib Knecht am besten so:",[1855,1856,1857],"blockquote",{},[10,1858,1859,1860],{},"Knecht ist ein selbst gehostetes Dashboard für Agenturen mit vielen DDEV-Projekten. Er bootet jedes Projekt als lauffähige Umgebung, erledigt Aufgaben darin über Workflows und liefert fertige Pull Requests mit Preview. Beta-Tester gesucht: ",[51,1861,1862],{"href":1862,"rel":1863},"https:\u002F\u002Fknecht.works\u002Fupdates\u002Fbeta-tester",[55],[10,1865,1866],{},"Das ist die ganze Idee in drei Sätzen, und mehr braucht es fürs Teilen nicht.",{"title":290,"searchDepth":291,"depth":291,"links":1868},[1869,1870,1871,1872,1873,1874,1875],{"id":1747,"depth":291,"text":1748},{"id":1777,"depth":291,"text":1778},{"id":1807,"depth":291,"text":1808},{"id":1823,"depth":291,"text":1824},{"id":1830,"depth":291,"text":1831},{"id":1837,"depth":291,"text":1838},{"id":1849,"depth":291,"text":1850},"2026-06-08","Knecht ist bereit für echte Projekte. Wir suchen ein paar Agenturen, die ihn als Erste einsetzen und mitgestalten.",{},"\u002Fupdates\u002Fbeta-tester",{"title":1727,"description":1877},"updates\u002Fbeta-tester","Beta","6wrKyn9hx6-SHr_dbeZUlmj7RJTMLTEJeRoLJRuU_NE",{"id":1885,"title":1886,"body":1887,"date":1957,"description":1958,"extension":310,"meta":1959,"navigation":312,"path":1960,"seo":1961,"stem":1962,"tag":1963,"__hash__":1964},"updates\u002Fupdates\u002Flandingpage.md","Landingpage ist Online",{"type":7,"value":1888,"toc":1950},[1889,1892,1898,1902,1905,1916,1920,1923,1927,1930,1932,1935,1938],[10,1890,1891],{},"Die Landingpage ist online, und das ganz bewusst, bevor überhaupt noch der Prototyp steht.",[34,1893,1895],{"color":36,"icon":1894},"i-lucide-mouse",[10,1896,1897],{},"Schon auf den Knecht geklickt?",[19,1899,1901],{"id":1900},"warum-die-seite","Warum die Seite",[10,1903,1904],{},"Ein Produkt komplett fertig zu bauen und dann zu hoffen, dass es jemand braucht, ist riskant. Deshalb steht die Landingpage am Anfang. Sie macht drei Dinge:",[139,1906,1907,1910,1913],{},[142,1908,1909],{},"Sie erklärt die Idee in wenigen Sätzen.",[142,1911,1912],{},"Sie sammelt die ersten Beta-Tester.",[142,1914,1915],{},"Sie macht den Weg öffentlich, damit Feedback früh kommt und nicht erst zum Launch.",[79,1917,1919],{"id":1918},"sprache","Sprache",[10,1921,1922],{},"Viele Startups zielen gleich auf den Internationalen Markt ab. Wir finden, dass dies mit der Sprache anfängt. Wir werden uns zuerst auf den DACH Raum konzentrieren. Englisch als zweite Sprache muss daher noch etwas warten.",[19,1924,1926],{"id":1925},"was-sie-noch-nicht-ist","Was sie (noch) nicht ist",[10,1928,1929],{},"Damit keine falschen Erwartungen entstehen: Diese Seite ist kein Produkt. Kein Login, keine Demo, nichts zum Ausprobieren. Was du siehst, ist die Idee und ein erster Blick auf die geplante Oberfläche.",[19,1931,1509],{"id":1508},[10,1933,1934],{},"Als Nächstes steht der Prototyp an: Projekte, Workflows und der AI Agent.",[10,1936,1937],{},"Das Ziel für die nächsten Wochen ist klar: die ersten 3 bis 5 Agenturen als Beta-Tester finden, die Knecht an echten Projekten testen und ehrlich zurückmelden, was funktioniert.",[34,1939,1942],{"color":1940,"icon":1941},"neutral","i-lucide-arrow-right",[10,1943,1944,1945,1949],{},"Willst du früh dabei sein? Trag dich als ",[51,1946,1948],{"href":1947},"#cta","Beta-Tester"," ein.",{"title":290,"searchDepth":291,"depth":291,"links":1951},[1952,1955,1956],{"id":1900,"depth":291,"text":1901,"children":1953},[1954],{"id":1918,"depth":297,"text":1919},{"id":1925,"depth":291,"text":1926},{"id":1508,"depth":291,"text":1509},"2026-06-07","Warum die Landingpage noch vor dem Produkt kommt und wie es weitergeht.",{},"\u002Fupdates\u002Flandingpage",{"title":1886,"description":1958},"updates\u002Flandingpage","Setup","6Ut9xEfO_7VeUn89Xui2p-1RcrLE1HGYnEtndXGVPEI",{"id":1966,"title":1273,"body":1967,"date":2073,"description":2074,"extension":310,"meta":2075,"navigation":312,"path":1272,"seo":2076,"stem":2077,"tag":1963,"__hash__":2078},"updates\u002Fupdates\u002Fwas-macht-knecht.md",{"type":7,"value":1968,"toc":2060},[1969,1972,1976,1979,1982,1986,1992,1996,1999,2009,2012,2015,2018,2021,2025,2028,2032,2035],[10,1970,1971],{},"Ein Bugreport beginnt immer mit dem Versuch, den Fehler zu reproduzieren. Und nach dem Fix muss man sicherstellen, dass er wirklich behoben ist. Oder?",[19,1973,1975],{"id":1974},"motivation","Motivation",[10,1977,1978],{},"Agenturen betreuen oft viele Projekte. Diese sind häufig unterschiedlich konfiguriert oder laufen nur auf dem Rechner des einen Mitarbeiters, der von Anfang an dabei ist. Trotzdem brauchen alle regelmäßig Security-Updates und Bugfixes.",[10,1980,1981],{},"Damit AI Agents hier wirklich helfen können, brauchen wir vollständig lauffähige Projekte.",[79,1983,1985],{"id":1984},"security-updates","Security Updates",[10,1987,1988,1989,1991],{},"Um ein Security-Update in ein CMS einzuspielen, ist fast immer eine laufende Datenbank nötig – denn nach einem Update können Datenbank-Migrationen anstehen. Dependabot und andere Bots können deshalb nicht einfach ein ",[14,1990,864],{}," machen und live ausrollen.",[79,1993,1995],{"id":1994},"bug-fixes","Bug fixes",[10,1997,1998],{},"Leider ist es nicht immer so einfach:",[2000,2001,2003],"prompt",{"description":2002},"Pls fix!",[10,2004,2005],{},[51,2006,2007],{"href":2007,"rel":2008},"https:\u002F\u002Fyoutu.be\u002FdQw4w9WgXcQ?t=0",[55],[10,2010,2011],{},"Für einen Bugfix muss zuerst ein Mitarbeiter frei sein – im besten Fall einer, der das Projekt schon lokal laufen hat, um den Fehler nachhaltig zu lösen. Und wenn nicht?",[19,2013,535],{"id":2014},"ddev",[10,2016,2017],{},"DDEV ist eine Docker-basierte Entwicklungsumgebung. Mit ihr lässt sich ein Projekt vollständig betreiben, ohne Entwickler-Tools direkt auf dem Gerät zu installieren.",[10,2019,2020],{},"So fährt jedes Projekt reproduzierbar und isoliert hoch: Web und Datenbank kommen sauber konfiguriert und immer auf die gleiche Weise. Genau diese Verlässlichkeit ist die Grundlage für alles Weitere. Denn was sich auf Knopfdruck immer gleich booten lässt, kann auch eine Maschine booten.",[19,2022,2024],{"id":2023},"was-der-knecht-macht","Was der Knecht macht",[10,2026,2027],{},"Knecht wird als Dashboard gebaut, das du auf deinem eigenen Server hostest – so behältst du die Daten. Im Dashboard gibt es Preview-Links und Screenshots (über Penthouse), mit denen du einen Bugfix direkt verifizieren kannst.",[79,2029,2031],{"id":2030},"how-it-works","How it works",[10,2033,2034],{},"In Knecht gibt es Projekte, Workflows und Trigger.",[75,2036,2037,2041,2044,2047,2050,2053,2057],{"level":77},[79,2038,2040],{"id":2039},"projekte","Projekte",[10,2042,2043],{},"Ein Projekt besteht immer aus einem GitHub-Repository, in dem DDEV bereits konfiguriert ist. Darin lassen sich dann noch Environment-Variablen und die Datenbank festlegen.",[79,2045,330],{"id":2046},"workflows",[10,2048,2049],{},"Ein Workflow ist ein vordefinierter Prozess, der mit Projekten arbeitet. Er kann zum Beispiel so aussehen: Projekt booten → Composer-Pakete updaten → PR mit den Änderungen erstellen. Die Schritte werden also deterministisch abgearbeitet.",[10,2051,2052],{},"Bugfixes sind aber selten immer gleiche, wiederholbare Schritte. Hier kommen AI Agents ins Spiel: Über Opencode können sie einen Bug im laufenden Projekt reproduzieren und beheben.",[79,2054,2056],{"id":2055},"trigger","Trigger",[10,2058,2059],{},"Ein Trigger ist der Startpunkt eines Workflows. Über Webhooks – etwa von Jira oder GitHub – kann der Knecht vordefinierte Workflows automatisch starten und die Ergebnisse später als Pull Request liefern.",{"title":290,"searchDepth":291,"depth":291,"links":2061},[2062,2066,2067],{"id":1974,"depth":291,"text":1975,"children":2063},[2064,2065],{"id":1984,"depth":297,"text":1985},{"id":1994,"depth":297,"text":1995},{"id":2014,"depth":291,"text":535},{"id":2023,"depth":291,"text":2024,"children":2068},[2069,2070,2071,2072],{"id":2030,"depth":297,"text":2031},{"id":2039,"depth":297,"text":2040},{"id":2046,"depth":297,"text":330},{"id":2055,"depth":297,"text":2056},"2026-05-15","Über die Ideenfindung, die Motivation und wie Knecht dabei hilft.",{},{"title":1273,"description":2074},"updates\u002Fwas-macht-knecht","a6-izLIbeWIxU83TytraKyJ8YYg9m2-gmKEefbh7Bwc",1785276229155]