[{"data":1,"prerenderedAt":1580},["ShallowReactive",2],{"search-de":3,"content-de-developer\u002Fself-hosting-production":4,"surround-de-\u002Fdeveloper\u002Fself-hosting-production":1571},[],{"id":5,"title":6,"body":7,"description":1564,"extension":1565,"meta":1566,"navigation":134,"path":1567,"seo":1568,"stem":1569,"__hash__":1570},"content_de\u002F3.developer\u002F33.self-hosting-production.md","Produktions-Deployment",{"type":8,"value":9,"toc":1543},"minimark",[10,25,30,33,38,54,250,257,295,298,360,375,379,419,551,554,586,590,593,685,779,782,803,809,815,862,868,892,901,916,920,927,989,999,1038,1041,1056,1060,1070,1096,1111,1117,1120,1124,1167,1174,1178,1182,1185,1199,1202,1420,1424,1427,1439,1442,1446,1449,1529,1539],[11,12,13,14,19,20,24],"p",{},"Diese Anleitung behandelt das Härten Ihrer selbst gehosteten Owlat-Instanz für den\nproduktiven Einsatz. Für die Ersteinrichtung siehe ",[15,16,18],"a",{"href":17},"\u002Fdeveloper\u002Fself-hosting","Self-Hosting",".\nBevor Sie den eingebauten MTA nach außen freigeben, arbeiten Sie den Preflight zu\nProvider, ausgehenden Ports, Reverse DNS und Warm-up in ",[15,21,23],{"href":22},"\u002Fguide\u002Fsending-from-a-vps","Versand von einem VPS"," durch.",[26,27,29],"h2",{"id":28},"reverse-proxy","Reverse Proxy",[11,31,32],{},"Setzen Sie in der Produktion einen Reverse Proxy vor den Docker-Stack, der die TLS-Terminierung übernimmt und den Traffic routet.",[34,35,37],"h3",{"id":36},"caddy-empfohlen","Caddy (empfohlen)",[11,39,40,41,45,46,49,50,53],{},"Owlat liefert einen fertigen Caddy-Reverse-Proxy als Service ",[42,43,44],"code",{},"caddy"," innerhalb von ",[42,47,48],{},"docker-compose.yml"," mit, abgesichert über das Compose-Profil ",[42,51,52],{},"tls",". Er beschafft und erneuert TLS-Zertifikate automatisch über Let's Encrypt und proxyt die übrigen Container über ihre Docker-Servicenamen — ein separater Proxy-Host ist nicht nötig.",[55,56,57,61,86,90,105,175,193,197,208,212,238],"steps",{},[34,58,60],{"id":59},"beispielkonfiguration-kopieren","Beispielkonfiguration kopieren",[62,63,68],"pre",{"className":64,"code":65,"language":66,"meta":67,"style":67},"language-bash shiki shiki-themes github-light github-dark-dimmed","cp Caddyfile.example Caddyfile\n","bash","",[42,69,70],{"__ignoreMap":67},[71,72,75,79,83],"span",{"class":73,"line":74},"line",1,[71,76,78],{"class":77},"sOLd2","cp",[71,80,82],{"class":81},"s-HuK"," Caddyfile.example",[71,84,85],{"class":81}," Caddyfile\n",[34,87,89],{"id":88},"ihre-domains-eintragen","Ihre Domains eintragen",[11,91,92,93,96,97,100,101,104],{},"Bearbeiten Sie die ",[42,94,95],{},"Caddyfile",", ersetzen Sie jedes ",[42,98,99],{},"example.com"," durch Ihre echte Domain und setzen Sie die ",[42,102,103],{},"email"," im globalen Optionsblock (wird für die Registrierung bei Let's Encrypt verwendet). Die mitgelieferte Datei proxyt drei Subdomains auf die netzinternen Servicenamen:",[62,106,110],{"className":107,"code":108,"language":109,"meta":67,"style":67},"language-caddyfile shiki shiki-themes github-light github-dark-dimmed","owlat.example.com {\n    reverse_proxy web:3000\n}\n\napi.example.com {\n    reverse_proxy convex:3210\n}\n\nrest.api.example.com {\n    reverse_proxy convex:3211\n}\n","caddyfile",[42,111,112,117,123,129,136,142,148,153,158,164,170],{"__ignoreMap":67},[71,113,114],{"class":73,"line":74},[71,115,116],{},"owlat.example.com {\n",[71,118,120],{"class":73,"line":119},2,[71,121,122],{},"    reverse_proxy web:3000\n",[71,124,126],{"class":73,"line":125},3,[71,127,128],{},"}\n",[71,130,132],{"class":73,"line":131},4,[71,133,135],{"emptyLinePlaceholder":134},true,"\n",[71,137,139],{"class":73,"line":138},5,[71,140,141],{},"api.example.com {\n",[71,143,145],{"class":73,"line":144},6,[71,146,147],{},"    reverse_proxy convex:3210\n",[71,149,151],{"class":73,"line":150},7,[71,152,128],{},[71,154,156],{"class":73,"line":155},8,[71,157,135],{"emptyLinePlaceholder":134},[71,159,161],{"class":73,"line":160},9,[71,162,163],{},"rest.api.example.com {\n",[71,165,167],{"class":73,"line":166},10,[71,168,169],{},"    reverse_proxy convex:3211\n",[71,171,173],{"class":73,"line":172},11,[71,174,128],{},[11,176,177,180,181,184,185,189,190,192],{},[42,178,179],{},"convex"," (3210, WebSocket\u002FHTTP zum Browser) und ",[42,182,183],{},"convex-site"," (3211, HTTP-Actions wie BetterAuth und Webhooks) werden vom ",[186,187,188],"strong",{},"selben"," ",[42,191,179],{},"-Container bedient — es sind zwei Ports eines Service, nicht zwei Services.",[34,194,196],{"id":195},"dns-setzen-und-ports-öffnen","DNS setzen und Ports öffnen",[11,198,199,200,203,204,207],{},"Legen Sie für jede Subdomain einen A-Record an, der auf diesen Server zeigt, und stellen Sie sicher, dass die Ports ",[42,201,202],{},"80"," und ",[42,205,206],{},"443"," offen sind. Caddy benötigt sie, um die ACME-HTTP-Challenge abzuschließen.",[34,209,211],{"id":210},"stack-starten","Stack starten",[62,213,215],{"className":64,"code":214,"language":66,"meta":67,"style":67},"docker compose --profile tls up -d\n",[42,216,217],{"__ignoreMap":67},[71,218,219,222,225,229,232,235],{"class":73,"line":74},[71,220,221],{"class":77},"docker",[71,223,224],{"class":81}," compose",[71,226,228],{"class":227},"sviXB"," --profile",[71,230,231],{"class":81}," tls",[71,233,234],{"class":81}," up",[71,236,237],{"class":227}," -d\n",[11,239,240,241,243,244,246,247,249],{},"Der ",[42,242,44],{},"-Service bindet ",[42,245,202],{},"\u002F",[42,248,206],{}," und bezieht die Zertifikate beim ersten Start.",[11,251,252,253,256],{},"Sobald der Proxy läuft, passen Sie Ihre ",[42,254,255],{},".env"," auf die HTTPS-URLs an:",[62,258,260],{"className":64,"code":259,"language":66,"meta":67,"style":67},"NUXT_PUBLIC_CONVEX_URL=https:\u002F\u002Fapi.example.com\nNUXT_PUBLIC_CONVEX_SITE_URL=https:\u002F\u002Frest.api.example.com\nNUXT_PUBLIC_SITE_URL=https:\u002F\u002Fowlat.example.com\n",[42,261,262,275,285],{"__ignoreMap":67},[71,263,264,268,272],{"class":73,"line":74},[71,265,267],{"class":266},"sYgZi","NUXT_PUBLIC_CONVEX_URL",[71,269,271],{"class":270},"s7YZ4","=",[71,273,274],{"class":81},"https:\u002F\u002Fapi.example.com\n",[71,276,277,280,282],{"class":73,"line":119},[71,278,279],{"class":266},"NUXT_PUBLIC_CONVEX_SITE_URL",[71,281,271],{"class":270},[71,283,284],{"class":81},"https:\u002F\u002Frest.api.example.com\n",[71,286,287,290,292],{"class":73,"line":125},[71,288,289],{"class":266},"NUXT_PUBLIC_SITE_URL",[71,291,271],{"class":270},[71,293,294],{"class":81},"https:\u002F\u002Fowlat.example.com\n",[11,296,297],{},"Aktualisieren Sie anschließend die zugehörigen Convex-Umgebungsvariablen:",[62,299,301],{"className":64,"code":300,"language":66,"meta":67,"style":67},"npx convex env set SITE_URL \"https:\u002F\u002Fowlat.example.com\" --url http:\u002F\u002Flocalhost:3210 --admin-key \u003Ckey>\n# CONVEX_SITE_URL is a Convex BUILT-IN — do not set it via `convex env set`\n   # (the CLI rejects it). It derives from CONVEX_SITE_ORIGIN on the convex\n   # container, which docker-compose interpolates from NUXT_PUBLIC_CONVEX_SITE_URL.\n",[42,302,303,344,350,355],{"__ignoreMap":67},[71,304,305,308,311,314,317,320,323,326,329,332,335,338,341],{"class":73,"line":74},[71,306,307],{"class":77},"npx",[71,309,310],{"class":81}," convex",[71,312,313],{"class":81}," env",[71,315,316],{"class":81}," set",[71,318,319],{"class":81}," SITE_URL",[71,321,322],{"class":81}," \"https:\u002F\u002Fowlat.example.com\"",[71,324,325],{"class":227}," --url",[71,327,328],{"class":81}," http:\u002F\u002Flocalhost:3210",[71,330,331],{"class":227}," --admin-key",[71,333,334],{"class":270}," \u003C",[71,336,337],{"class":81},"ke",[71,339,340],{"class":266},"y",[71,342,343],{"class":270},">\n",[71,345,346],{"class":73,"line":119},[71,347,349],{"class":348},"sDN9O","# CONVEX_SITE_URL is a Convex BUILT-IN — do not set it via `convex env set`\n",[71,351,352],{"class":73,"line":125},[71,353,354],{"class":348},"   # (the CLI rejects it). It derives from CONVEX_SITE_ORIGIN on the convex\n",[71,356,357],{"class":73,"line":131},[71,358,359],{"class":348},"   # container, which docker-compose interpolates from NUXT_PUBLIC_CONVEX_SITE_URL.\n",[361,362,365],"callout",{"title":363,"type":364},"Staging-Zertifikate","tip",[11,366,367,368,370,371,374],{},"Um den Proxy zu testen, ohne die Rate Limits von Let's Encrypt aufzubrauchen, entfernen Sie im globalen Optionsblock der ",[42,369,95],{}," das Kommentarzeichen vor der Zeile ",[42,372,373],{},"acme_ca https:\u002F\u002Facme-staging-v02.api.letsencrypt.org\u002Fdirectory",", starten den Stack und wechseln zurück zur Produktion, sobald DNS und Routing stimmen.",[34,376,378],{"id":377},"externer-reverse-proxy-alternative","Externer Reverse Proxy (Alternative)",[11,380,381,382,384,385,388,389,391,392,395,396,399,400,403,404,407,408,411,412,415,416,418],{},"Wenn Sie den Proxy lieber außerhalb von Docker auf dem Host betreiben — oder dem Stack ein bestehendes Nginx\u002FCaddy voranstellen —, verwenden Sie das mitgelieferte ",[42,383,52],{},"-Profil ",[186,386,387],{},"nicht",". Proxyen Sie stattdessen auf die am Host veröffentlichten Container-Ports. Standardmäßig veröffentlicht ",[42,390,48],{}," web (",[42,393,394],{},"3000",") auf allen Interfaces, während convex (",[42,397,398],{},"3210",") und convex-site (",[42,401,402],{},"3211",") nur an ",[42,405,406],{},"127.0.0.1"," gebunden sind — ein hostlokaler Proxy auf ",[42,409,410],{},"localhost:*"," funktioniert also genau wie beschrieben. Läuft der Proxy auf einem anderen Host, setzen Sie ",[42,413,414],{},"CONVEX_BIND",", um 3210\u002F3211 über das Loopback hinaus verfügbar zu machen (und sichern Sie sie per Firewall ab). Da der Proxy hier außerhalb des Docker-Netzwerks läuft, muss er ",[42,417,410],{}," ansprechen, nicht die Servicenamen.",[62,420,424],{"className":421,"code":422,"language":423,"meta":67,"style":67},"language-nginx shiki shiki-themes github-light github-dark-dimmed","server {\n    listen 80;\n    server_name owlat.example.com;\n    return 301 https:\u002F\u002F$server_name$request_uri;\n}\n\nserver {\n    listen 443 ssl http2;\n    server_name owlat.example.com;\n\n    ssl_certificate \u002Fetc\u002Fletsencrypt\u002Flive\u002Fowlat.example.com\u002Ffullchain.pem;\n    ssl_certificate_key \u002Fetc\u002Fletsencrypt\u002Flive\u002Fowlat.example.com\u002Fprivkey.pem;\n\n    location \u002F {\n        proxy_pass http:\u002F\u002Flocalhost:3000;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    }\n}\n\n# Repeat similar blocks for api.example.com (localhost:3210)\n# and rest.api.example.com (localhost:3211).\n","nginx",[42,425,426,431,436,441,446,450,454,458,463,467,471,476,482,487,493,499,505,511,517,523,529,534,539,545],{"__ignoreMap":67},[71,427,428],{"class":73,"line":74},[71,429,430],{},"server {\n",[71,432,433],{"class":73,"line":119},[71,434,435],{},"    listen 80;\n",[71,437,438],{"class":73,"line":125},[71,439,440],{},"    server_name owlat.example.com;\n",[71,442,443],{"class":73,"line":131},[71,444,445],{},"    return 301 https:\u002F\u002F$server_name$request_uri;\n",[71,447,448],{"class":73,"line":138},[71,449,128],{},[71,451,452],{"class":73,"line":144},[71,453,135],{"emptyLinePlaceholder":134},[71,455,456],{"class":73,"line":150},[71,457,430],{},[71,459,460],{"class":73,"line":155},[71,461,462],{},"    listen 443 ssl http2;\n",[71,464,465],{"class":73,"line":160},[71,466,440],{},[71,468,469],{"class":73,"line":166},[71,470,135],{"emptyLinePlaceholder":134},[71,472,473],{"class":73,"line":172},[71,474,475],{},"    ssl_certificate \u002Fetc\u002Fletsencrypt\u002Flive\u002Fowlat.example.com\u002Ffullchain.pem;\n",[71,477,479],{"class":73,"line":478},12,[71,480,481],{},"    ssl_certificate_key \u002Fetc\u002Fletsencrypt\u002Flive\u002Fowlat.example.com\u002Fprivkey.pem;\n",[71,483,485],{"class":73,"line":484},13,[71,486,135],{"emptyLinePlaceholder":134},[71,488,490],{"class":73,"line":489},14,[71,491,492],{},"    location \u002F {\n",[71,494,496],{"class":73,"line":495},15,[71,497,498],{},"        proxy_pass http:\u002F\u002Flocalhost:3000;\n",[71,500,502],{"class":73,"line":501},16,[71,503,504],{},"        proxy_set_header Host $host;\n",[71,506,508],{"class":73,"line":507},17,[71,509,510],{},"        proxy_set_header X-Real-IP $remote_addr;\n",[71,512,514],{"class":73,"line":513},18,[71,515,516],{},"        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n",[71,518,520],{"class":73,"line":519},19,[71,521,522],{},"        proxy_set_header X-Forwarded-Proto $scheme;\n",[71,524,526],{"class":73,"line":525},20,[71,527,528],{},"    }\n",[71,530,532],{"class":73,"line":531},21,[71,533,128],{},[71,535,537],{"class":73,"line":536},22,[71,538,135],{"emptyLinePlaceholder":134},[71,540,542],{"class":73,"line":541},23,[71,543,544],{},"# Repeat similar blocks for api.example.com (localhost:3210)\n",[71,546,548],{"class":73,"line":547},24,[71,549,550],{},"# and rest.api.example.com (localhost:3211).\n",[11,552,553],{},"Installieren Sie die Zertifikate mit Certbot:",[62,555,557],{"className":64,"code":556,"language":66,"meta":67,"style":67},"sudo certbot --nginx -d owlat.example.com -d api.example.com -d rest.api.example.com\n",[42,558,559],{"__ignoreMap":67},[71,560,561,564,567,570,573,576,578,581,583],{"class":73,"line":74},[71,562,563],{"class":77},"sudo",[71,565,566],{"class":81}," certbot",[71,568,569],{"class":227}," --nginx",[71,571,572],{"class":227}," -d",[71,574,575],{"class":81}," owlat.example.com",[71,577,572],{"class":227},[71,579,580],{"class":81}," api.example.com",[71,582,572],{"class":227},[71,584,585],{"class":81}," rest.api.example.com\n",[26,587,589],{"id":588},"firewall","Firewall",[11,591,592],{},"Verwenden Sie UFW (oder die Firewall Ihrer Distribution), um den Zugriff auf die notwendigen Ports zu beschränken.",[62,594,596],{"className":64,"code":595,"language":66,"meta":67,"style":67},"# Allow SSH\nsudo ufw allow 22\u002Ftcp\n\n# Allow HTTP\u002FHTTPS (reverse proxy)\nsudo ufw allow 80\u002Ftcp\nsudo ufw allow 443\u002Ftcp\n\n# Allow SMTP (bounce processing)\nsudo ufw allow 25\u002Ftcp\n\n# Enable the firewall\nsudo ufw enable\n",[42,597,598,603,616,620,625,636,647,651,656,667,671,676],{"__ignoreMap":67},[71,599,600],{"class":73,"line":74},[71,601,602],{"class":348},"# Allow SSH\n",[71,604,605,607,610,613],{"class":73,"line":119},[71,606,563],{"class":77},[71,608,609],{"class":81}," ufw",[71,611,612],{"class":81}," allow",[71,614,615],{"class":81}," 22\u002Ftcp\n",[71,617,618],{"class":73,"line":125},[71,619,135],{"emptyLinePlaceholder":134},[71,621,622],{"class":73,"line":131},[71,623,624],{"class":348},"# Allow HTTP\u002FHTTPS (reverse proxy)\n",[71,626,627,629,631,633],{"class":73,"line":138},[71,628,563],{"class":77},[71,630,609],{"class":81},[71,632,612],{"class":81},[71,634,635],{"class":81}," 80\u002Ftcp\n",[71,637,638,640,642,644],{"class":73,"line":144},[71,639,563],{"class":77},[71,641,609],{"class":81},[71,643,612],{"class":81},[71,645,646],{"class":81}," 443\u002Ftcp\n",[71,648,649],{"class":73,"line":150},[71,650,135],{"emptyLinePlaceholder":134},[71,652,653],{"class":73,"line":155},[71,654,655],{"class":348},"# Allow SMTP (bounce processing)\n",[71,657,658,660,662,664],{"class":73,"line":160},[71,659,563],{"class":77},[71,661,609],{"class":81},[71,663,612],{"class":81},[71,665,666],{"class":81}," 25\u002Ftcp\n",[71,668,669],{"class":73,"line":166},[71,670,135],{"emptyLinePlaceholder":134},[71,672,673],{"class":73,"line":172},[71,674,675],{"class":348},"# Enable the firewall\n",[71,677,678,680,682],{"class":73,"line":478},[71,679,563],{"class":77},[71,681,609],{"class":81},[71,683,684],{"class":81}," enable\n",[361,686,689,717,736,739],{"title":687,"type":688},"Interne Ports nicht freigeben","warning",[11,690,691,692,694,695,694,697,694,699,694,702,203,705,708,709,711,712,246,714,716],{},"Die Ports ",[42,693,398],{},", ",[42,696,402],{},[42,698,394],{},[42,700,701],{},"3100",[42,703,704],{},"6379",[42,706,707],{},"3310"," sollten aus dem Internet ",[186,710,387],{}," erreichbar sein. Der Reverse Proxy nimmt externen Traffic auf den Ports ",[42,713,202],{},[42,715,206],{}," entgegen und leitet ihn an die internen Services weiter.",[11,718,719,720,722,723,246,725,727,728,731,732,735],{},"Beachten Sie, dass web (",[42,721,394],{},") standardmäßig auf allen Interfaces veröffentlicht wird (damit die SPA für den lokalen Quickstart-Weg erreichbar ist), während convex (",[42,724,398],{},[42,726,402],{},") an das Loopback gebunden ist. Wenn nur der Reverse Proxy die Web-Oberfläche erreichen soll, setzen Sie ",[42,729,730],{},"WEB_PORT"," mit vorangestellter Host-IP (z. B. ",[42,733,734],{},"127.0.0.1:3000",") oder sichern Sie den Port per Firewall ab.",[11,737,738],{},"Zwei weitere verdienen Beachtung:",[740,741,742,758],"ul",{},[743,744,745,750,751,753,754,757],"li",{},[186,746,747],{},[42,748,749],{},"3200"," — der Updater-Sidecar. Er ist konzeptbedingt rein intern (kein veröffentlichter Port in ",[42,752,48],{},", erreichbar nur als ",[42,755,756],{},"http:\u002F\u002Fupdater:3200"," über das Docker-Netzwerk). Lassen Sie das so.",[743,759,760,765,766,769,770,773,774,778],{},[186,761,762],{},[42,763,764],{},"6791"," — das Convex-Dashboard. Es ist an das Loopback gebunden (",[42,767,768],{},"127.0.0.1:6791",") und profilgesteuert, also standardmäßig aus und nur über einen SSH-Tunnel erreichbar, nachdem Sie es mit ",[42,771,772],{},"--profile dashboard"," gestartet haben. Siehe ",[15,775,777],{"href":776},"#secure-the-dashboard","Dashboard absichern"," weiter unten.",[26,780,777],{"id":781},"dashboard-absichern",[11,783,784,785,787,788,791,792,794,795,798,799,802],{},"Das Convex-Dashboard auf Port ",[42,786,764],{}," bietet vollen Lese-\u002FSchreibzugriff auf Ihre Datenbank und hat ",[186,789,790],{},"keine eingebaute Authentifizierung",". Die ",[42,793,48],{}," im Projektstamm bindet es bereits ausschließlich an das Loopback-Interface (",[42,796,797],{},"127.0.0.1:${DASHBOARD_PORT:-6791}:6791",") und sichert es über das Compose-Profil ",[42,800,801],{},"dashboard"," ab; in einem frischen Stack ist es also weder öffentlich veröffentlicht noch überhaupt gestartet. Belassen Sie es vor dem Produktivgang dabei.",[11,804,805,808],{},[186,806,807],{},"Option 1: Den ausgelieferten Standard nutzen"," (empfohlen)",[11,810,811,812,814],{},"Es ist kein Override nötig — das Dashboard ist bereits an das Loopback gebunden und startet nur unter ",[42,813,772],{},". Starten Sie es bei Bedarf und prüfen Sie die Bindung:",[62,816,818],{"className":64,"code":817,"language":66,"meta":67,"style":67},"docker compose --profile dashboard up -d convex-dashboard\ndocker compose --profile dashboard port convex-dashboard 6791\n# Should show: 127.0.0.1:6791\n",[42,819,820,838,857],{"__ignoreMap":67},[71,821,822,824,826,828,831,833,835],{"class":73,"line":74},[71,823,221],{"class":77},[71,825,224],{"class":81},[71,827,228],{"class":227},[71,829,830],{"class":81}," dashboard",[71,832,234],{"class":81},[71,834,572],{"class":227},[71,836,837],{"class":81}," convex-dashboard\n",[71,839,840,842,844,846,848,851,854],{"class":73,"line":119},[71,841,221],{"class":77},[71,843,224],{"class":81},[71,845,228],{"class":227},[71,847,830],{"class":81},[71,849,850],{"class":81}," port",[71,852,853],{"class":81}," convex-dashboard",[71,855,856],{"class":227}," 6791\n",[71,858,859],{"class":73,"line":125},[71,860,861],{"class":348},"# Should show: 127.0.0.1:6791\n",[11,863,864,867],{},[186,865,866],{},"Option 2: SSH-Tunnel"," (für Fernzugriff nach der Bindung an localhost)",[62,869,871],{"className":64,"code":870,"language":66,"meta":67,"style":67},"ssh -L 6791:localhost:6791 user@your-server\n# Then open http:\u002F\u002Flocalhost:6791 in your browser\n",[42,872,873,887],{"__ignoreMap":67},[71,874,875,878,881,884],{"class":73,"line":74},[71,876,877],{"class":77},"ssh",[71,879,880],{"class":227}," -L",[71,882,883],{"class":81}," 6791:localhost:6791",[71,885,886],{"class":81}," user@your-server\n",[71,888,889],{"class":73,"line":119},[71,890,891],{"class":348},"# Then open http:\u002F\u002Flocalhost:6791 in your browser\n",[11,893,894,897,898,900],{},[186,895,896],{},"Option 3: Per Firewall absichern"," — wenn Sie die Bindung nicht ändern können, blockieren Sie Port ",[42,899,764],{}," in der Host-Firewall (er fehlt in den UFW-Regeln oben bewusst), sodass nur das Loopback oder ein VPN ihn erreicht.",[361,902,905],{"title":903,"type":904},"Das Dashboard niemals offen proxyen","danger",[11,906,907,908,911,912,915],{},"Wenn Sie das Dashboard über den mitgelieferten Caddy-Proxy freigeben, sichern Sie es hinter Basic Auth oder einer IP-Allowlist ab — es wird ohne Login ausgeliefert. Der auskommentierte Block ",[42,909,910],{},"convex-dashboard.example.com"," in ",[42,913,914],{},"Caddyfile.example"," zeigt ein Beispiel für eine IP-Allowlist.",[26,917,919],{"id":918},"redis-authentifizierung","Redis-Authentifizierung",[11,921,922,923,926],{},"Vergeben Sie für die Produktion ein Passwort für Redis. Legen Sie eine ",[42,924,925],{},"docker-compose.override.yml"," an:",[62,928,932],{"className":929,"code":930,"language":931,"meta":67,"style":67},"language-yaml shiki shiki-themes github-light github-dark-dimmed","services:\n  redis:\n    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}\n\n  mta:\n    environment:\n      REDIS_URL: redis:\u002F\u002F:${REDIS_PASSWORD}@redis:6379\n","yaml",[42,933,934,943,950,961,965,972,979],{"__ignoreMap":67},[71,935,936,940],{"class":73,"line":74},[71,937,939],{"class":938},"sIAta","services",[71,941,942],{"class":266},":\n",[71,944,945,948],{"class":73,"line":119},[71,946,947],{"class":938},"  redis",[71,949,942],{"class":266},[71,951,952,955,958],{"class":73,"line":125},[71,953,954],{"class":938},"    command",[71,956,957],{"class":266},": ",[71,959,960],{"class":81},"redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}\n",[71,962,963],{"class":73,"line":131},[71,964,135],{"emptyLinePlaceholder":134},[71,966,967,970],{"class":73,"line":138},[71,968,969],{"class":938},"  mta",[71,971,942],{"class":266},[71,973,974,977],{"class":73,"line":144},[71,975,976],{"class":938},"    environment",[71,978,942],{"class":266},[71,980,981,984,986],{"class":73,"line":150},[71,982,983],{"class":938},"      REDIS_URL",[71,985,957],{"class":266},[71,987,988],{"class":81},"redis:\u002F\u002F:${REDIS_PASSWORD}@redis:6379\n",[11,990,991,992,995,996,998],{},"Fügen Sie ",[42,993,994],{},"REDIS_PASSWORD"," Ihrer ",[42,997,255],{}," hinzu:",[62,1000,1002],{"className":64,"code":1001,"language":66,"meta":67,"style":67},"# Generate a Redis password\necho \"REDIS_PASSWORD=$(openssl rand -base64 32)\" >> .env\n",[42,1003,1004,1009],{"__ignoreMap":67},[71,1005,1006],{"class":73,"line":74},[71,1007,1008],{"class":348},"# Generate a Redis password\n",[71,1010,1011,1014,1017,1020,1023,1026,1029,1032,1035],{"class":73,"line":119},[71,1012,1013],{"class":227},"echo",[71,1015,1016],{"class":81}," \"REDIS_PASSWORD=$(",[71,1018,1019],{"class":77},"openssl",[71,1021,1022],{"class":81}," rand ",[71,1024,1025],{"class":227},"-base64",[71,1027,1028],{"class":227}," 32",[71,1030,1031],{"class":81},")\"",[71,1033,1034],{"class":270}," >>",[71,1036,1037],{"class":81}," .env\n",[11,1039,1040],{},"Starten Sie den Stack neu:",[62,1042,1044],{"className":64,"code":1043,"language":66,"meta":67,"style":67},"docker compose up -d\n",[42,1045,1046],{"__ignoreMap":67},[71,1047,1048,1050,1052,1054],{"class":73,"line":74},[71,1049,221],{"class":77},[71,1051,224],{"class":81},[71,1053,234],{"class":81},[71,1055,237],{"class":227},[26,1057,1059],{"id":1058},"backups","Backups",[11,1061,1062,1063,1066,1067,1069],{},"Verwenden Sie das mitgelieferte Skript — es ist der einzige Erzeuger von Archiven,\ndie ",[42,1064,1065],{},"scripts\u002Frestore.sh"," akzeptiert, und es übernimmt die Konsistenzarbeit für Sie\n(kurzes Pausieren von convex + redis rund um den Kopiervorgang, Ermitteln sämtlicher\nProjekt-Volumes, Sichern von ",[42,1068,255],{},", des Compose-Overrides und der Caddyfile sowie\nSchreiben einer SHA256-Sidecar-Datei):",[62,1071,1073],{"className":64,"code":1072,"language":66,"meta":67,"style":67},"bash scripts\u002Fbackup.sh                 # → .\u002Fbackups\u002Fowlat-\u003Ctimestamp>.tar.gz\nowlat backup                           # same, via the CLI wrapper\n",[42,1074,1075,1085],{"__ignoreMap":67},[71,1076,1077,1079,1082],{"class":73,"line":74},[71,1078,66],{"class":77},[71,1080,1081],{"class":81}," scripts\u002Fbackup.sh",[71,1083,1084],{"class":348},"                 # → .\u002Fbackups\u002Fowlat-\u003Ctimestamp>.tar.gz\n",[71,1086,1087,1090,1093],{"class":73,"line":119},[71,1088,1089],{"class":77},"owlat",[71,1091,1092],{"class":81}," backup",[71,1094,1095],{"class":348},"                           # same, via the CLI wrapper\n",[11,1097,1098,1099,1102,1103,1106,1107,1110],{},"Das Volume ",[42,1100,1101],{},"convex-data"," ist das kritische — es enthält sämtliche Anwendungsdaten\n(Kontakte, Kampagnen, Vorlagen, hochgeladene Dateien, Einstellungen). Das\nConvex-Backend speichert seinen Zustand in ",[186,1104,1105],{},"SQLite",", rohe Hot-Kopien des Volumes\nsind daher nicht crash-konsistent; das Skript pausiert den Container für die\nSekunden, die das Kopieren dauert. Dasselbe gilt für die mehrteilige AOF von\nRedis 7 (",[42,1108,1109],{},"appendonlydir\u002F",").",[11,1112,1113,1116],{},[186,1114,1115],{},"Volume-Snapshots des Anbieters"," (Hetzner, AWS, …) sind eine gute Ergänzung, mit\ndemselben Vorbehalt: Erstellen Sie den Snapshot bei gestopptem Stack, oder nehmen\nSie in Kauf, dass ein mitten im Schreibvorgang erstellter Snapshot beim\nWiederherstellen eine AOF-\u002FSQLite-Wiederherstellung erfordern kann.",[11,1118,1119],{},"ClamAV-Signaturen und Ollama-Modelle benötigen kein Backup — beide laden sich neu herunter.",[34,1121,1123],{"id":1122},"disaster-recovery-neuer-vps-defekte-festplatte","Disaster Recovery (neuer VPS \u002F defekte Festplatte)",[1125,1126,1127,1130,1141,1155,1164],"ol",{},[743,1128,1129],{},"Stellen Sie einen frischen VPS bereit und installieren Sie Docker + Compose v2.",[743,1131,1132,1133,1136,1137,1140],{},"Klonen Sie das Owlat-Repository per ",[42,1134,1135],{},"git clone"," (dieselbe Version wie das Backup — das\nMANIFEST hält fest, wann es erstellt wurde) und wechseln Sie mit ",[42,1138,1139],{},"cd"," hinein.",[743,1142,1143,1144,189,1147,1154],{},"Kopieren Sie Ihr ",[42,1145,1146],{},"owlat-\u003Ctimestamp>.tar.gz",[186,1148,1149,1150,1153],{},"und dessen ",[42,1151,1152],{},".sha256","-Sidecar-Datei"," herüber.",[743,1156,1157,1160,1161,1163],{},[42,1158,1159],{},"bash scripts\u002Frestore.sh owlat-\u003Ctimestamp>.tar.gz"," — das überprüft die\nPrüfsumme, stellt jedes Volume sowie ",[42,1162,255],{},"\u002FOverride\u002FCaddyfile wieder her und\nstartet den Stack mit Ihren Feature-Profilen.",[743,1165,1166],{},"Richten Sie DNS auf den neuen Server um; TLS-Zertifikate werden automatisch neu\nausgestellt, sobald DNS auflöst.",[11,1168,1169,1170,1173],{},"Bewahren Sie mindestens ein Backup ",[186,1171,1172],{},"außerhalb des VPS"," auf (Object Storage, Ihr Laptop) —\nein Backup auf der defekten Festplatte ist kein Backup.",[26,1175,1177],{"id":1176},"monitoring","Monitoring",[34,1179,1181],{"id":1180},"healthchecks","Healthchecks",[11,1183,1184],{},"Für die kritischen Services (Convex, Redis, ClamAV) sind Docker-Healthchecks bereits konfiguriert. Ihren Status prüfen Sie so:",[62,1186,1188],{"className":64,"code":1187,"language":66,"meta":67,"style":67},"docker compose ps\n",[42,1189,1190],{"__ignoreMap":67},[71,1191,1192,1194,1196],{"class":73,"line":74},[71,1193,221],{"class":77},[71,1195,224],{"class":81},[71,1197,1198],{"class":81}," ps\n",[11,1200,1201],{},"Für externes Monitoring erstellen Sie ein Healthcheck-Skript:",[62,1203,1205],{"className":64,"code":1204,"language":66,"meta":67,"style":67},"#!\u002Fbin\u002Fbash\n# health-check.sh\n\nCONVEX_OK=$(curl -sf http:\u002F\u002Flocalhost:3210\u002Fversion && echo \"ok\" || echo \"fail\")\nWEB_OK=$(curl -sf http:\u002F\u002Flocalhost:3000 && echo \"ok\" || echo \"fail\")\nMTA_OK=$(curl -sf http:\u002F\u002Flocalhost:3100\u002Fhealth && echo \"ok\" || echo \"fail\")\n\necho \"Convex: $CONVEX_OK | Web: $WEB_OK | MTA: $MTA_OK\"\n\nif [[ \"$CONVEX_OK\" != \"ok\" || \"$WEB_OK\" != \"ok\" || \"$MTA_OK\" != \"ok\" ]]; then\n  exit 1  # Unhealthy — trigger your alerting system\nfi\n",[42,1206,1207,1212,1217,1221,1260,1290,1320,1324,1349,1353,1404,1415],{"__ignoreMap":67},[71,1208,1209],{"class":73,"line":74},[71,1210,1211],{"class":348},"#!\u002Fbin\u002Fbash\n",[71,1213,1214],{"class":73,"line":119},[71,1215,1216],{"class":348},"# health-check.sh\n",[71,1218,1219],{"class":73,"line":125},[71,1220,135],{"emptyLinePlaceholder":134},[71,1222,1223,1226,1228,1231,1234,1237,1240,1243,1245,1248,1251,1254,1257],{"class":73,"line":131},[71,1224,1225],{"class":266},"CONVEX_OK",[71,1227,271],{"class":270},[71,1229,1230],{"class":266},"$(",[71,1232,1233],{"class":77},"curl",[71,1235,1236],{"class":227}," -sf",[71,1238,1239],{"class":81}," http:\u002F\u002Flocalhost:3210\u002Fversion",[71,1241,1242],{"class":266}," && ",[71,1244,1013],{"class":227},[71,1246,1247],{"class":81}," \"ok\"",[71,1249,1250],{"class":270}," ||",[71,1252,1253],{"class":227}," echo",[71,1255,1256],{"class":81}," \"fail\"",[71,1258,1259],{"class":266},")\n",[71,1261,1262,1265,1267,1269,1271,1273,1276,1278,1280,1282,1284,1286,1288],{"class":73,"line":138},[71,1263,1264],{"class":266},"WEB_OK",[71,1266,271],{"class":270},[71,1268,1230],{"class":266},[71,1270,1233],{"class":77},[71,1272,1236],{"class":227},[71,1274,1275],{"class":81}," http:\u002F\u002Flocalhost:3000",[71,1277,1242],{"class":266},[71,1279,1013],{"class":227},[71,1281,1247],{"class":81},[71,1283,1250],{"class":270},[71,1285,1253],{"class":227},[71,1287,1256],{"class":81},[71,1289,1259],{"class":266},[71,1291,1292,1295,1297,1299,1301,1303,1306,1308,1310,1312,1314,1316,1318],{"class":73,"line":144},[71,1293,1294],{"class":266},"MTA_OK",[71,1296,271],{"class":270},[71,1298,1230],{"class":266},[71,1300,1233],{"class":77},[71,1302,1236],{"class":227},[71,1304,1305],{"class":81}," http:\u002F\u002Flocalhost:3100\u002Fhealth",[71,1307,1242],{"class":266},[71,1309,1013],{"class":227},[71,1311,1247],{"class":81},[71,1313,1250],{"class":270},[71,1315,1253],{"class":227},[71,1317,1256],{"class":81},[71,1319,1259],{"class":266},[71,1321,1322],{"class":73,"line":150},[71,1323,135],{"emptyLinePlaceholder":134},[71,1325,1326,1328,1331,1334,1337,1340,1343,1346],{"class":73,"line":155},[71,1327,1013],{"class":227},[71,1329,1330],{"class":81}," \"Convex: ",[71,1332,1333],{"class":266},"$CONVEX_OK",[71,1335,1336],{"class":81}," | Web: ",[71,1338,1339],{"class":266},"$WEB_OK",[71,1341,1342],{"class":81}," | MTA: ",[71,1344,1345],{"class":266},"$MTA_OK",[71,1347,1348],{"class":81},"\"\n",[71,1350,1351],{"class":73,"line":160},[71,1352,135],{"emptyLinePlaceholder":134},[71,1354,1355,1358,1361,1364,1366,1368,1371,1373,1375,1378,1380,1382,1384,1386,1388,1390,1392,1394,1396,1398,1401],{"class":73,"line":166},[71,1356,1357],{"class":270},"if",[71,1359,1360],{"class":266}," [[ ",[71,1362,1363],{"class":81},"\"",[71,1365,1333],{"class":266},[71,1367,1363],{"class":81},[71,1369,1370],{"class":270}," !=",[71,1372,1247],{"class":81},[71,1374,1250],{"class":270},[71,1376,1377],{"class":81}," \"",[71,1379,1339],{"class":266},[71,1381,1363],{"class":81},[71,1383,1370],{"class":270},[71,1385,1247],{"class":81},[71,1387,1250],{"class":270},[71,1389,1377],{"class":81},[71,1391,1345],{"class":266},[71,1393,1363],{"class":81},[71,1395,1370],{"class":270},[71,1397,1247],{"class":81},[71,1399,1400],{"class":266}," ]]; ",[71,1402,1403],{"class":270},"then\n",[71,1405,1406,1409,1412],{"class":73,"line":172},[71,1407,1408],{"class":227},"  exit",[71,1410,1411],{"class":227}," 1",[71,1413,1414],{"class":348},"  # Unhealthy — trigger your alerting system\n",[71,1416,1417],{"class":73,"line":478},[71,1418,1419],{"class":270},"fi\n",[34,1421,1423],{"id":1422},"mta-metriken","MTA-Metriken",[11,1425,1426],{},"Der MTA stellt Prometheus-kompatible Metriken bereit:",[62,1428,1430],{"className":64,"code":1429,"language":66,"meta":67,"style":67},"curl http:\u002F\u002Flocalhost:3100\u002Fmetrics\n",[42,1431,1432],{"__ignoreMap":67},[71,1433,1434,1436],{"class":73,"line":74},[71,1435,1233],{"class":77},[71,1437,1438],{"class":81}," http:\u002F\u002Flocalhost:3100\u002Fmetrics\n",[11,1440,1441],{},"Verbinden Sie diese mit Prometheus + Grafana, um Dashboards für Versandraten, Bounce-Raten und Queue-Tiefe zu erhalten.",[26,1443,1445],{"id":1444},"ressourcenanforderungen","Ressourcenanforderungen",[11,1447,1448],{},"Dies sind informelle Größenklassen, keine Tarife — der OSS-Self-Host-Build kennt weder Stufen noch Abrechnung.",[1450,1451,1452,1474],"table",{},[1453,1454,1455],"thead",{},[1456,1457,1458,1462,1465,1468,1471],"tr",{},[1459,1460,1461],"th",{},"Größe",[1459,1463,1464],{},"vCPU",[1459,1466,1467],{},"RAM",[1459,1469,1470],{},"Festplatte",[1459,1472,1473],{},"Geeignet für",[1475,1476,1477,1495,1512],"tbody",{},[1456,1478,1479,1483,1486,1489,1492],{},[1480,1481,1482],"td",{},"Klein",[1480,1484,1485],{},"2",[1480,1487,1488],{},"4 GB",[1480,1490,1491],{},"40 GB",[1480,1493,1494],{},"Bis zu 10.000 Kontakte, geringes Versandvolumen",[1456,1496,1497,1500,1503,1506,1509],{},[1480,1498,1499],{},"Mittel",[1480,1501,1502],{},"4",[1480,1504,1505],{},"8 GB",[1480,1507,1508],{},"80 GB",[1480,1510,1511],{},"Bis zu 100.000 Kontakte, regelmäßige Kampagnen",[1456,1513,1514,1517,1520,1523,1526],{},[1480,1515,1516],{},"Groß",[1480,1518,1519],{},"8",[1480,1521,1522],{},"16 GB",[1480,1524,1525],{},"160 GB",[1480,1527,1528],{},"100.000+ Kontakte, hohes Versandvolumen",[11,1530,1531,1532,246,1535,1538],{},"ClamAV belegt für die Virendefinitionen etwa 1 GB RAM. Wenn der Arbeitsspeicher knapp ist, können Sie ClamAV deaktivieren, indem Sie es aus der Docker-Compose-Datei entfernen und die Konfiguration von ",[42,1533,1534],{},"CLAMAV_HOST",[42,1536,1537],{},"CLAMAV_PORT"," weglassen.",[1540,1541,1542],"style",{},"html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}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);}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sDN9O, html code.shiki .sDN9O{--shiki-default:#6A737D;--shiki-dark:#768390}html pre.shiki code .sIAta, html code.shiki .sIAta{--shiki-default:#22863A;--shiki-dark:#8DDB8C}",{"title":67,"searchDepth":119,"depth":119,"links":1544},[1545,1553,1554,1555,1556,1559,1563],{"id":28,"depth":119,"text":29,"children":1546},[1547,1548,1549,1550,1551,1552],{"id":36,"depth":125,"text":37},{"id":59,"depth":125,"text":60},{"id":88,"depth":125,"text":89},{"id":195,"depth":125,"text":196},{"id":210,"depth":125,"text":211},{"id":377,"depth":125,"text":378},{"id":588,"depth":119,"text":589},{"id":781,"depth":119,"text":777},{"id":918,"depth":119,"text":919},{"id":1058,"depth":119,"text":1059,"children":1557},[1558],{"id":1122,"depth":125,"text":1123},{"id":1176,"depth":119,"text":1177,"children":1560},[1561,1562],{"id":1180,"depth":125,"text":1181},{"id":1422,"depth":125,"text":1423},{"id":1444,"depth":119,"text":1445},"Ihre selbst gehostete Owlat-Instanz mit TLS, Firewall-Regeln, Backups und Monitoring absichern.","md",{},"\u002Fdeveloper\u002Fself-hosting-production",{"title":6,"description":1564},"3.developer\u002F33.self-hosting-production","JR5wPmgHzQvEk4CoB8bCdaO4yTuusKSTKvicRoR_DrU",[1572,1576],{"title":1573,"path":1574,"stem":1575,"children":-1},"DNS- & E-Mail-Setup","\u002Fdeveloper\u002Fself-hosting-dns-email","3.developer\u002F32.self-hosting-dns-email",{"title":1577,"path":1578,"stem":1579,"children":-1},"Wartung & Updates","\u002Fdeveloper\u002Fself-hosting-maintenance","3.developer\u002F34.self-hosting-maintenance",1786915110053]