[{"data":1,"prerenderedAt":990},["ShallowReactive",2],{"repo-tree":3,"repo-\u002Flearning-api-preview-hetzner-setup":283},[4,7,10,13,16,19,22,25,28,31,34,37,40,43,46,49,52,55,58,61,64,67,69,72,75,78,81,84,86,88,90,93,96,99,102,105,108,111,114,117,120,123,125,127,129,131,133,135,138,141,143,146,149,152,155,158,161,164,167,169,172,175,178,180,183,186,189,192,195,198,201,203,206,209,212,215,218,221,224,227,230,233,236,239,242,245,248,251,254,257,260,263,266,269,272,275,278,281],{"path":5,"title":6},"\u002Fagents\u002Fbackend-code-style","Backend Conventions",{"path":8,"title":9},"\u002Fagents\u002Fdatabase","Database",{"path":11,"title":12},"\u002Fagents\u002Fportal-code-style","Portal Conventions",{"path":14,"title":15},"\u002Fagents\u002Ftranslation","Translation",{"path":17,"title":18},"\u002Fconventions\u002Fbackend-coding","Backend coding conventions",{"path":20,"title":21},"\u002Fconventions\u002Ffrontend-coding","Frontend coding conventions",{"path":23,"title":24},"\u002Fdevelopment-process","Development process",{"path":26,"title":27},"\u002Flearning-api-preview-hetzner-setup","Learning API Preview on Hetzner + Cloudflare",{"path":29,"title":30},"\u002Flearning-api-preview-vm-plan","Learning API Preview VM Plan",{"path":32,"title":33},"\u002Fmonorepo-structure","Monorepo structure",{"path":35,"title":36},"\u002Foperations","Operations — bugs and support",{"path":38,"title":39},"\u002Fpostmortems\u002F2026-03-16_onboarding-currency-regression","Onboarding Zod transform silently broken — web signups assigned wrong checkout currency",{"path":41,"title":42},"\u002Fpostmortems\u002Freadme","Postmortems",{"path":44,"title":45},"\u002Fpostmortems\u002F_template","TEMPLATE",{"path":47,"title":48},"\u002Fpostmortems\u002Fposthog-comparison","Postmortem practice — comparison with PostHog",{"path":50,"title":51},"\u002Fpreview-environment-plan","Preview Environment Plan",{"path":53,"title":54},"\u002Fprinciples","Engineering principles",{"path":56,"title":57},"\u002Fworking-with-ai","Working with AI",{"path":59,"title":60},"\u002F.claude\u002Fskills\u002Feval-playground\u002Fskill","Eval Playground — Co-development Skill",{"path":62,"title":63},"\u002F.claude\u002Fskills\u002Ffigma-diff-section\u002Fskill","Figma Diff Section Pipeline",{"path":65,"title":66},"\u002Fagents","AGENTS.md",{"path":68,"title":66},"\u002Fclaude",{"path":70,"title":71},"\u002Freadme","Studyflash",{"path":73,"title":74},"\u002Fapps\u002Fcore-api\u002Fagents","Core API (apps\u002Fcore-api)",{"path":76,"title":77},"\u002Fapps\u002Fcore-api\u002Freadme","README",{"path":79,"title":80},"\u002Fapps\u002Femail-previews\u002Fagents","Email Previews (apps\u002Femail-previews)",{"path":82,"title":83},"\u002Fapps\u002Flanding-page\u002Fagents","Landing Page (apps\u002Flanding-page)",{"path":85,"title":83},"\u002Fapps\u002Flanding-page\u002Fclaude",{"path":87,"title":66},"\u002Fapps\u002Flearning-api\u002Fagents",{"path":89,"title":77},"\u002Fapps\u002Flearning-api\u002Freadme",{"path":91,"title":92},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Feval_metrics_design","Surface-Specific Eval Metrics Design",{"path":94,"title":95},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Ftest_set","Quiz Eval Test Set",{"path":97,"title":98},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Ffrontend\u002Freadme","React + TypeScript + Vite",{"path":100,"title":101},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Fknown-issues\u002Fcontent-pillar-shallow-coverage\u002Freadme","Content pillar misses subtopics in dense documents",{"path":103,"title":104},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Fknown-issues\u002Fdocling-empty-section-headers\u002Freadme","Empty section headers dropped by docling chunker",{"path":106,"title":107},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Fknown-issues\u002Fdocling-table-reading-order\u002Freadme","Table\u002Fbox layout causes wrong reading order",{"path":109,"title":110},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Fmetrics\u002Freadme","Quiz eval metrics — canonical rubrics",{"path":112,"title":113},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Freports\u002F2026-04-12-quiz-summary-feedback-current-state","Quiz and Summary Feedback Current State",{"path":115,"title":116},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Freports\u002F2026-04-24-quiz-eval-metrics","Quiz Evaluation Metrics",{"path":118,"title":119},"\u002Fapps\u002Flearning-api\u002Fevals-playground\u002Freports\u002F2026-05-01-quiz-eval-current-state","Quiz Eval Current State",{"path":121,"title":122},"\u002Fapps\u002Flearning-api\u002Fmonitoring\u002Freadme","Monitoring Stack",{"path":124,"title":77},"\u002Fapps\u002Flearning-api\u002Fshared\u002Freadme",{"path":126,"title":77},"\u002Fapps\u002Flearning-api\u002Fworkers\u002Flearning_agents\u002Fflashcard_agent\u002Freadme",{"path":128,"title":77},"\u002Fapps\u002Flearning-api\u002Fworkers\u002Flearning_agents\u002Fingestion_agent\u002Freadme",{"path":130,"title":77},"\u002Fapps\u002Flearning-api\u002Fworkers\u002Flearning_agents\u002Fquiz_agent\u002Freadme",{"path":132,"title":77},"\u002Fapps\u002Flearning-api\u002Fworkers\u002Flearning_agents\u002Fsummary_agent\u002Freadme",{"path":134,"title":77},"\u002Fapps\u002Flearning-api\u002Fworkers\u002Fparser\u002Freadme",{"path":136,"title":137},"\u002Fapps\u002Fmarketing-emails-preview\u002Fagents","Marketing Emails Preview (apps\u002Fmarketing-emails-preview)",{"path":139,"title":140},"\u002Fapps\u002Fmobile-app\u002Fagents","StudyFlash Mobile App - Claude Code Configuration",{"path":142,"title":140},"\u002Fapps\u002Fmobile-app\u002Fclaude",{"path":144,"title":145},"\u002Fapps\u002Fmountain-max\u002Fagents","Mountain Max (apps\u002Fmountain-max)",{"path":147,"title":148},"\u002Fapps\u002Fmountain-max\u002Fgame\u002Freadme","Mountain Max Game",{"path":150,"title":151},"\u002Fapps\u002Fportal\u002Fagents","Portal (apps\u002Fportal)",{"path":153,"title":154},"\u002Fapps\u002Fportal\u002Freadme","Nuxt Minimal Starter",{"path":156,"title":157},"\u002Fapps\u002Fportal\u002Fapp\u002Fcomposables\u002Ffiles\u002Freadme","File Upload Composables",{"path":159,"title":160},"\u002Fapps\u002Fportal\u002Fdocs\u002Flibrary-routing","Library Routing Documentation",{"path":162,"title":163},"\u002Fapps\u002Fsupabase\u002Fagents","Supabase (apps\u002Fsupabase)",{"path":165,"title":166},"\u002Fapps\u002Fwrapped\u002Fagents","Wrapped (apps\u002Fwrapped)",{"path":168,"title":98},"\u002Fapps\u002Fwrapped\u002Freadme",{"path":170,"title":171},"\u002Finfra\u002Freadme","infra\u002F",{"path":173,"title":174},"\u002Finfra\u002Fdns\u002Freadme","DNS Infrastructure",{"path":176,"title":177},"\u002Finfra\u002Fdokploy\u002Freadme","studyflash-dokploy",{"path":179,"title":77},"\u002Finfra\u002Fdokploy\u002Fsdk\u002Fnodejs\u002Freadme",{"path":181,"title":182},"\u002Finfra\u002Finfisical\u002Freadme","Infisical Infrastructure",{"path":184,"title":185},"\u002Finfra\u002Flearning-api\u002Freadme","Pulumi GCP TypeScript Template",{"path":187,"title":188},"\u002Finfra\u002Fopenreplay\u002Freadme","OpenReplay on Hetzner",{"path":190,"title":191},"\u002Finfra\u002Fscripts\u002Freadme","infra\u002Fscripts\u002F",{"path":193,"title":194},"\u002Finfra\u002Fturborepo-cache\u002Freadme","Turborepo Remote Cache Infrastructure",{"path":196,"title":197},"\u002Finternal\u002Fchatwoot\u002Freadme","Chatwoot Infrastructure",{"path":199,"title":200},"\u002Finternal\u002Fchatwoot\u002Fprovider\u002Freadme","studyflash-chatwoot-provider",{"path":202,"title":77},"\u002Finternal\u002Fchatwoot\u002Fprovider\u002Fsdk\u002Fnodejs\u002Freadme",{"path":204,"title":205},"\u002Finternal\u002Fdocs\u002Freadme","internal\u002Fdocs",{"path":207,"title":208},"\u002Finternal\u002Fsupport-bot\u002Fclaude","Support Bot (Maximilian)",{"path":210,"title":211},"\u002Finternal\u002Fsupport-bot\u002Freadme","Studyflash Customer Support Bot (Maximilian)",{"path":213,"title":214},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Faccount_issues","Account Issues",{"path":216,"title":217},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fbilling_invoice","Billing Invoice",{"path":219,"title":220},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fcontent_upload","Content Upload",{"path":222,"title":223},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fdata_loss","Data Loss",{"path":225,"title":226},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fflashcard_issues","Flashcard Issues",{"path":228,"title":229},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fgarbage","Garbage",{"path":231,"title":232},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fgeneral_how_to","General How To",{"path":234,"title":235},"\u002Finternal\u002Fsupport-bot\u002Fkb","Knowledge Base Index",{"path":237,"title":238},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Flanguage_issues","Language Issues",{"path":240,"title":241},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fmindmap_issues","Mindmap Issues",{"path":243,"title":244},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fmisunderstanding","Misunderstanding",{"path":246,"title":247},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fmock_exam_issues","Mock Exam Issues",{"path":249,"title":250},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fpodcast_issues","Podcast Issues",{"path":252,"title":253},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fquiz_issues","Quiz Issues",{"path":255,"title":256},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Frefund_request","Refund Request",{"path":258,"title":259},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fsubscription_cancellation","Subscription Cancellation",{"path":261,"title":262},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fsubscription_info","Subscription Info",{"path":264,"title":265},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fsummary_issues","Summary Issues",{"path":267,"title":268},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Ftechnical_errors","Technical Errors",{"path":270,"title":271},"\u002Finternal\u002Fsupport-bot\u002Fkb\u002Fvideo_issues","Video Issues",{"path":273,"title":274},"\u002Fpackages\u002Fcommon\u002Fdocs\u002Fearly-access-features","Declarative Early Access Features",{"path":276,"title":277},"\u002Fpackages\u002Fcommon\u002Fscripts\u002Freadme","Common Package Scripts",{"path":279,"title":280},"\u002Fpackages\u002Fdevtools\u002Ffigma-plugins\u002Freadme","Figma plugins",{"path":282,"title":77},"\u002Fpackages\u002Fpulumi-infisical\u002Freadme",{"id":284,"title":27,"body":285,"description":295,"extension":983,"lastReviewed":984,"meta":985,"navigation":986,"owner":984,"path":26,"seo":987,"status":984,"stem":988,"tags":984,"__hash__":989},"handbook\u002Flearning-api-preview-hetzner-setup.md",{"type":286,"value":287,"toc":968},"minimark",[288,292,296,301,330,333,337,340,353,356,449,452,472,475,493,495,499,502,535,538,555,557,561,568,581,584,600,602,606,611,663,667,695,699,729,731,735,767,769,773,780,886,891,899,901,905,944,946,950,964],[289,290,27],"h1",{"id":291},"learning-api-preview-on-hetzner-cloudflare",[293,294,295],"p",{},"This runbook covers what you need to do manually to make the automated preview workflow work.",[297,298,300],"h2",{"id":299},"what-this-setup-gives-you","What This Setup Gives You",[302,303,304,313,323],"ul",{},[305,306,307,308,312],"li",{},"One shared Hetzner VM hosting multiple PR preview stacks for ",[309,310,311],"code",{},"learning-api",".",[305,314,315,316],{},"One stable public base URL (behind Cloudflare), with per-PR path routing:\n",[302,317,318],{},[305,319,320],{},[309,321,322],{},"https:\u002F\u002F\u003Cbase-host>\u002Fpr-\u003CPR_NUMBER>",[305,324,325,326,329],{},"Existing preview workflow sets core-api preview ",[309,327,328],{},"LEARNING_API_BASE_URL"," to that PR URL.",[331,332],"hr",{},[297,334,336],{"id":335},"_1-hetzner-vm-provisioning","1) Hetzner VM Provisioning",[293,338,339],{},"Recommended starter VM:",[302,341,342,347,350],{},[305,343,344],{},[309,345,346],{},"16 vCPU \u002F 32 GB RAM \u002F 250 GB NVMe",[305,348,349],{},"Ubuntu 22.04 or 24.04",[305,351,352],{},"Public IPv4",[293,354,355],{},"Provision with Hetzner CLI (manual):",[357,358,363],"pre",{"className":359,"code":360,"language":361,"meta":362,"style":362},"language-bash shiki shiki-themes github-light github-dark","hcloud server create \\\n  --name studyflash-learning-api-preview \\\n  --type cpx52 \\\n  --image ubuntu-24.04 \\\n  --location nbg1 \\\n  --ssh-key \u003Cyour_ssh_key_name>\n","bash","",[309,364,365,385,396,407,418,429],{"__ignoreMap":362},[366,367,370,374,378,381],"span",{"class":368,"line":369},"line",1,[366,371,373],{"class":372},"sScJk","hcloud",[366,375,377],{"class":376},"sZZnC"," server",[366,379,380],{"class":376}," create",[366,382,384],{"class":383},"sj4cs"," \\\n",[366,386,388,391,394],{"class":368,"line":387},2,[366,389,390],{"class":383},"  --name",[366,392,393],{"class":376}," studyflash-learning-api-preview",[366,395,384],{"class":383},[366,397,399,402,405],{"class":368,"line":398},3,[366,400,401],{"class":383},"  --type",[366,403,404],{"class":376}," cpx52",[366,406,384],{"class":383},[366,408,410,413,416],{"class":368,"line":409},4,[366,411,412],{"class":383},"  --image",[366,414,415],{"class":376}," ubuntu-24.04",[366,417,384],{"class":383},[366,419,421,424,427],{"class":368,"line":420},5,[366,422,423],{"class":383},"  --location",[366,425,426],{"class":376}," nbg1",[366,428,384],{"class":383},[366,430,432,435,439,442,446],{"class":368,"line":431},6,[366,433,434],{"class":383},"  --ssh-key",[366,436,438],{"class":437},"szBVR"," \u003C",[366,440,441],{"class":376},"your_ssh_key_nam",[366,443,445],{"class":444},"sVt8B","e",[366,447,448],{"class":437},">\n",[293,450,451],{},"Then SSH in and bootstrap:",[302,453,454,457,460,466],{},[305,455,456],{},"Docker Engine + Docker Compose plugin",[305,458,459],{},"cloudflared (Cloudflare Tunnel client)",[305,461,462,463],{},"preview root directory ",[309,464,465],{},"\u002Fopt\u002Fstudyflash\u002Flearning-api-previews",[305,467,468,469],{},"shared Docker network ",[309,470,471],{},"learning-api-preview-proxy",[293,473,474],{},"Make sure the SSH user used by GitHub Actions can run:",[302,476,477,482,487],{},[305,478,479],{},[309,480,481],{},"docker",[305,483,484],{},[309,485,486],{},"docker compose",[305,488,489,492],{},[309,490,491],{},"docker run"," (one-time Traefik bootstrap is handled by deploy script)",[331,494],{},[297,496,498],{"id":497},"_2-cloudflare-setup","2) Cloudflare Setup",[293,500,501],{},"You need one Cloudflare Tunnel from the VM to Cloudflare.",[503,504,505,508,518,521,532],"ol",{},[305,506,507],{},"Create a tunnel and install credentials on the VM.",[305,509,510,511],{},"Create DNS for one host, for example:\n",[302,512,513],{},[305,514,515],{},[309,516,517],{},"learning-api-preview.preview.your-domain.com",[305,519,520],{},"Point that DNS host to the tunnel.",[305,522,523,524],{},"Configure tunnel ingress to forward that host to local Traefik:\n",[302,525,526],{},[305,527,528,529],{},"service: ",[309,530,531],{},"http:\u002F\u002Flocalhost:8080",[305,533,534],{},"Run cloudflared as a system service.",[293,536,537],{},"Result:",[302,539,540],{},[305,541,542,543],{},"All PR routes are path-based under the same host:\n",[302,544,545],{},[305,546,547,550,551,554],{},[309,548,549],{},"\u002Fpr-123",", ",[309,552,553],{},"\u002Fpr-456",", etc.",[331,556],{},[297,558,560],{"id":559},"_3-traefik-dynamic-router-auto-managed","3) Traefik Dynamic Router (Auto-Managed)",[293,562,563,564,567],{},"The deploy script auto-creates a Traefik container (",[309,565,566],{},"learning-api-preview-traefik",") if missing and keeps it running on:",[302,569,570,576],{},[305,571,572,575],{},[309,573,574],{},"127.0.0.1:8080"," (for cloudflared ingress)",[305,577,578,579],{},"Docker network: ",[309,580,471],{},[293,582,583],{},"Each PR stack adds Docker labels on the API container:",[302,585,586,591,597],{},[305,587,588],{},[309,589,590],{},"Host(\u003Cpreview-host>) && PathPrefix(\u002Fpr-\u003Cn>)",[305,592,593,594,312],{},"Strip-prefix middleware so backend still serves at ",[309,595,596],{},"\u002F",[305,598,599],{},"No per-PR web server files, no reloads.",[331,601],{},[297,603,605],{"id":604},"_4-github-repo-configuration","4) GitHub Repo Configuration",[607,608,610],"h3",{"id":609},"required-repository-secrets","Required Repository Secrets",[302,612,613,623,633,651],{},[305,614,615,618],{},[309,616,617],{},"HETZNER_PREVIEW_HOST",[302,619,620],{},[305,621,622],{},"VM IP or hostname",[305,624,625,628],{},[309,626,627],{},"HETZNER_PREVIEW_SSH_PRIVATE_KEY",[302,629,630],{},[305,631,632],{},"Private key corresponding to SSH public key authorized on VM",[305,634,635,638],{},[309,636,637],{},"HETZNER_PREVIEW_SSH_USER",[302,639,640],{},[305,641,642,643,646,647,650],{},"Example: ",[309,644,645],{},"deploy-preview"," (falls back to ",[309,648,649],{},"root"," if empty)",[305,652,653,656],{},[309,654,655],{},"LEARNING_API_PREVIEW_BASE_URL",[302,657,658],{},[305,659,642,660],{},[309,661,662],{},"https:\u002F\u002Flearning-api-preview.preview.your-domain.com",[607,664,666],{"id":665},"optional-fallback-secrets","Optional Fallback Secrets",[302,668,669,685],{},[305,670,671,674],{},[309,672,673],{},"CORE_API_UAT_BASE_URL",[302,675,676,679],{},[305,677,678],{},"UAT core-api URL used when core-api preview is skipped.",[305,680,681,682],{},"Default if unset: ",[309,683,684],{},"https:\u002F\u002Fapi.uat.studyflash.ch",[305,686,687,690],{},[309,688,689],{},"LEARNING_API_UAT_BASE_URL",[302,691,692],{},[305,693,694],{},"UAT learning-api URL used when learning-api preview is skipped.",[607,696,698],{"id":697},"required-infisical-access","Required Infisical Access",[302,700,701],{},[305,702,703,704,707,708],{},"The GitHub machine identity used by ",[309,705,706],{},"pnpm run infisical-auth"," must have read access to:\n",[302,709,710,721],{},[305,711,712,713,716,717,720],{},"path ",[309,714,715],{},"\u002Fbackend\u002F"," in ",[309,718,719],{},"staging"," for core-api preview worker secrets",[305,722,712,723,716,726,728],{},[309,724,725],{},"\u002Flearning-api\u002F",[309,727,719],{}," for learning-api base preview env",[331,730],{},[297,732,734],{"id":733},"_5-files-added-for-automation","5) Files Added for Automation",[302,736,737,742,747,752],{},[305,738,739],{},[309,740,741],{},"apps\u002Flearning-api\u002Fdocker-compose.preview.yml",[305,743,744],{},[309,745,746],{},"apps\u002Flearning-api\u002Fscripts\u002Fpreview\u002Fdeploy_learning_api_preview.sh",[305,748,749],{},[309,750,751],{},"apps\u002Flearning-api\u002Fscripts\u002Fpreview\u002Fcleanup_learning_api_preview.sh",[305,753,754,755],{},"Workflow integration:\n",[302,756,757,762],{},[305,758,759],{},[309,760,761],{},".github\u002Fworkflows\u002Fdeploy_preview_env.yaml",[305,763,764],{},[309,765,766],{},".github\u002Fworkflows\u002Fcleanup-preview.yml",[331,768],{},[297,770,772],{"id":771},"_6-preview-lifecycle","6) Preview Lifecycle",[293,774,775,776,779],{},"On PR ",[309,777,778],{},"opened\u002Freopened\u002Fsynchronize",":",[503,781,782,794,800,811,824,883],{},[305,783,784,785,550,788,550,790,793],{},"Workflow determines changed scopes (",[309,786,787],{},"core-api",[309,789,311],{},[309,791,792],{},"supabase"," schema\u002Fmigrations).",[305,795,796,799],{},[309,797,798],{},"portal"," preview always deploys.",[305,801,802,804,805,807,808,810],{},[309,803,311],{}," preview deploys only when ",[309,806,311],{}," or ",[309,809,792],{}," schema changed.",[305,812,813,815,816,818,819,596,821,823],{},[309,814,787],{}," preview deploys when ",[309,817,787],{}," changed, or when dependency chain requires it (",[309,820,311],{},[309,822,792],{}," change).",[305,825,826,827,829,830,832,833,835,836],{},"If ",[309,828,311],{}," preview is deployed, workflow exports ",[309,831,725],{}," secrets from Infisical ",[309,834,719],{}," and overrides preview runtime values:\n",[302,837,838,843,848,858],{},[305,839,840],{},[309,841,842],{},"SUPABASE_URL",[305,844,845],{},[309,846,847],{},"SUPABASE_SERVICE_ROLE_KEY",[305,849,850,853,854,857],{},[309,851,852],{},"API_KEY"," (aligned with core-api preview ",[309,855,856],{},"LEARNING_API_KEY",")",[305,859,860,861],{},"internal service wiring is always forced at runtime:\n",[302,862,863,868,873,878],{},[305,864,865],{},[309,866,867],{},"API_URL=http:\u002F\u002Fapi:8000",[305,869,870],{},[309,871,872],{},"REDIS_URL=redis:\u002F\u002Fredis:6379\u002F0",[305,874,875],{},[309,876,877],{},"CELERY_BROKER_URL=redis:\u002F\u002Fredis:6379\u002F0",[305,879,880],{},[309,881,882],{},"CELERY_RESULT_BACKEND=redis:\u002F\u002Fredis:6379\u002F0",[305,884,885],{},"If preview deploy is skipped for a service, workflow reuses configured UAT URL for that service.",[293,887,775,888,779],{},[309,889,890],{},"closed",[503,892,893,896],{},[305,894,895],{},"Workflow runs cleanup script on Hetzner.",[305,897,898],{},"Stack is removed; Traefik route disappears automatically when container is gone.",[331,900],{},[297,902,904],{"id":903},"_7-validate-end-to-end","7) Validate End-to-End",[503,906,907,910,917,920,930,933],{},[305,908,909],{},"Open a PR.",[305,911,912,913,916],{},"Wait for ",[309,914,915],{},"Deploy Preview Environment"," workflow.",[305,918,919],{},"Verify PR comment contains Learning API preview URL.",[305,921,922,923],{},"Open:\n",[302,924,925],{},[305,926,927],{},[309,928,929],{},"https:\u002F\u002F\u003Cbase-host>\u002Fpr-\u003CPR_NUMBER>\u002Fdocs",[305,931,932],{},"Trigger a core-api path that calls learning-api and confirm no 5xx from base URL mismatch.",[305,934,935,936],{},"Close PR and verify cleanup removed:\n",[302,937,938],{},[305,939,940,941],{},"Docker project ",[309,942,943],{},"pr-\u003CPR_NUMBER>",[331,945],{},[297,947,949],{"id":948},"_8-operational-notes","8) Operational Notes",[302,951,952,955,958,961],{},[305,953,954],{},"This setup intentionally avoids one DNS record per PR.",[305,956,957],{},"Path-based routing simplifies Cloudflare and keeps lock-in low.",[305,959,960],{},"Traefik with Docker provider removes route file management overhead.",[305,962,963],{},"If parser previews are needed later, add an opt-in path (label\u002Fmanual trigger), not default for every PR.",[965,966,967],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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":362,"searchDepth":387,"depth":387,"links":969},[970,971,972,973,974,979,980,981,982],{"id":299,"depth":387,"text":300},{"id":335,"depth":387,"text":336},{"id":497,"depth":387,"text":498},{"id":559,"depth":387,"text":560},{"id":604,"depth":387,"text":605,"children":975},[976,977,978],{"id":609,"depth":398,"text":610},{"id":665,"depth":398,"text":666},{"id":697,"depth":398,"text":698},{"id":733,"depth":387,"text":734},{"id":771,"depth":387,"text":772},{"id":903,"depth":387,"text":904},{"id":948,"depth":387,"text":949},"md",null,{},true,{"title":27,"description":295},"learning-api-preview-hetzner-setup","gO2rUjaoFDDXH6qZdRxQI3Mjlt5yAM2fHZFwl79FbC8",1779007962943]