[{"data":1,"prerenderedAt":1484},["ShallowReactive",2],{"search-dialog-posts":3,"post-\u002Fposts\u002Fdocker-node-postgres":316,"post-surround-\u002Fposts\u002Fdocker-node-postgres":1477},[4,12,21,29,37,47,57,67,77,85,96,104,112,121,129,137,147,155,164,173,181,188,195,207,215,224,236,241,247,254,260,266,272,279,286,291,298,304,310],{"path":5,"title":6,"description":7,"category":8,"tags":9},"\u002Fposts\u002Fgit-commands-cheatsheet","Git 常用指令與終端機筆記","2018 年整理的 Git 學習筆記，包含基本終端機指令、Git 操作、分支、reset、SourceTree、Vim 與一些常用設定。","notes",[10,11],"Git","版本控制",{"path":13,"title":14,"description":15,"category":16,"tags":17},"\u002Fposts\u002Fjs-prototype-inheritance","JavaScript 物件導向：建構函式、prototype 與繼承三步驟","整理 ES5 時代用建構函式加 prototype 手刻物件導向的完整流程——封裝屬性與方法、共用函式為什麼要掛在 prototype 上、以及繼承的三個步驟（call 借用母類別建構子、Object.create 接上原型鏈、修正 constructor 指向），並附上物件縮寫與計算屬性這兩個順便學到的 ES6 寫法。","frontend",[18,19,20],"JavaScript","物件導向","prototype",{"path":22,"title":23,"description":24,"category":16,"tags":25},"\u002Fposts\u002Fflexbox-notes","Flexbox 對齊與彈性尺寸筆記","記錄 Flexbox 的主軸與交錯軸對齊、flex-direction、flex-wrap，以及 flex-grow、flex-shrink、flex-basis 等常用屬性。",[26,27,28],"CSS","Flexbox","版面",{"path":30,"title":31,"description":32,"category":16,"tags":33},"\u002Fposts\u002Fscreen-orientation-api","screen.orientation：現在判斷與鎖定螢幕方向的做法","整理現在 JavaScript 判斷螢幕方向的方式，包括 screen.orientation、change 事件與 lock()，並說明 RWD 使用 matchMedia 或 CSS orientation 會更適合。",[18,34,35,36],"RWD","螢幕方向","PWA",{"path":38,"title":39,"description":40,"category":16,"tags":41},"\u002Fposts\u002Fpinia-vuex-state-management","Pinia \u002F Vuex 狀態管理及差異","紀錄 Vuex、Pinia 的狀態管理概念、差異與用法。",[42,43,44,45,46],"Pinia","Vuex","Vue3","狀態管理","Nuxt",{"path":48,"title":49,"description":50,"category":8,"tags":51},"\u002Fposts\u002Fpyenv-python-version-management","Python 多版本管理 pyenv","紀錄 macOS 與 Windows 使用 pyenv 管理 Python 版本的方式。",[52,53,54,55,56],"Python","pyenv","macOS","Windows","版本管理",{"path":58,"title":59,"description":60,"category":8,"tags":61},"\u002Fposts\u002Fnpm-publish-vue2-component","npm 發布流程","紀錄 Vue 2 元件的 npm 發布、dist-tag 與打包流程。",[62,63,64,65,66],"npm","Vue2","Vue CLI","套件發布","MIT",{"path":68,"title":69,"description":70,"category":8,"tags":71},"\u002Fposts\u002Fmobile-debugging-today","現在怎麼 debug 手機網頁：從裝一堆工具到幾乎不用裝","對照 2022 年那套 scoop、iTunes、remotedebug-ios-webkit-adapter 的繁瑣設定，整理現在偵錯手機網頁的做法：Android 無線偵錯、iOS 的實際選項、on-device console、Playwright 裝置模擬，以及本機 HTTPS 的處理。",[72,73,74,75,76],"debug","iOS","Android","Chrome DevTools","Playwright",{"path":78,"title":79,"description":80,"category":16,"tags":81},"\u002Fposts\u002Fplaywright-vs-cypress","E2E 測試現在選什麼：Playwright 與 Cypress 的實際差異","對照幾年前用 Cypress 的做法，說明 Playwright 為什麼變成多數新專案的預設選擇：真正的多瀏覽器支援、免費的平行執行、trace viewer 除錯，以及兩者在架構上的根本差異與各自仍然適合的場景。",[76,82,83,84],"Cypress","E2E","測試",{"path":86,"title":87,"description":88,"category":89,"tags":90},"\u002Fposts\u002Fjs-encoding-today","現在處理編碼：TextEncoder、Uint8Array 的 Base64 方法與 URLSearchParams","整理 JavaScript 現代編碼方式，使用 TextEncoder、TextDecoder 與 Uint8Array 處理 UTF-8 與 Base64，並說明 Base64URL、URLSearchParams 與 encodeURIComponent 的差異。","research",[91,92,93,94,95,18],"編碼","TextEncoder","Base64","URLSearchParams","Uint8Array",{"path":97,"title":98,"description":99,"category":16,"tags":100},"\u002Fposts\u002Fdart-sass-migration","Dart Sass 遷移筆記：node-sass 之後怎麼裝 Sass","node-sass 與 LibSass 已停止維護，現在改用 Dart Sass。整理 webpack 與 Vite 的安裝方式，以及為什麼不再像 node-sass 一樣容易遇到 Node.js 版本與原生編譯問題。",[101,102,103],"Sass","Webpack","Vite",{"path":105,"title":106,"description":107,"category":16,"tags":108},"\u002Fposts\u002Fdayjs-vue3-computed","day.js 搭配 Vue 3 的日期格式化","整理 Vue 3 日期格式化的常見做法，使用 day.js 取代舊專案常見的 moment.js，並以 method 或 computed 取代 Vue 2 filter，也補充原生 Intl.DateTimeFormat 的使用方式。",[44,109,110,111],"day.js","日期","Intl",{"path":113,"title":114,"description":115,"category":16,"tags":116},"\u002Fposts\u002Fes-modules-today","ES Module 現在的樣子：Node ESM、exports 與 import maps","整理現代 JavaScript ES Module 的常見用法，包括 Node.js 原生 ESM、package.json exports、瀏覽器 import maps、JSON import attributes，以及 Vite 動態載入的注意事項。",[117,118,119,120,103,18],"ES Module","Node.js","import maps","exports",{"path":122,"title":123,"description":124,"category":89,"tags":125},"\u002Fposts\u002Fstructured-clone","structuredClone()：終於有內建的深層拷貝","整理 JavaScript structuredClone() 的深層拷貝用法，說明 Date、Map、Set、循環參考的處理方式，以及 Function、class 與 Vue reactive Proxy 的注意事項。",[18,126,127,128],"structuredClone","深拷貝","Vue",{"path":130,"title":131,"description":132,"category":16,"tags":133},"\u002Fposts\u002Fdynamic-import","JavaScript 動態載入：import() 與 Vue 非同步元件","整理靜態 import、動態 import() 與 Vue defineAsyncComponent 的使用方式，並簡單補充 Vite import.meta.glob 的用途。",[18,134,135,103,136],"ES Modules","Vue 3","Code Splitting",{"path":138,"title":139,"description":140,"category":16,"tags":141},"\u002Fposts\u002Fscreen-wake-lock-api","Screen Wake Lock API：不用套件也能讓頁面保持亮著","整理 Screen Wake Lock API 的基本用法，說明 HTTPS、頁面可見性、切換分頁後重新取得 wake lock，以及 Vue／Nuxt 計時器使用時的注意事項。",[142,128,143,144,145,146],"Wake Lock","Nuxt4","行動裝置","計時器","Web API",{"path":148,"title":149,"description":150,"category":16,"tags":151},"\u002Fposts\u002Fhtml-meta-boilerplate-today","現在的 HTML meta 標籤怎麼寫","整理現在一個新頁面該有的 meta 標籤骨架——viewport 補上 viewport-fit=cover 因應瀏海與圓角螢幕、color-scheme 讓表單控制項跟著深色模式換色、theme-color 決定行動瀏覽器工具列顏色，以及基本的 SEO 與 Open Graph 標籤。",[152,153,154],"HTML","meta","SEO",{"path":156,"title":157,"description":158,"category":8,"tags":159},"\u002Fposts\u002Fdocker-node-postgres","把 Node + PostgreSQL 的 backend 裝進 Docker：Dockerfile、.dockerignore 與 compose","從「本機裝了就跑」到打包成容器的通用做法——Dockerfile 每一行的理由與 layer cache 為什麼決定了指令順序、.dockerignore 漏掉 node_modules 對原生模組是災難級的問題、容器之間為什麼不能用 localhost 互相找到對方，以及怎麼驗證資料真的寫進了 volume。",[160,161,118,162,163],"Docker","Docker Compose","PostgreSQL","Express",{"path":165,"title":166,"description":167,"category":16,"tags":168},"\u002Fposts\u002Fnuxt-server-proxy","Nuxt 3 \u002F 4 用 Server API 做代理與轉發","整理 Nuxt 3 與 Nuxt 4 使用 server\u002Fapi 轉發第三方 API 的方式，可以避免瀏覽器 CORS 問題、隱藏 API Key，也能在伺服器端加入快取。",[169,143,170,171,172],"Nuxt3","CORS","Nitro","API",{"path":174,"title":175,"description":176,"category":16,"tags":177},"\u002Fposts\u002Fnuxt4-project-setup","Nuxt 4 建立專案：和 Nuxt 3 有哪些不同","整理 Nuxt 4 建立專案的基本流程與目錄結構，並對照 Nuxt 3 的差異，包括 app\u002F 目錄、路徑別名、TypeScript，以及目前常用的 ESLint 與 Tailwind CSS 4 設定。",[143,44,178,179,180],"TypeScript","ESLint","Tailwind CSS 4",{"path":182,"title":183,"description":184,"category":16,"tags":185},"\u002Fposts\u002Fnuxt3-route-middleware","Nuxt 3 \u002F 4 的 Route Middleware：進頁面前先做判斷","整理 Nuxt 3 與 Nuxt 4 的 Route Middleware，用 defineNuxtRouteMiddleware 在進入頁面前檢查登入狀態，搭配 navigateTo 重新導向，並說明一般 middleware、global middleware 與 definePageMeta 的用法。",[169,143,186,187],"Middleware","驗證",{"path":189,"title":190,"description":191,"category":16,"tags":192},"\u002Fposts\u002Fnuxt-route-rules-redirect","Nuxt 3 \u002F 4 的 routeRules：重新導向與頁面設定","整理 Nuxt 3 與 Nuxt 4 的 routeRules 用法，可以依照不同網址設定重新導向、預渲染、SSR 與快取，並簡單對照 Nuxt 2 的舊做法。",[169,143,193,194],"routeRules","重新導向",{"path":196,"title":197,"description":198,"category":16,"tags":199},"\u002Fposts\u002Fios-iframe-google-login-blank-page","iOS iframe 串接 Google 登入出現空白頁：Popup 與頁面回收問題排查紀錄","記錄 iframe 內串接 Google 第三方登入時，部分 iOS 裝置因背景頁面遭到回收並重新載入，導致 Popup 登入流程中斷，以及最後透過 Redirect 分流與登入狀態接回機制處理的過程。",[73,200,201,202,203,204,205,206],"Safari","iframe","Google Login","GIS","OAuth","Popup","Redirect",{"path":208,"title":209,"description":210,"category":16,"tags":211},"\u002Fposts\u002Fios-login-tab-return-behavior","iOS 第三方登入後回不到原頁？分頁開啟關係與回頁行為實測","記錄 iOS Safari 與 Chrome 在多分頁第三方登入流程中，登入頁關閉後不一定回到原頁的實測，以及分頁開啟關係、window.close 與 window.focus 的限制。",[73,200,212,202,204,201,213,214],"Chrome","window.close","window.focus",{"path":216,"title":217,"description":218,"category":16,"tags":219},"\u002Fposts\u002Fgoogle-app-about-blank-placeholder","Google App 內建瀏覽器：about:blank 佔位頁被關閉的排查紀錄","記錄 iOS Google App 內建瀏覽器在非同步開啟外部頁面時，about:blank 佔位分頁被自動關閉，導致第三方登入或外部導轉失敗的排查與解法。",[73,220,221,222,204,223],"Google App","window.open","about:blank","WebView",{"path":225,"title":226,"description":227,"category":228,"tags":229},"\u002Fposts\u002Fclaude-code-codex-cli-init-basics","AI 相關紀錄 v1｜Claude Code \u002F Codex CLI 初始化與基本概念","從 Claude Code 與 Codex CLI 開始，整理 CLI、Coding Agent、Context、\u002Finit 與 CLAUDE.md／AGENTS.md 這幾個一開始最容易搞混的概念，以及在既有專案中的安裝與啟動方式。","ai",[230,231,232,233,234,235],"AI","Claude Code","Codex CLI","Coding Agent","CLAUDE.md","AGENTS.md",{"path":237,"title":238,"description":239,"category":228,"tags":240},"\u002Fposts\u002Fclaude-agents-md-project-guidelines","AI 相關紀錄 v2｜CLAUDE.md \u002F AGENTS.md 怎麼寫？專案規範與實際範例","整理 CLAUDE.md 與 AGENTS.md 的用途、適合記錄的專案規範，以及可直接參考的設定範例。",[230,231,232,234,235],{"path":242,"title":243,"description":244,"category":228,"tags":245},"\u002Fposts\u002Fclaude-codex-config-locations","AI 相關紀錄 v3｜Claude Code `.claude\u002F` \u002F Codex CLI 設定位置：全域與專案設定基本概要介紹","整理 Claude Code 與 Codex CLI 的使用者層級、專案層級設定位置，以及 rules、skills、agents 等目錄的用途。",[230,231,232,246],"設定檔",{"path":248,"title":249,"description":250,"category":228,"tags":251},"\u002Fposts\u002Fclaude-settings-codex-config","AI 相關紀錄 v4｜Claude Code settings \u002F Codex config 基本設定","整理 Claude Code settings.json 與 Codex config.toml 的設定層級、權限控制和常見基本設定。",[230,231,232,252,253],"settings.json","config.toml",{"path":255,"title":256,"description":257,"category":228,"tags":258},"\u002Fposts\u002Fclaude-codex-rules-structure","AI 相關紀錄 v5｜Rules：專案規則多了之後怎麼拆？","整理 Claude Code Rules 與 Codex AGENTS.md 的拆分方式，說明何時該拆、怎麼依目錄管理專案規則。",[230,231,232,259,235],"Rules",{"path":261,"title":262,"description":263,"category":228,"tags":264},"\u002Fposts\u002Fclaude-codex-slash-commands","AI 相關紀錄 v6｜Commands \u002F Slash Commands：CLI 裡的 `\u002F` 指令是什麼？","介紹 Claude Code 與 Codex CLI 的 Slash Commands，整理常用內建指令、自訂指令與 Skills 的差異。",[230,231,232,265],"Slash Commands",{"path":267,"title":268,"description":269,"category":228,"tags":270},"\u002Fposts\u002Fclaude-codex-skills","AI 相關紀錄 v7｜Skills：把重複工作流程整理起來","介紹 Claude Code 與 Codex Skills，整理適合保存成 Skill 的重複流程、基本結構與使用方式。",[230,231,232,271],"Skills",{"path":273,"title":274,"description":275,"category":228,"tags":276},"\u002Fposts\u002Fclaude-codex-agents-subagents","AI 相關紀錄 v8｜Agent \u002F Subagent：什麼時候真的需要分工？","整理 Agent、Subagent 與 Custom Agent 的用途，說明何時適合分工，以及它們和 Rules、Skills 的差異。",[230,231,232,277,278],"Agent","Subagent",{"path":280,"title":281,"description":282,"category":228,"tags":283},"\u002Fposts\u002Fmcp-basics","AI 相關紀錄 v9｜MCP：讓 AI 使用其他工具與資料","介紹 MCP 如何讓 AI 使用外部工具與資料，並以 Pencil MCP 說明實際用途、權限風險及與 Skill 的差異。",[230,284,231,232,285],"MCP","Pencil",{"path":287,"title":288,"description":289,"category":228,"tags":290},"\u002Fposts\u002Fai-coding-agent-concepts-summary","AI 相關紀錄 v10｜基本概念小結論","用一篇快速整理 CLAUDE.md、AGENTS.md、設定檔、Rules、Commands、Skills、Agent 與 MCP 的角色和差異。",[230,231,232,284,271,277],{"path":292,"title":293,"description":294,"category":228,"tags":295},"\u002Fposts\u002Fclaude-code-codex-model-selection","Claude Code 與 Codex：不同模型怎麼選？","比較 Claude Code 與 Codex 不同模型的速度、能力與成本取捨，依工作難度整理實用的選擇方向。",[230,231,296,297],"Codex","模型選擇",{"path":299,"title":300,"description":301,"category":228,"tags":302},"\u002Fposts\u002Fclaude-model-selection","Claude 模型簡單怎麼選？","整理 Claude 各模型在速度、能力與使用情境上的差異，提供從輕量工作到複雜任務的選擇方向。",[230,231,303,297],"Claude",{"path":305,"title":306,"description":307,"category":228,"tags":308},"\u002Fposts\u002Fcodex-model-selection","Codex 模型簡單怎麼選？","整理 Codex 各模型在速度、能力與使用情境上的差異，提供從簡單修改到大型端到端任務的選擇方向。",[230,296,309,297],"OpenAI",{"path":311,"title":312,"description":313,"category":228,"tags":314},"\u002Fposts\u002Fclaude-code-codex-cli-command-cheatsheet","Claude Code \u002F Codex CLI 常用指令速查表","整理 Claude Code 與 Codex CLI 的常用斜線指令、快捷操作與進階功能，方便需要時快速查找。",[230,231,232,265,315],"速查表",{"id":317,"title":157,"body":318,"category":8,"description":158,"draft":1467,"extension":1468,"image":1469,"meta":1470,"navigation":1245,"path":156,"publishedAt":1471,"relatedExperiments":1472,"seo":1473,"stem":1474,"tags":1475,"updatedAt":1471,"__hash__":1476},"posts\u002Fposts\u002F20260613.docker-node-postgres.md",{"type":319,"value":320,"toc":1440},"minimark",[321,339,344,394,398,401,406,428,431,434,454,458,471,482,486,508,534,545,560,564,576,590,594,607,617,621,634,641,653,657,682,706,715,719,727,738,780,784,795,799,837,858,862,881,892,896,962,974,978,1091,1104,1122,1126,1132,1144,1161,1164,1168,1186,1197,1201,1216,1229,1232,1277,1279,1401,1404,1436],[322,323,324,325,338],"p",{},"從「本機裝了就跑」到「打包成一個貨櫃、到哪都能開」——這份筆記整理把一個 ",[326,327,328,329,333,334,337],"strong",{},"Node.js（Express + ",[330,331,332],"code",{},"pg","，含 ",[330,335,336],{},"bcrypt"," 原生模組）+ PostgreSQL"," 的 backend 裝進 Docker 的通用做法：三個檔案分別在做什麼、每一行的理由、常見的兩個雷，以及最後怎麼驗證資料沒有因為重啟而憑空消失。同樣的邏輯套用到其他 Express／Node backend 專案也通用。",[340,341,343],"h2",{"id":342},"三個檔案各管一段","三個檔案，各管一段",[345,346,347,360],"table",{},[348,349,350],"thead",{},[351,352,353,357],"tr",{},[354,355,356],"th",{},"檔案",[354,358,359],{},"角色",[361,362,363,374,384],"tbody",{},[351,364,365,371],{},[366,367,368],"td",{},[330,369,370],{},"Dockerfile",[366,372,373],{},"怎麼把程式碼蓋成一個能跑的環境（image 的食譜）",[351,375,376,381],{},[366,377,378],{},[330,379,380],{},".dockerignore",[366,382,383],{},"蓋的時候，哪些東西不該被抄進去",[351,385,386,391],{},[366,387,388],{},[330,389,390],{},"docker-compose.yml",[366,392,393],{},"image 蓋好之後，怎麼跟資料庫一起啟動、怎麼連線",[340,395,397],{"id":396},"step-1dockerfile","Step 1：Dockerfile",[322,399,400],{},"Dockerfile 是「食譜」，Docker 照順序一行行執行，每一行都會蓋成一層。層與層之間可以被快取——這也是為什麼順序不是隨便排的。",[402,403,405],"h3",{"id":404},"_01-選底圖","01 選底圖",[407,408,413],"pre",{"className":409,"code":410,"language":411,"meta":412,"style":412},"language-dockerfile shiki shiki-themes github-light github-dark-default","FROM node:20-slim\n","dockerfile","",[330,414,415],{"__ignoreMap":412},[416,417,420,424],"span",{"class":418,"line":419},"line",1,[416,421,423],{"class":422},"sDgYj","FROM",[416,425,427],{"class":426},"szmH1"," node:20-slim\n",[322,429,430],{},"不從零開始，直接用官方裝好 Node.js 的環境當地基。",[322,432,433],{},"這裡用 20 是照專案既有的 Node 版本；新專案直接挑當時的現行 LTS 就好，沒有理由跟著別人的筆記走。",[435,436,437],"blockquote",{},[322,438,439,442,443,445,446,449,450,453],{},[326,440,441],{},"為什麼不用 alpine","\n如果專案用到 ",[330,444,336],{},"（原生模組，C++ 編譯，不是純 JS 的 ",[330,447,448],{},"bcryptjs","），alpine 用 musl libc，常抓不到 prebuilt binary、容易編譯失敗。",[330,451,452],{},"slim","（Debian glibc）相容性好很多，是最少踩雷的選擇。沒有原生模組依賴的話，alpine 體積更小也是合理選項。",[402,455,457],{"id":456},"_02-設定工作目錄","02 設定工作目錄",[407,459,461],{"className":409,"code":460,"language":411,"meta":412,"style":412},"WORKDIR \u002Fapp\n",[330,462,463],{"__ignoreMap":412},[416,464,465,468],{"class":418,"line":419},[416,466,467],{"class":422},"WORKDIR",[416,469,470],{"class":426}," \u002Fapp\n",[322,472,473,474,477,478,481],{},"之後所有指令都在容器裡的 ",[330,475,476],{},"\u002Fapp"," 資料夾底下做，不存在會自動建立——相當於容器版的 ",[330,479,480],{},"cd \u002Fapp","，但對後面所有指令持續生效。",[402,483,485],{"id":484},"_03-先裝套件","03 先裝套件",[407,487,489],{"className":409,"code":488,"language":411,"meta":412,"style":412},"COPY package*.json .\u002F\nRUN npm ci --omit=dev\n",[330,490,491,499],{"__ignoreMap":412},[416,492,493,496],{"class":418,"line":419},[416,494,495],{"class":422},"COPY",[416,497,498],{"class":426}," package*.json .\u002F\n",[416,500,502,505],{"class":418,"line":501},2,[416,503,504],{"class":422},"RUN",[416,506,507],{"class":426}," npm ci --omit=dev\n",[322,509,510,513,514,517,518,521,522,525,526,529,530,533],{},[326,511,512],{},"先只複製"," ",[330,515,516],{},"package.json"," 與 ",[330,519,520],{},"package-lock.json","，還沒動到程式碼，就先把套件裝好。",[330,523,524],{},"--omit=dev"," 跳過 ",[330,527,528],{},"devDependencies","（像 ",[330,531,532],{},"nodemon","），正式容器用不到。",[322,535,536,537,540,541,544],{},"用 ",[330,538,539],{},"npm ci"," 而不是 ",[330,542,543],{},"npm install","：它嚴格照著 lock file 裝，版本完全可重現，而且不會回頭去改 lock file。沒有 lock file 時它會直接失敗——這在 CI 與容器裡是我們要的行為，因為「lock file 忘了 commit」應該當場被抓到，而不是安靜地裝到別的版本。",[435,546,547],{},[322,548,549,552,553,556,557,559],{},[326,550,551],{},"為什麼要拆開寫","\nDocker 每行都是一層快取。如果先複製全部程式碼再裝套件，之後只改一行程式碼，重新 build 時「複製程式碼」那層變了，",[326,554,555],{},"後面所有層（含安裝套件）都要重跑","——套件明明沒變還是要重新下載。拆開寫、先裝套件，只要 ",[330,558,516],{}," 沒變，這層就能直接吃快取。",[402,561,563],{"id":562},"_04-複製其餘程式碼","04 複製其餘程式碼",[407,565,567],{"className":409,"code":566,"language":411,"meta":412,"style":412},"COPY . .\n",[330,568,569],{"__ignoreMap":412},[416,570,571,573],{"class":418,"line":419},[416,572,495],{"class":422},[416,574,575],{"class":426}," . .\n",[322,577,578,579,581,582,585,586,589],{},"這時候才把剩下的所有原始碼複製進容器——搭配 ",[330,580,380],{},"（Step 2）跳過 ",[330,583,584],{},"node_modules"," 跟 ",[330,587,588],{},".env","，不然會把剛裝好的東西蓋掉、也會把密鑰打包進 image。",[402,591,593],{"id":592},"_05-宣告對外-port","05 宣告對外 port",[407,595,597],{"className":409,"code":596,"language":411,"meta":412,"style":412},"EXPOSE 8080\n",[330,598,599],{"__ignoreMap":412},[416,600,601,604],{"class":418,"line":419},[416,602,603],{"class":422},"EXPOSE",[416,605,606],{"class":426}," 8080\n",[322,608,609,610,612,613,616],{},"純文件性質的宣告：「這個容器會在這個 port 服務」。實際要不要對外開放，是 ",[330,611,390],{}," 的 ",[330,614,615],{},"ports"," 才真正生效。",[402,618,620],{"id":619},"_06-不要用-root-跑","06 不要用 root 跑",[407,622,624],{"className":409,"code":623,"language":411,"meta":412,"style":412},"USER node\n",[330,625,626],{"__ignoreMap":412},[416,627,628,631],{"class":418,"line":419},[416,629,630],{"class":422},"USER",[416,632,633],{"class":426}," node\n",[322,635,636,637,640],{},"官方的 node image 裡已經內建一個叫 ",[330,638,639],{},"node"," 的非特權使用者。不加這行的話容器裡的程式是以 root 身分執行，一旦被打進來，攻擊者在容器裡就有完整權限。",[322,642,643,644,647,648,517,650,652],{},"放在 ",[330,645,646],{},"CMD"," 之前、所有 ",[330,649,495],{},[330,651,504],{}," 之後——前面那些步驟需要寫入權限。",[402,654,656],{"id":655},"_07-啟動指令","07 啟動指令",[407,658,660],{"className":409,"code":659,"language":411,"meta":412,"style":412},"CMD [\"npm\", \"start\"]\n",[330,661,662],{"__ignoreMap":412},[416,663,664,666,669,673,676,679],{"class":418,"line":419},[416,665,646],{"class":422},[416,667,668],{"class":426}," [",[416,670,672],{"class":671},"sNbzA","\"npm\"",[416,674,675],{"class":426},", ",[416,677,678],{"class":671},"\"start\"",[416,680,681],{"class":426},"]\n",[322,683,684,685,688,689,692,693,513,696,699,700,702,703,705],{},"容器啟動時真正執行的指令，等同 ",[330,686,687],{},"npm start","（通常對應 ",[330,690,691],{},"node .\u002Fserver.js"," 之類的正式啟動腳本）。",[326,694,695],{},"不是",[330,697,698],{},"npm run dev","——",[330,701,532],{}," 是本機開發用的自動重啟，正式容器不需要，也沒被裝進來（Step 3 的 ",[330,704,524],{},"）。",[435,707,708],{},[322,709,710,711,714],{},"需要編譯步驟（TypeScript 轉譯、前端打包）的專案值得再進一階，改成",[326,712,713],{},"多階段建置","：第一階段裝完整依賴並編譯，第二階段只把產出物複製過來。最終 image 裡不會有原始碼與建置工具，體積小很多。單純的 JavaScript 專案不必為此增加複雜度。",[340,716,718],{"id":717},"step-2dockerignore","Step 2：.dockerignore",[407,720,725],{"className":721,"code":723,"language":724,"meta":412},[722],"language-text","node_modules\n.env\n","text",[330,726,723],{"__ignoreMap":412},[322,728,729,730,733,734,737],{},"跟 ",[330,731,732],{},".gitignore"," 的邏輯很像，但管的是「",[330,735,736],{},"COPY . ."," 複製時要跳過誰」：",[345,739,740,750],{},[348,741,742],{},[351,743,744,747],{},[354,745,746],{},"項目",[354,748,749],{},"為什麼要排除",[361,751,752,764],{},[351,753,754,758],{},[366,755,756],{},[330,757,584],{},[366,759,760,761,763],{},"本機（你的作業系統）裝的版本會蓋掉容器裡剛裝好、相容容器系統的版本——對 ",[330,762,336],{}," 這種原生模組是直接爆炸等級的問題",[351,765,766,770],{},[366,767,768],{},[330,769,588],{},[366,771,772,773,776,777,779],{},"裡面有真正的密鑰（如 ",[330,774,775],{},"JWT_SECRET","、資料庫密碼），不該打包進 image（image 之後可能被分享、上傳，密鑰就外洩）。容器的環境變數改由 ",[330,778,390],{}," 注入",[340,781,783],{"id":782},"step-3docker-composeyml-的-backend-服務","Step 3：docker-compose.yml 的 backend 服務",[322,785,786,787,790,791,794],{},"Image 蓋好只是半成品，這段負責「怎麼跟資料庫一起啟動、互相找到對方」。以服務名稱 ",[330,788,789],{},"backend"," 連 ",[330,792,793],{},"postgres"," 為例。",[402,796,798],{"id":797},"a-build","A build",[407,800,804],{"className":801,"code":802,"language":803,"meta":412,"style":412},"language-yaml shiki shiki-themes github-light github-dark-default","build:\n  context: .\u002Fbackend\n  dockerfile: Dockerfile\n","yaml",[330,805,806,815,826],{"__ignoreMap":412},[416,807,808,812],{"class":418,"line":419},[416,809,811],{"class":810},"sXQ1o","build",[416,813,814],{"class":426},":\n",[416,816,817,820,823],{"class":418,"line":501},[416,818,819],{"class":810},"  context",[416,821,822],{"class":426},": ",[416,824,825],{"class":671},".\u002Fbackend\n",[416,827,829,832,834],{"class":418,"line":828},3,[416,830,831],{"class":810},"  dockerfile",[416,833,822],{"class":426},[416,835,836],{"class":671},"Dockerfile\n",[322,838,839,842,843,846,847,850,851,853,854,857],{},[330,840,841],{},"context"," 是 build 時的根目錄——這樣 Dockerfile 裡的 ",[330,844,845],{},"COPY package*.json .\u002F"," 才找得到 ",[330,848,849],{},"backend\u002Fpackage.json","，不是專案根目錄那個。如果你的 Dockerfile 就放在專案根目錄，",[330,852,841],{}," 也要跟著改成 ",[330,855,856],{},".","。",[402,859,861],{"id":860},"b-ports","B ports",[407,863,865],{"className":801,"code":864,"language":803,"meta":412,"style":412},"ports:\n  - \"8080:8080\"\n",[330,866,867,873],{"__ignoreMap":412},[416,868,869,871],{"class":418,"line":419},[416,870,615],{"class":810},[416,872,814],{"class":426},[416,874,875,878],{"class":418,"line":501},[416,876,877],{"class":426},"  - ",[416,879,880],{"class":671},"\"8080:8080\"\n",[322,882,883,884,887,888,891],{},"格式是 ",[330,885,886],{},"\"外部:內部\"","。左邊（外部）是宿主機打進來的 port，通常照你的專案規格固定；右邊（內部）是容器裡 server 實際監聽的 port，來自下面 ",[330,889,890],{},"PORT"," 這個環境變數。兩邊數字不一定要一樣，只是設一樣比較不容易搞混。",[402,893,895],{"id":894},"c-environment","C environment",[407,897,899],{"className":801,"code":898,"language":803,"meta":412,"style":412},"environment:\n  - PORT=8080\n  - DB_HOST=postgres\n  - DB_PORT=5432\n  - DB_USERNAME=\u003C照抄 postgres 服務的 POSTGRES_USER>\n  - DB_PASSWORD=\u003C照抄 postgres 服務的 POSTGRES_PASSWORD>\n  - DB_DATABASE=\u003C照抄 postgres 服務的 POSTGRES_DB>\n  - JWT_SECRET=\u003C自己的亂數字串，不要用 .env.example 的預設值>\n",[330,900,901,908,915,922,930,938,946,954],{"__ignoreMap":412},[416,902,903,906],{"class":418,"line":419},[416,904,905],{"class":810},"environment",[416,907,814],{"class":426},[416,909,910,912],{"class":418,"line":501},[416,911,877],{"class":426},[416,913,914],{"class":671},"PORT=8080\n",[416,916,917,919],{"class":418,"line":828},[416,918,877],{"class":426},[416,920,921],{"class":671},"DB_HOST=postgres\n",[416,923,925,927],{"class":418,"line":924},4,[416,926,877],{"class":426},[416,928,929],{"class":671},"DB_PORT=5432\n",[416,931,933,935],{"class":418,"line":932},5,[416,934,877],{"class":426},[416,936,937],{"class":671},"DB_USERNAME=\u003C照抄 postgres 服務的 POSTGRES_USER>\n",[416,939,941,943],{"class":418,"line":940},6,[416,942,877],{"class":426},[416,944,945],{"class":671},"DB_PASSWORD=\u003C照抄 postgres 服務的 POSTGRES_PASSWORD>\n",[416,947,949,951],{"class":418,"line":948},7,[416,950,877],{"class":426},[416,952,953],{"class":671},"DB_DATABASE=\u003C照抄 postgres 服務的 POSTGRES_DB>\n",[416,955,957,959],{"class":418,"line":956},8,[416,958,877],{"class":426},[416,960,961],{"class":671},"JWT_SECRET=\u003C自己的亂數字串，不要用 .env.example 的預設值>\n",[322,963,964,965,967,968,970,971,973],{},"相當於容器裡有一份 ",[330,966,588],{},"，但值寫在這裡而不是檔案裡。資料庫相關變數要跟 ",[330,969,390],{}," 裡 ",[330,972,793],{}," 服務本身設定的值對得起來，才連得上。",[402,975,977],{"id":976},"d-depends_on-與-healthcheck","D depends_on 與 healthcheck",[407,979,981],{"className":801,"code":980,"language":803,"meta":412,"style":412},"depends_on:\n  postgres:\n    condition: service_healthy\nhealthcheck:\n  test: [\"CMD-SHELL\",\n    \"node -e \\\"fetch('http:\u002F\u002Flocalhost:8080\u002Fhealthcheck')\n      .then(r => process.exit(r.ok ? 0 : 1))\n      .catch(() => process.exit(1))\\\"\"]\n  interval: 10s\n  timeout: 3s\n  retries: 5\n",[330,982,983,990,997,1007,1014,1028,1040,1045,1057,1068,1079],{"__ignoreMap":412},[416,984,985,988],{"class":418,"line":419},[416,986,987],{"class":810},"depends_on",[416,989,814],{"class":426},[416,991,992,995],{"class":418,"line":501},[416,993,994],{"class":810},"  postgres",[416,996,814],{"class":426},[416,998,999,1002,1004],{"class":418,"line":828},[416,1000,1001],{"class":810},"    condition",[416,1003,822],{"class":426},[416,1005,1006],{"class":671},"service_healthy\n",[416,1008,1009,1012],{"class":418,"line":924},[416,1010,1011],{"class":810},"healthcheck",[416,1013,814],{"class":426},[416,1015,1016,1019,1022,1025],{"class":418,"line":932},[416,1017,1018],{"class":810},"  test",[416,1020,1021],{"class":426},": [",[416,1023,1024],{"class":671},"\"CMD-SHELL\"",[416,1026,1027],{"class":426},",\n",[416,1029,1030,1033,1037],{"class":418,"line":940},[416,1031,1032],{"class":671},"    \"node -e ",[416,1034,1036],{"class":1035},"sw7DY","\\\"",[416,1038,1039],{"class":671},"fetch('http:\u002F\u002Flocalhost:8080\u002Fhealthcheck')\n",[416,1041,1042],{"class":418,"line":948},[416,1043,1044],{"class":671},"      .then(r => process.exit(r.ok ? 0 : 1))\n",[416,1046,1047,1050,1052,1055],{"class":418,"line":956},[416,1048,1049],{"class":671},"      .catch(() => process.exit(1))",[416,1051,1036],{"class":1035},[416,1053,1054],{"class":671},"\"",[416,1056,681],{"class":426},[416,1058,1060,1063,1065],{"class":418,"line":1059},9,[416,1061,1062],{"class":810},"  interval",[416,1064,822],{"class":426},[416,1066,1067],{"class":671},"10s\n",[416,1069,1071,1074,1076],{"class":418,"line":1070},10,[416,1072,1073],{"class":810},"  timeout",[416,1075,822],{"class":426},[416,1077,1078],{"class":671},"3s\n",[416,1080,1082,1085,1087],{"class":418,"line":1081},11,[416,1083,1084],{"class":810},"  retries",[416,1086,822],{"class":426},[416,1088,1090],{"class":1089},"sPgVT","5\n",[322,1092,1093,1096,1097,1100,1101,705],{},[330,1094,1095],{},"service_healthy"," 不只是等 postgres「開機」，而是等它自己的 healthcheck（例如 ",[330,1098,1099],{},"pg_isready","）真的通過，backend 才開始啟動——不然應用程式一連線就失敗、直接 crash。backend 自己的 healthcheck 打的是你專案裡真正存在的健康檢查路由（範例用 ",[330,1102,1103],{},"GET \u002Fhealthcheck",[435,1105,1106],{},[322,1107,1108,1111,1114,1115,1118,1119,1121],{},[326,1109,1110],{},"最核心的觀念",[330,1112,1113],{},"DB_HOST"," 從本機開發的 ",[330,1116,1117],{},"localhost"," 改成容器化後的服務名稱（例如 ",[330,1120,793],{},"）——這一行最容易漏改，往下看圖解釋為什麼。",[402,1123,1125],{"id":1124},"圖解localhost-與服務名稱","圖解：localhost 與服務名稱",[407,1127,1130],{"className":1128,"code":1129,"language":724,"meta":412},[722],"本機開發\n┌───────────────┐  DB_HOST=localhost:5432   ┌──────────────────┐\n│    backend    │ ───(port mapping 到宿主機)──▶ │  postgres 容器    │\n│  npm run dev  │                            │  5432 對外開放     │\n└───────────────┘                            └──────────────────┘\n\n\n容器化後\n┌───────────┐  localhost:8080   ┌ ─ ─ ─ ─ ─ docker compose 內部網路 ─ ─ ─ ─ ─ ┐\n│ 你的電腦   │ ─────────────────▶│ ┌───────────────┐  DB_HOST=postgres ┌──────────────────┐│\n└───────────┘                   │ │ backend 容器   │ ──(service 名稱解析)─▶│ postgres 容器     ││\n                                 │ │ PORT=8080     │                  │ service: postgres││\n                                 │ └───────────────┘                  └──────────────────┘│\n                                 └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘\n",[330,1131,1129],{"__ignoreMap":412},[322,1133,1134,1135,1137,1138,1140,1141,1143],{},"本機開發時 backend 跟 postgres 都在同一台電腦，透過 port mapping 到 localhost 互通；容器化後 backend 自己也變成容器，容器之間不能用 localhost 找到彼此，要改用 compose 內部網路裡的「服務名稱」——這正是 ",[330,1136,1113],{}," 要改的原因，跟 postgres 服務在 ",[330,1139,390],{}," 裡叫什麼名字，",[330,1142,1113],{}," 就填什麼。",[322,1145,1146,1147,1150,1151,1154,1155,857,1158],{},"換成別的服務也是同一套規則：MongoDB 的連線字串要從 ",[330,1148,1149],{},"mongodb:\u002F\u002Flocalhost:27017"," 改成 ",[330,1152,1153],{},"mongodb:\u002F\u002Fmongo:27017","，Redis 則是 ",[330,1156,1157],{},"redis:\u002F\u002Fredis:6379",[326,1159,1160],{},"主機名稱一律換成服務名稱。",[340,1162,1163],{"id":1163},"常見雷點",[402,1165,1167],{"id":1166},"dockerfile冒號後面多了一格","Dockerfile：冒號後面多了一格",[407,1169,1173],{"className":1170,"code":1171,"language":1172,"meta":412,"style":412},"language-diff shiki shiki-themes github-light github-dark-default","- FROM node: 20-slim\n+ FROM node:20-slim\n","diff",[330,1174,1175,1181],{"__ignoreMap":412},[416,1176,1177],{"class":418,"line":419},[416,1178,1180],{"class":1179},"s_eBU","- FROM node: 20-slim\n",[416,1182,1183],{"class":418,"line":501},[416,1184,1185],{"class":810},"+ FROM node:20-slim\n",[322,1187,1188,1189,1192,1193,1196],{},"Docker 把它解析成 image 名稱是 ",[330,1190,1191],{},"node:","，後面 ",[330,1194,1195],{},"20-slim"," 變成不合法的東西，抓不到對應的 image——編輯器會直接標紅警告。",[402,1198,1200],{"id":1199},"docker-composeyml少了一個斜線","docker-compose.yml：少了一個斜線",[407,1202,1204],{"className":1170,"code":1203,"language":1172,"meta":412,"style":412},"- context: .backend\n+ context: .\u002Fbackend\n",[330,1205,1206,1211],{"__ignoreMap":412},[416,1207,1208],{"class":418,"line":419},[416,1209,1210],{"class":1179},"- context: .backend\n",[416,1212,1213],{"class":418,"line":501},[416,1214,1215],{"class":810},"+ context: .\u002Fbackend\n",[322,1217,1218,1220,1221,1224,1225,1228],{},[330,1219,856],{}," 代表「目前檔案所在的資料夾」，",[330,1222,1223],{},".\u002Fbackend"," 才是「該資料夾底下的 backend 子資料夾」；少了斜線會被當成一個叫 ",[330,1226,1227],{},".backend"," 的資料夾去找，這個資料夾不存在，build 會直接失敗。",[340,1230,1231],{"id":1231},"驗證流程",[1233,1234,1237,1255,1261,1271],"ul",{"className":1235},[1236],"contains-task-list",[1238,1239,1242,513,1247,1250,1251,1254],"li",{"className":1240},[1241],"task-list-item",[1243,1244],"input",{"disabled":1245,"type":1246},true,"checkbox",[330,1248,1249],{},"docker compose up -d --build \u003Cbackend 服務名> \u003Cdb 服務名>","\nbackend 容器狀態變成 ",[330,1252,1253],{},"healthy","，log 裡看到「連線成功／伺服器啟動」之類的訊息。",[1238,1256,1258,1260],{"className":1257},[1241],[1243,1259],{"disabled":1245,"type":1246}," 打一次 healthcheck 或任一支簡單的 API，確認容器裡的服務真的活著。",[1238,1262,1264,1266,1267,1270],{"className":1263},[1241],[1243,1265],{"disabled":1245,"type":1246}," 先寫入一筆測試資料（例如註冊一個帳號）→ ",[330,1268,1269],{},"docker compose restart \u003Cbackend 服務名>"," → 再查一次同一筆資料。\n重啟後資料依然查得到——證明資料是真的寫進資料庫的 volume，不是存在記憶體變數裡，重啟就消失。",[1238,1272,1274,1276],{"className":1273},[1241],[1243,1275],{"disabled":1245,"type":1246}," 跑一次完整測試（合約測試或整合測試），確認容器化後行為跟本機開發時一致。",[340,1278,315],{"id":315},[345,1280,1281,1291],{},[348,1282,1283],{},[351,1284,1285,1288],{},[354,1286,1287],{},"檔案／欄位",[354,1289,1290],{},"值",[361,1292,1293,1307,1322,1333,1345,1358,1369,1381,1392],{},[351,1294,1295,1298],{},[366,1296,1297],{},"base image",[366,1299,1300,1303,1304,1306],{},[330,1301,1302],{},"node:\u003CLTS>-slim","（有原生模組如 ",[330,1305,336],{}," 時避開 alpine）",[351,1308,1309,1312],{},[366,1310,1311],{},"安裝套件",[366,1313,1314,1315,1318,1319,1321],{},"先 ",[330,1316,1317],{},"COPY package*.json"," 再 ",[330,1320,539],{},"，善用 layer cache",[351,1323,1324,1327],{},[366,1325,1326],{},"執行身分",[366,1328,1329,1332],{},[330,1330,1331],{},"USER node","，不要用 root",[351,1334,1335,1337],{},[366,1336,646],{},[366,1338,1339,1341,1342,1344],{},[330,1340,687],{},"（不是 ",[330,1343,698],{},"，容器不需要 nodemon）",[351,1346,1347,1351],{},[366,1348,1349],{},[330,1350,380],{},[366,1352,1353,1355,1356],{},[330,1354,584],{},"、",[330,1357,588],{},[351,1359,1360,1364],{},[366,1361,1362],{},[330,1363,615],{},[366,1365,1366,1368],{},[330,1367,886],{},"，外部通常固定成專案規定的 port",[351,1370,1371,1375],{},[366,1372,1373],{},[330,1374,1113],{},[366,1376,1377,1378,1380],{},"資料庫服務在 ",[330,1379,390],{}," 裡的服務名稱（容器間用服務名稱，不是 localhost）",[351,1382,1383,1387],{},[366,1384,1385],{},[330,1386,987],{},[366,1388,1389],{},[330,1390,1391],{},"\u003Cdb 服務名>: condition: service_healthy",[351,1393,1394,1398],{},[366,1395,1396],{},[330,1397,1011],{},[366,1399,1400],{},"打 backend 自己的健康檢查路由",[340,1402,1403],{"id":1403},"參考資料",[1233,1405,1406,1415,1422,1429],{},[1238,1407,1408],{},[1409,1410,1414],"a",{"href":1411,"rel":1412},"https:\u002F\u002Fdocs.docker.com\u002Freference\u002Fdockerfile\u002F",[1413],"nofollow","Docker：Dockerfile reference",[1238,1416,1417],{},[1409,1418,1421],{"href":1419,"rel":1420},"https:\u002F\u002Fdocs.docker.com\u002Freference\u002Fcompose-file\u002F",[1413],"Docker：Compose file reference",[1238,1423,1424],{},[1409,1425,1428],{"href":1426,"rel":1427},"https:\u002F\u002Fdocs.docker.com\u002Fguides\u002Fnodejs\u002F",[1413],"Docker：Node.js language-specific guide",[1238,1430,1431],{},[1409,1432,1435],{"href":1433,"rel":1434},"https:\u002F\u002Fdocs.docker.com\u002Fbuild\u002Fcache\u002F",[1413],"Docker：Build cache",[1437,1438,1439],"style",{},"html pre.shiki code .sDgYj, html code.shiki .sDgYj{--shiki-default:#D73A49;--shiki-dark:#FF7B72}html pre.shiki code .szmH1, html code.shiki .szmH1{--shiki-default:#24292E;--shiki-dark:#E6EDF3}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 .sNbzA, html code.shiki .sNbzA{--shiki-default:#032F62;--shiki-dark:#A5D6FF}html pre.shiki code .sXQ1o, html code.shiki .sXQ1o{--shiki-default:#22863A;--shiki-dark:#7EE787}html pre.shiki code .sw7DY, html code.shiki .sw7DY{--shiki-default:#005CC5;--shiki-dark:#FF7B72}html pre.shiki code .sPgVT, html code.shiki .sPgVT{--shiki-default:#005CC5;--shiki-dark:#79C0FF}html pre.shiki code .s_eBU, html code.shiki .s_eBU{--shiki-default:#B31D28;--shiki-dark:#FFA198}",{"title":412,"searchDepth":828,"depth":828,"links":1441},[1442,1443,1452,1453,1460,1464,1465,1466],{"id":342,"depth":501,"text":343},{"id":396,"depth":501,"text":397,"children":1444},[1445,1446,1447,1448,1449,1450,1451],{"id":404,"depth":828,"text":405},{"id":456,"depth":828,"text":457},{"id":484,"depth":828,"text":485},{"id":562,"depth":828,"text":563},{"id":592,"depth":828,"text":593},{"id":619,"depth":828,"text":620},{"id":655,"depth":828,"text":656},{"id":717,"depth":501,"text":718},{"id":782,"depth":501,"text":783,"children":1454},[1455,1456,1457,1458,1459],{"id":797,"depth":828,"text":798},{"id":860,"depth":828,"text":861},{"id":894,"depth":828,"text":895},{"id":976,"depth":828,"text":977},{"id":1124,"depth":828,"text":1125},{"id":1163,"depth":501,"text":1163,"children":1461},[1462,1463],{"id":1166,"depth":828,"text":1167},{"id":1199,"depth":828,"text":1200},{"id":1231,"depth":501,"text":1231},{"id":315,"depth":501,"text":315},{"id":1403,"depth":501,"text":1403},false,"md",null,{},"2026-06-13",[],{"title":157,"description":158},"posts\u002F20260613.docker-node-postgres",[160,161,118,162,163],"XgkZoDefXf1-YaZO0VnZ_IdaPsWdcQr9y7o3xH8GkcQ",[1478,1481],{"title":139,"path":138,"stem":1479,"description":140,"category":16,"publishedAt":1480,"children":-1},"posts\u002F20260516.screen-wake-lock-api","2026-06-16",{"title":149,"path":148,"stem":1482,"description":150,"category":16,"publishedAt":1483,"children":-1},"posts\u002F20260519.html-meta-boilerplate-today","2026-05-19",1791786901101]