{"id":1,"date":"2026-04-12T02:07:12","date_gmt":"2026-04-12T02:07:12","guid":{"rendered":"https:\/\/blog-api.minminworld.com\/?p=1"},"modified":"2026-04-12T15:45:28","modified_gmt":"2026-04-12T15:45:28","slug":"hello-world","status":"publish","type":"post","link":"https:\/\/blog-api.minminworld.com\/?p=1","title":{"rendered":"[FastAPI + Ollama] 404\uc758 \uc9c4\uc9dc \ubc94\uc778\uc744 \ucc3e\uc544\uc11c \u2014 Hunting the Real 404"},"content":{"rendered":"\n<style>\n* { box-sizing: border-box; margin: 0; padding: 0; }\n.blog-wrap { max-width: 780px; margin: 0 auto; padding: 2rem 1rem; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }\n.lang-toggle { display: flex; gap: 8px; margin-bottom: 2rem; }\n.lang-btn { padding: 6px 18px; border-radius: 20px; border: 1px solid #ddd; background: transparent; color: #888; font-size: 13px; cursor: pointer; transition: all 0.2s; }\n.lang-btn.active { background: #111; color: #fff; border-color: transparent; }\n.post { display: none; }\n.post.active { display: block; }\n.post-tag { display: inline-block; font-size: 11px; font-weight: 600; padding: 3px 10px; border-radius: 20px; background: #e8f0fe; color: #1a73e8; margin-bottom: 1.2rem; letter-spacing: 0.05em; }\n.post-title { font-size: 26px; font-weight: 600; color: #111; line-height: 1.35; margin-bottom: 0.8rem; }\n.post-subtitle { font-size: 15px; color: #555; line-height: 1.6; margin-bottom: 1.8rem; }\n.meta { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; font-size: 12px; color: #aaa; margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 1px solid #eee; }\n.meta-dot { width: 3px; height: 3px; border-radius: 50%; background: #ccc; }\n.arch-box { background: #f8f8f8; border-radius: 12px; padding: 1.2rem 1.5rem; margin: 1.5rem 0; }\n.arch-box p { font-size: 11px; color: #aaa; margin-bottom: 8px; }\n.arch-flow { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }\n.arch-node { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 4px 10px; color: #333; font-size: 12px; white-space: nowrap; }\n.arch-arrow { color: #bbb; font-size: 12px; }\n.section { margin: 2rem 0; }\n.section-header { display: flex; align-items: center; gap: 10px; margin-bottom: 1rem; }\n.section-icon { font-size: 18px; }\n.section-title { font-size: 17px; font-weight: 600; color: #111; }\n.body-text { font-size: 15px; color: #444; line-height: 1.75; margin-bottom: 1rem; }\n.highlight-box { border-left: 3px solid #1a73e8; padding: 0.8rem 1rem; margin: 1.2rem 0; background: #f0f4ff; border-radius: 0 8px 8px 0; }\n.highlight-box p { font-size: 14px; color: #333; line-height: 1.65; margin: 0; }\n.code-block { background: #1e1e1e; color: #d4d4d4; border-radius: 8px; padding: 1rem 1.2rem; margin: 1rem 0; font-family: 'Courier New', monospace; font-size: 13px; line-height: 1.7; overflow-x: auto; }\n.key-clue { background: #fffbea; border: 1px solid #f5c842; border-radius: 8px; padding: 0.8rem 1rem; margin: 1rem 0; }\n.key-clue p { font-size: 13.5px; color: #7a5c00; margin: 0; line-height: 1.6; }\n.divider { border: none; border-top: 1px solid #eee; margin: 2rem 0; }\n.lessons { margin: 1.5rem 0; display: flex; flex-direction: column; gap: 12px; }\n.lesson-item { display: flex; gap: 14px; align-items: flex-start; padding: 1rem 1.2rem; background: #f8f8f8; border-radius: 12px; }\n.lesson-num { font-size: 18px; flex-shrink: 0; }\n.lesson-content { flex: 1; }\n.lesson-title { font-size: 14px; font-weight: 600; color: #111; margin-bottom: 4px; }\n.lesson-desc { font-size: 13px; color: #666; line-height: 1.6; }\n.closing { background: #f0f4ff; border-radius: 12px; padding: 1.5rem; margin-top: 2rem; text-align: center; }\n.closing p { font-size: 15px; color: #555; line-height: 1.7; }\n.closing .em { color: #111; font-weight: 600; }\n.tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 1.5rem; }\n.tag { font-size: 11px; padding: 3px 10px; border-radius: 20px; border: 1px solid #e0e0e0; color: #999; }\n<\/style>\n\n<div class=\"blog-wrap\">\n  <div class=\"lang-toggle\">\n    <button class=\"lang-btn active\" onclick=\"switchLang('ko', this)\">\ud83c\uddf0\ud83c\uddf7 \ud55c\uad6d\uc5b4<\/button>\n    <button class=\"lang-btn\" onclick=\"switchLang('en', this)\">\ud83c\uddfa\ud83c\uddf8 English<\/button>\n  <\/div>\n\n  <!-- \ud55c\uad6d\uc5b4 -->\n  <div class=\"post active\" id=\"post-ko\">\n    <div class=\"post-tag\">\ud83d\udee0\ufe0f \ub514\ubc84\uae45 \uc77c\uc9c0<\/div>\n    <h2 class=\"post-title\">\ub77c\uc6b0\ud2b8\ub294 \uc788\ub294\ub370 \uc65c 404\uac00 \ub0a0\uae4c\uc694?<br>FastAPI + Ollama \uc0bd\uc9c8\uae30 \ud83d\udd0d<\/h2>\n    <p class=\"post-subtitle\">\uc9d1\uc5d0\uc11c mini AI-RAG\uc640 \ucc57\ubd07 \uc11c\ube44\uc2a4\ub97c \uc571\uc5d0 \uac04\uc2e0\ud788 \uc5f0\uacb0\ud588\uc2b5\ub2c8\ub2e4. \uc544\uc9c1 \ud504\ub85c\ud1a0\ud0c0\uc785\uc774\ub77c \ud14d\uc2a4\ud2b8\ubc16\uc5d0 \uc5c6\uc5b4 \ubcfc\ud488\uc5c6\uc9c0\ub9cc, \ub531 \ubd99\uc5ec\uc11c \ub418\uc5c8\uc744 \ub550 \uc815\ub9d0 \ubfcc\ub4ef\ud588\uc2b5\ub2c8\ub2e4. \uadf8 \uacfc\uc815\uc5d0\uc11c \uc0dd\uae34 \uc774\uc57c\uae30\uc785\ub2c8\ub2e4.<\/p>\n    <div class=\"meta\">\n      <span>\u26a1 FastAPI<\/span><span class=\"meta-dot\"><\/span>\n      <span>\ud83d\udc33 Docker<\/span><span class=\"meta-dot\"><\/span>\n      <span>\ud83e\udd16 Ollama<\/span><span class=\"meta-dot\"><\/span>\n      <span>\u2601\ufe0f Cloud Run<\/span>\n    <\/div>\n\n    <div class=\"arch-box\">\n      <p>\ud83c\udfd7\ufe0f \uc11c\ube44\uc2a4 \uad6c\uc131<\/p>\n      <div class=\"arch-flow\">\n        <span class=\"arch-node\">\ud83d\udcf1 \uc571<\/span><span class=\"arch-arrow\">\u2192<\/span>\n        <span class=\"arch-node\">\u2601\ufe0f Cloud Run<\/span><span class=\"arch-arrow\">\u2192<\/span>\n        <span class=\"arch-node\">\ud83d\udd12 Cloudflare<\/span><span class=\"arch-arrow\">\u2192<\/span>\n        <span class=\"arch-node\">\ud83c\udfe0 \uacf5\uc720\uae30<\/span><span class=\"arch-arrow\">\u2192<\/span>\n        <span class=\"arch-node\">\ud83d\udda5\ufe0f \uc6b4\uc601 \uc11c\ubc84<\/span><span class=\"arch-arrow\">\u2192<\/span>\n        <span class=\"arch-node\">\ud83d\udcbb \uac1c\ubc1c \uc11c\ubc84 (RTX 3060)<\/span>\n      <\/div>\n    <\/div>\n\n    <div class=\"section\">\n      <div class=\"section-header\"><span class=\"section-icon\">\ud83d\ude30<\/span><span class=\"section-title\">\ub808\uc774\uc5b4\uac00 \uc313\uc77c\uc218\ub85d \uc774\uc0c1\ud55c \uc77c\uc774 \uc0dd\uacbc\uc2b5\ub2c8\ub2e4<\/span><\/div>\n      <p class=\"body-text\">iptables + NordVPN + Docker\uac00 \uc5bd\ud788\uba74\uc11c \ud328\ud0b7\uc774 \uc0ac\ub77c\uc84c\uace0, tcpdump\ub85c \uaca8\uc6b0 \ubc29\ud5a5\uc744 \uc7a1\uc558\uc2b5\ub2c8\ub2e4. \ub9e5\ubd81 SSH \ud130\ub110\uc774 \ub04a\uacbc\ub2e4 \ubd99\uc5c8\ub2e4 \ud558\uba74\uc11c \uac1c\ubc1c \uc11c\ubc84\uac00 \uc694\uccad\uc744 \ud1b5\uc9f8\ub85c \uc0bc\ud0a4\ub294 \uc99d\uc0c1\ub3c4 \uc788\uc5c8\uc2b5\ub2c8\ub2e4. \ub85c\uadf8\uc5d4 \uc544\ubb34\uac83\ub3c4 \uc548 \ucc0d\ud788\ub294\ub370 \uc11c\ubc84\ub294 \uc0b4\uc544\uc788\ub294 \uc0c1\ud669 \u2014 \uaed0\ub2e4 \ucf1c\ub2c8\uae4c \ubc14\ub85c \ud574\uacb0\ub410\uc2b5\ub2c8\ub2e4.<\/p>\n      <p class=\"body-text\">\uadf8\uac78 \ub6ab\uace0 \ub098\ub2c8 \uc774\ubc88\uc5d4 404\uac00 \uae30\ub2e4\ub9ac\uace0 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.<\/p>\n    <\/div>\n\n    <div class=\"section\">\n      <div class=\"section-header\"><span class=\"section-icon\">\ud83d\udea8<\/span><span class=\"section-title\">\uc99d\uc0c1: \ub77c\uc6b0\ud2b8\ub294 \uc788\ub294\ub370 404<\/span><\/div>\n      <p class=\"body-text\">Docker \ucee8\ud14c\uc774\ub108\uc5d0\uc11c <code>POST \/internal\/llm\/chat<\/code> \uc694\uccad\uc774 \uacc4\uc18d 404\ub97c \ubc18\ud658\ud588\uc2b5\ub2c8\ub2e4. \uac1c\ubc1c \uc11c\ubc84\uc5d0\uc11c\ub294 \uc815\uc0c1 \ub3d9\uc791. \ub77c\uc6b0\ud2b8 \ubaa9\ub85d\uc744 \uc9c1\uc811 \ucd9c\ub825\ud574\ubd24\ub354\ub2c8 \uc5d4\ub4dc\ud3ec\uc778\ud2b8\ub294 \ubd84\uba85\ud788 \ub4f1\ub85d\ub418\uc5b4 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.<\/p>\n      <div class=\"highlight-box\"><p>\ud83d\udca1 \ub0b4\ubd80(127.0.0.1)\uc5d0\uc11c\ub294 <strong>401 Unauthorized<\/strong>, \uc678\ubd80(nginx)\uc5d0\uc11c\ub294 <strong>404 Not Found<\/strong>. \uac19\uc740 \ucee8\ud14c\uc774\ub108, \uac19\uc740 \ud504\ub85c\uc138\uc2a4\uc5d0\uc11c \ub2e4\ub978 \uacb0\uacfc\uac00 \ub098\uc654\uc2b5\ub2c8\ub2e4.<\/p><\/div>\n    <\/div>\n\n    <div class=\"section\">\n      <div class=\"section-header\"><span class=\"section-icon\">\ud83d\udd0e<\/span><span class=\"section-title\">\uacb0\uc815\uc801 \ub2e8\uc11c: \uc751\ub2f5 \ud06c\uae30<\/span><\/div>\n      <p class=\"body-text\">nginx \uc561\uc138\uc2a4 \ub85c\uadf8\uc758 \uc751\ub2f5 \ud06c\uae30\uac00 \uc774\uc0c1\ud588\uc2b5\ub2c8\ub2e4.<\/p>\n      <div class=\"key-clue\"><p>\u26a0\ufe0f FastAPI \uae30\ubcf8 404 \uc751\ub2f5 <code>{\"detail\":\"Not Found\"}<\/code> = <strong>22\ubc14\uc774\ud2b8<\/strong><br>\uc2e4\uc81c \uc751\ub2f5 \ud06c\uae30 = <strong>64~89\ubc14\uc774\ud2b8<\/strong><br>\ub204\uad70\uac00 \ub2e4\ub978 404\ub97c \ub9cc\ub4e4\uace0 \uc788\uc5c8\uc2b5\ub2c8\ub2e4.<\/p><\/div>\n      <p class=\"body-text\">\uc5ed\uc0b0\ud574\ubcf4\ub2c8 Ollama\uc758 \uc5d0\ub7ec \uba54\uc2dc\uc9c0\uc600\uc2b5\ub2c8\ub2e4.<\/p>\n      <div class=\"code-block\">{&#8220;detail&#8221;:&#8221;model &#8216;llama3&#8217; not found, try pulling it first&#8221;}<\/div>\n    <\/div>\n\n    <div class=\"section\">\n      <div class=\"section-header\"><span class=\"section-icon\">\ud83d\udd75\ufe0f<\/span><span class=\"section-title\">\uc6d0\uc778: upstream \uc5d0\ub7ec \uc804\ud30c<\/span><\/div>\n      <p class=\"body-text\">\ud074\ub77c\uc774\uc5b8\ud2b8 \uc11c\ube44\uc2a4\uac00 GCP Secret Manager\uc5d0\uc11c <code>OLLAMA_MODEL=llama3<\/code>\ub97c \uc77d\uc5b4 \uc694\uccad body\uc5d0 \ub2f4\uc544 \ubcf4\ub0b4\uace0 \uc788\uc5c8\uace0, \uc11c\ubc84\uc5d0 \uc2e4\uc81c \uc124\uce58\ub41c \ubaa8\ub378\uc740 <code>llama3.1:8b<\/code>\uc600\uc2b5\ub2c8\ub2e4. Ollama\uc758 404\uac00 FastAPI\ub97c \ud1b5\ud574 \uadf8\ub300\ub85c \uc804\ud30c\ub41c \uac83\uc785\ub2c8\ub2e4. \ub77c\uc6b0\ud305 \ubb38\uc81c\uac00 \uc544\ub2c8\ub77c upstream \uc5d0\ub7ec \uc804\ud30c \ubb38\uc81c\uc600\uc2b5\ub2c8\ub2e4.<\/p>\n    <\/div>\n\n    <div class=\"section\">\n      <div class=\"section-header\"><span class=\"section-icon\">\ud83e\udd16<\/span><span class=\"section-title\">AI\uc640 \ud568\uaed8 \ud310 \uc774\uc57c\uae30<\/span><\/div>\n      <p class=\"body-text\">AI\uc5d0\uac8c \ub300\ub7c9\uc758 \ub85c\uadf8 \ubd84\uc11d\uc744 \ub9e1\uacbc\ub294\ub370, \uc5b4\ub290 \uc21c\uac04 AI\uac00 \uc124\uacc4 \uad6c\uc131 \uc790\uccb4\ub97c \ubb34\ub108\ub728\ub9ac\ub294 \ubc29\ud5a5\uc73c\ub85c \uc720\ub3c4\ud558\uae30 \uc2dc\uc791\ud588\uc2b5\ub2c8\ub2e4. Claude\uc640 \ud568\uaed8 \ub2e4\uc2dc \ucc98\uc74c\ubd80\ud130 \ud30c\uace0\ub4e4\uc5b4\uc11c \uac04\uc2e0\ud788 \uc2e4\uc81c \uc6d0\uc778\uc744 \ucc3e\uc544\ub0c8\uc2b5\ub2c8\ub2e4.<\/p>\n      <div class=\"highlight-box\"><p>AI\ub294 \uba87 \uc8fc\uc758 \uc0bd\uc9c8\uc744 \uba70\uce60\ub85c \uc904\uc5ec\uc92c\uc9c0\ub9cc, \uc124\uacc4\ub97c \uc9c0\ud0a4\ub294 \uac74 \uacb0\uad6d \uc0ac\ub78c\uc758 \ubaab\uc774\uc5c8\uc2b5\ub2c8\ub2e4. \ub124\ud2b8\uc6cc\ud06c \uad00\ub9ac \ud3ec\uc778\ud2b8\uac00 \ub298\uc5b4\ub0a0\uc218\ub85d, AI\ub3c4 \uae38\uc744 \uc783\uc2b5\ub2c8\ub2e4.<\/p><\/div>\n    <\/div>\n\n    <hr class=\"divider\">\n\n    <div class=\"section\">\n      <div class=\"section-header\"><span class=\"section-icon\">\ud83d\udcdd<\/span><span class=\"section-title\">\uc774 \uacbd\ud5d8\uc5d0\uc11c \uc5bb\uc740 \uad50\ud6c8<\/span><\/div>\n      <div class=\"lessons\">\n        <div class=\"lesson-item\">\n          <span class=\"lesson-num\">1\ufe0f\u20e3<\/span>\n          <div class=\"lesson-content\">\n            <p class=\"lesson-title\">\uc751\ub2f5 \ud06c\uae30\ub97c \ud655\uc778\ud558\ub77c<\/p>\n            <p class=\"lesson-desc\">22\ubc14\uc774\ud2b8 vs 64\ubc14\uc774\ud2b8. \uac19\uc740 404\ub3c4 \ub0b4\uc6a9\uc774 \ub2e4\ub985\ub2c8\ub2e4. \uc751\ub2f5 \ud06c\uae30 \ud558\ub098\uac00 \uacb0\uc815\uc801 \ub2e8\uc11c\uac00 \ub429\ub2c8\ub2e4.<\/p>\n          <\/div>\n        <\/div>\n        <div class=\"lesson-item\">\n          <span class=\"lesson-num\">2\ufe0f\u20e3<\/span>\n          <div class=\"lesson-content\">\n            <p class=\"lesson-title\">404\uac00 \ud56d\uc0c1 \ub77c\uc6b0\ud305 \ubb38\uc81c\ub294 \uc544\ub2c8\ub2e4<\/p>\n            <p class=\"lesson-desc\">Upstream \uc5d0\ub7ec\ub294 \uc870\uc6a9\ud788 \uc804\ud30c\ub429\ub2c8\ub2e4. \uba85\uc2dc\uc801\uc73c\ub85c \ucc98\ub9ac\ud558\uc9c0 \uc54a\uc73c\uba74 \ucd94\uc801\uc774 \ub9e4\uc6b0 \uc5b4\ub835\uc2b5\ub2c8\ub2e4.<\/p>\n          <\/div>\n        <\/div>\n        <div class=\"lesson-item\">\n          <span class=\"lesson-num\">3\ufe0f\u20e3<\/span>\n          <div class=\"lesson-content\">\n            <p class=\"lesson-title\">Upstream \uc5d0\ub7ec\ub294 502\/503\uc73c\ub85c \ubcc0\ud658\ud558\ub77c<\/p>\n            <p class=\"lesson-desc\">\ub0b4\ubd80 \uc624\ub958\uc640 \ub77c\uc6b0\ud305 \uc624\ub958\ub97c \uac19\uc740 \ucf54\ub4dc\ub85c \ubc18\ud658\ud558\uba74 \ub514\ubc84\uae45 \uc2dc\uac04\uc774 \uba87 \ubc30\ub85c \ub298\uc5b4\ub0a9\ub2c8\ub2e4.<\/p>\n          <\/div>\n        <\/div>\n        <div class=\"lesson-item\">\n          <span class=\"lesson-num\">4\ufe0f\u20e3<\/span>\n          <div class=\"lesson-content\">\n            <p class=\"lesson-title\">\uc774\ud574 \uc548 \ub418\uba74 \uc77c\ub2e8 \ub044\uace0 \ucf1c\ub77c \u2014 \uc9c4\uc9c0\ud558\uac8c<\/p>\n            <p class=\"lesson-desc\">\ubb54\uac00 \uc815\ub9d0 \uc774\ud574 \uc548 \ub418\ub294 \uc0c1\ud669\uc774 \uc0dd\uae30\uba74, \uc77c\ub2e8 \uc815\ub9ac\ud558\uace0 \ucef4\ud4e8\ud130\ub97c \uaed0\ub2e4 \ud0a4\ub294 \uac8c \uc815\uc2e0\uac74\uac15\uc5d0 \uc88b\uc2b5\ub2c8\ub2e4.<\/p>\n          <\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"closing\">\n      <p>\ub85c\uadf8\uac00 \uc870\uc6a9\ud560\uc218\ub85d, <span class=\"em\">\uc2dc\uc57c\ub97c \ub113\ud600\uc57c \ud569\ub2c8\ub2e4.<\/span> \ud83d\udd2d<br>\uadf8\ub9ac\uace0 \ubb54\uac00 \uc774\uc0c1\ud558\uba74 \u2014 \uc77c\ub2e8 \uc7ac\ubd80\ud305\ubd80\ud130.<\/p>\n    <\/div>\n\n    <div class=\"tags\">\n      <span class=\"tag\">#Backend<\/span><span class=\"tag\">#FastAPI<\/span><span class=\"tag\">#Docker<\/span><span class=\"tag\">#Ollama<\/span><span class=\"tag\">#AI<\/span><span class=\"tag\">#RAG<\/span><span class=\"tag\">#Debugging<\/span><span class=\"tag\">#SoftwareEngineering<\/span>\n    <\/div>\n  <\/div>\n\n  <!-- English -->\n  <div class=\"post\" id=\"post-en\">\n    <div class=\"post-tag\">\ud83d\udee0\ufe0f Debugging Story<\/div>\n    <h2 class=\"post-title\">The Route Exists. So Why Is It Returning 404?<br>A FastAPI + Ollama Deep Dive \ud83d\udd0d<\/h2>\n    <p class=\"post-subtitle\">I finally got my mini AI-RAG and chatbot service talking to my app. Still a prototype \u2014 text only, nothing fancy to look at. But when it all clicked together, that feeling made every late night worth it.<\/p>\n    <div class=\"meta\">\n      <span>\u26a1 FastAPI<\/span><span class=\"meta-dot\"><\/span>\n      <span>\ud83d\udc33 Docker<\/span><span class=\"meta-dot\"><\/span>\n      <span>\ud83e\udd16 Ollama<\/span><span class=\"meta-dot\"><\/span>\n      <span>\u2601\ufe0f Cloud Run<\/span>\n    <\/div>\n\n    <div class=\"arch-box\">\n      <p>\ud83c\udfd7\ufe0f Service Architecture<\/p>\n      <div class=\"arch-flow\">\n        <span class=\"arch-node\">\ud83d\udcf1 App<\/span><span class=\"arch-arrow\">\u2192<\/span>\n        <span class=\"arch-node\">\u2601\ufe0f Cloud Run<\/span><span class=\"arch-arrow\">\u2192<\/span>\n        <span class=\"arch-node\">\ud83d\udd12 Cloudflare<\/span><span class=\"arch-arrow\">\u2192<\/span>\n        <span class=\"arch-node\">\ud83c\udfe0 Home Router<\/span><span class=\"arch-arrow\">\u2192<\/span>\n        <span class=\"arch-node\">\ud83d\udda5\ufe0f Prod Server<\/span><span class=\"arch-arrow\">\u2192<\/span>\n        <span class=\"arch-node\">\ud83d\udcbb Dev Server (RTX 3060)<\/span>\n      <\/div>\n    <\/div>\n\n    <div class=\"section\">\n      <div class=\"section-header\"><span class=\"section-icon\">\ud83d\ude30<\/span><span class=\"section-title\">As the layers stacked up, strange things started happening<\/span><\/div>\n      <p class=\"body-text\">iptables + NordVPN + Docker were interfering with each other \u2014 packets were vanishing, and tcpdump was the only thing giving me any direction. My MacBook&#8217;s SSH tunnel kept dropping and reconnecting, and at some point the dev server started silently swallowing requests. Nothing in the logs, server still running. Rebooted the MacBook. Fixed immediately.<\/p>\n      <p class=\"body-text\">Once I got through all that, a 404 was waiting for me.<\/p>\n    <\/div>\n\n    <div class=\"section\">\n      <div class=\"section-header\"><span class=\"section-icon\">\ud83d\udea8<\/span><span class=\"section-title\">The route exists. So why 404?<\/span><\/div>\n      <p class=\"body-text\"><code>POST \/internal\/llm\/chat<\/code> kept returning 404 inside Docker while working perfectly on the dev server. I printed the route table directly \u2014 the endpoint was clearly registered. It wasn&#8217;t a proxy issue either, since requests were showing up in the FastAPI logs.<\/p>\n      <div class=\"highlight-box\"><p>\ud83d\udca1 From inside the container (127.0.0.1): <strong>401 Unauthorized<\/strong>. From outside via nginx: <strong>404 Not Found<\/strong>. Same container, same process, different results.<\/p><\/div>\n    <\/div>\n\n    <div class=\"section\">\n      <div class=\"section-header\"><span class=\"section-icon\">\ud83d\udd0e<\/span><span class=\"section-title\">The key clue: response size<\/span><\/div>\n      <p class=\"body-text\">The response sizes in the nginx access log looked wrong.<\/p>\n      <div class=\"key-clue\"><p>\u26a0\ufe0f FastAPI default 404 <code>{\"detail\":\"Not Found\"}<\/code> = <strong>22 bytes<\/strong><br>Actual response sizes = <strong>64\u201389 bytes<\/strong><br>Something else was generating that 404.<\/p><\/div>\n      <p class=\"body-text\">Working backwards, it was an Ollama error message:<\/p>\n      <div class=\"code-block\">{&#8220;detail&#8221;:&#8221;model &#8216;llama3&#8217; not found, try pulling it first&#8221;}<\/div>\n    <\/div>\n\n    <div class=\"section\">\n      <div class=\"section-header\"><span class=\"section-icon\">\ud83d\udd75\ufe0f<\/span><span class=\"section-title\">Root cause: upstream error propagation<\/span><\/div>\n      <p class=\"body-text\">A client service was reading <code>OLLAMA_MODEL=llama3<\/code> from GCP Secret Manager and passing it in the request body. The model actually installed was <code>llama3.1:8b<\/code>. Ollama&#8217;s 404 was propagating straight through FastAPI to the client \u2014 making it look like the route didn&#8217;t exist. Not a routing problem. An upstream error propagation problem.<\/p>\n    <\/div>\n\n    <div class=\"section\">\n      <div class=\"section-header\"><span class=\"section-icon\">\ud83e\udd16<\/span><span class=\"section-title\">Debugging with AI<\/span><\/div>\n      <p class=\"body-text\">I had been relying on AI to analyze the flood of logs. At some point, it started nudging me toward tearing down the architecture itself. I stepped back, worked through it with Claude from scratch, and eventually tracked down the real cause.<\/p>\n      <div class=\"highlight-box\"><p>AI compressed what could have been weeks of debugging into days. But holding the design together was still a human job. The more network layers you add, the easier it is for AI to lose the thread too.<\/p><\/div>\n    <\/div>\n\n    <hr class=\"divider\">\n\n    <div class=\"section\">\n      <div class=\"section-header\"><span class=\"section-icon\">\ud83d\udcdd<\/span><span class=\"section-title\">What I took away<\/span><\/div>\n      <div class=\"lessons\">\n        <div class=\"lesson-item\">\n          <span class=\"lesson-num\">1\ufe0f\u20e3<\/span>\n          <div class=\"lesson-content\">\n            <p class=\"lesson-title\">Check the response size<\/p>\n            <p class=\"lesson-desc\">22 bytes vs 64 bytes. Two 404s can mean completely different things. That single number was the decisive clue.<\/p>\n          <\/div>\n        <\/div>\n        <div class=\"lesson-item\">\n          <span class=\"lesson-num\">2\ufe0f\u20e3<\/span>\n          <div class=\"lesson-content\">\n            <p class=\"lesson-title\">404 doesn&#8217;t always mean routing failure<\/p>\n            <p class=\"lesson-desc\">Upstream errors propagate silently. If you don&#8217;t handle them explicitly, they&#8217;re very hard to trace.<\/p>\n          <\/div>\n        <\/div>\n        <div class=\"lesson-item\">\n          <span class=\"lesson-num\">3\ufe0f\u20e3<\/span>\n          <div class=\"lesson-content\">\n            <p class=\"lesson-title\">Wrap upstream errors as 502\/503<\/p>\n            <p class=\"lesson-desc\">Returning internal errors and routing errors with the same status code multiplies your debugging time significantly.<\/p>\n          <\/div>\n        <\/div>\n        <div class=\"lesson-item\">\n          <span class=\"lesson-num\">4\ufe0f\u20e3<\/span>\n          <div class=\"lesson-content\">\n            <p class=\"lesson-title\">When nothing makes sense \u2014 reboot. Seriously.<\/p>\n            <p class=\"lesson-desc\">Sometimes the most powerful debugging tool is turning it off and back on. Your mental health will thank you.<\/p>\n          <\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"closing\">\n      <p>When the logs go quiet, <span class=\"em\">widen your view.<\/span> \ud83d\udd2d<br>And when nothing makes sense \u2014 reboot first.<\/p>\n    <\/div>\n\n    <div class=\"tags\">\n      <span class=\"tag\">#Backend<\/span><span class=\"tag\">#FastAPI<\/span><span class=\"tag\">#Docker<\/span><span class=\"tag\">#Ollama<\/span><span class=\"tag\">#AI<\/span><span class=\"tag\">#RAG<\/span><span class=\"tag\">#Debugging<\/span><span class=\"tag\">#SoftwareEngineering<\/span>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script>\nfunction switchLang(lang, btn) {\n  document.querySelectorAll('.post').forEach(function(p) { p.classList.remove('active'); });\n  document.querySelectorAll('.lang-btn').forEach(function(b) { b.classList.remove('active'); });\n  document.getElementById('post-' + lang).classList.add('active');\n  btn.classList.add('active');\n}\n<\/script>\n\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\ud83c\uddf0\ud83c\uddf7 \ud55c\uad6d\uc5b4 \ud83c\uddfa\ud83c\uddf8 English \ud83d\udee0\ufe0f \ub514\ubc84\uae45 \uc77c\uc9c0 \ub77c\uc6b0\ud2b8\ub294 \uc788\ub294\ub370 \uc65c 404\uac00 \ub0a0\uae4c\uc694?FastAPI + Ollama \uc0bd\uc9c8\uae30 \ud83d\udd0d \uc9d1\uc5d0\uc11c mini AI-RAG\uc640 \ucc57\ubd07 \uc11c\ube44\uc2a4\ub97c \uc571\uc5d0 \uac04\uc2e0\ud788 \uc5f0\uacb0\ud588\uc2b5\ub2c8\ub2e4. \uc544\uc9c1 \ud504\ub85c\ud1a0\ud0c0\uc785\uc774\ub77c \ud14d\uc2a4\ud2b8\ubc16\uc5d0 \uc5c6\uc5b4 \ubcfc\ud488\uc5c6\uc9c0\ub9cc, \ub531 \ubd99\uc5ec\uc11c \ub418\uc5c8\uc744 \ub550 \uc815\ub9d0 \ubfcc\ub4ef\ud588\uc2b5\ub2c8\ub2e4. \uadf8 \uacfc\uc815\uc5d0\uc11c \uc0dd\uae34 \uc774\uc57c\uae30\uc785\ub2c8\ub2e4. \u26a1 FastAPI \ud83d\udc33 Docker \ud83e\udd16 Ollama \u2601\ufe0f Cloud Run \ud83c\udfd7\ufe0f \uc11c\ube44\uc2a4 \uad6c\uc131 \ud83d\udcf1 \uc571\u2192 \u2601\ufe0f Cloud [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":true,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[8,7,9,6],"class_list":["post-1","post","type-post","status-publish","format-standard","hentry","category-it","tag-architecture","tag-fastapi","tag-network","tag-ollama"],"_links":{"self":[{"href":"https:\/\/blog-api.minminworld.com\/index.php?rest_route=\/wp\/v2\/posts\/1","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog-api.minminworld.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog-api.minminworld.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog-api.minminworld.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog-api.minminworld.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1"}],"version-history":[{"count":1,"href":"https:\/\/blog-api.minminworld.com\/index.php?rest_route=\/wp\/v2\/posts\/1\/revisions"}],"predecessor-version":[{"id":13,"href":"https:\/\/blog-api.minminworld.com\/index.php?rest_route=\/wp\/v2\/posts\/1\/revisions\/13"}],"wp:attachment":[{"href":"https:\/\/blog-api.minminworld.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog-api.minminworld.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog-api.minminworld.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}