<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ted Dam</title>
    <description>The latest articles on DEV Community by Ted Dam (@ted).</description>
    <link>https://dev.to/ted</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F79140%2F2e7b9378-1f24-4d7f-8b80-cbd73fd7558d.jpeg</url>
      <title>DEV Community: Ted Dam</title>
      <link>https://dev.to/ted</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ted"/>
    <language>en</language>
    <item>
      <title>Part 1 — Container hoá app &amp; chạy trong Kubernetes local</title>
      <dc:creator>Ted Dam</dc:creator>
      <pubDate>Wed, 08 Jul 2026 03:25:26 +0000</pubDate>
      <link>https://dev.to/ted/part-1-container-hoa-app-chay-trong-kubernetes-local-5cpb</link>
      <guid>https://dev.to/ted/part-1-container-hoa-app-chay-trong-kubernetes-local-5cpb</guid>
      <description>&lt;h2&gt;
  
  
  Bạn sẽ học gì
&lt;/h2&gt;

&lt;p&gt;Sau bài này, bạn sẽ tự tay đưa một app từ &lt;strong&gt;số 0&lt;/strong&gt; (một thư mục trống) đến &lt;strong&gt;chạy được bên trong một cluster Kubernetes chạy trên máy của bạn&lt;/strong&gt;. Cụ thể:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Viết một app Todo API nhỏ bằng Node.js + Express.&lt;/li&gt;
&lt;li&gt;Đóng gói (container hoá) nó thành một Docker image.&lt;/li&gt;
&lt;li&gt;Tạo một cluster Kubernetes local bằng &lt;code&gt;kind&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Deploy app bằng file YAML "thật" (không dùng lệnh tắt) để hiểu Kubernetes vận hành thế nào.&lt;/li&gt;
&lt;li&gt;Truy cập app đang chạy trong cluster từ máy của bạn.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Đây là &lt;strong&gt;Part 1&lt;/strong&gt; trong series &lt;em&gt;"DevOps 101 — Học K8s, Helm, ArgoCD từ số 0"&lt;/em&gt;. Cả series dùng chung một app tên &lt;code&gt;todo-ops&lt;/code&gt;, các part sau sẽ xây tiếp lên nền này (thêm database, config, ingress, Helm, GitOps với ArgoCD).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5yh7yqs0eqc1iszgb85u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5yh7yqs0eqc1iszgb85u.png" alt="Kiến trúc todo-ops end-to-end: Browser → Ingress → Service → Todo API → Postgres" width="800" height="955"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Điều kiện tiên quyết
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt; (hoặc Docker Desktop / OrbStack) — đang chạy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;kind&lt;/strong&gt; — công cụ tạo cluster Kubernetes trong Docker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;kubectl&lt;/strong&gt; — CLI để nói chuyện với Kubernetes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js 20+&lt;/strong&gt; và &lt;strong&gt;npm&lt;/strong&gt; — để chạy thử app local.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;git&lt;/strong&gt; — để quản lý mã nguồn.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cài đặt
&lt;/h3&gt;

&lt;p&gt;Chọn theo hệ điều hành của bạn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS&lt;/strong&gt; (dùng &lt;a href="https://brew.sh" rel="noopener noreferrer"&gt;Homebrew&lt;/a&gt; — nếu chưa có, cài trước):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Cài Homebrew (bỏ qua nếu đã có)&lt;/span&gt;
/bin/bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Docker Desktop (hoặc OrbStack: brew install --cask orbstack)&lt;/span&gt;
brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; docker

&lt;span class="c"&gt;# Các CLI còn lại&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;kind kubectl node git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sau khi cài xong, &lt;strong&gt;mở Docker Desktop&lt;/strong&gt; (hoặc OrbStack) và chờ nó báo &lt;em&gt;Running&lt;/em&gt; trước khi chạy tiếp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linux (Ubuntu/Debian):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Docker Engine&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.docker.com | sh
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# cho phép chạy docker không cần sudo (đăng xuất/đăng nhập lại để có hiệu lực)&lt;/span&gt;

&lt;span class="c"&gt;# kubectl&lt;/span&gt;
curl &lt;span class="nt"&gt;-LO&lt;/span&gt; &lt;span class="s2"&gt;"https://dl.k8s.io/release/&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-Ls&lt;/span&gt; https://dl.k8s.io/release/stable.txt&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/bin/linux/amd64/kubectl"&lt;/span&gt;
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 kubectl /usr/local/bin/kubectl &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm &lt;/span&gt;kubectl

&lt;span class="c"&gt;# kind&lt;/span&gt;
curl &lt;span class="nt"&gt;-Lo&lt;/span&gt; ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 kind /usr/local/bin/kind &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm &lt;/span&gt;kind

&lt;span class="c"&gt;# Node.js 20 (qua NodeSource) + git&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://deb.nodesource.com/setup_20.x | &lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; bash -
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nodejs git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 Trên &lt;strong&gt;Apple Silicon (M1/M2/M3)&lt;/strong&gt; đổi &lt;code&gt;amd64&lt;/code&gt; → &lt;code&gt;arm64&lt;/code&gt; trong hai lệnh &lt;code&gt;kubectl&lt;/code&gt; và &lt;code&gt;kind&lt;/code&gt;. Trên macOS đã dùng &lt;code&gt;brew&lt;/code&gt; nên không phải lo.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Kiểm tra nhanh (mỗi lệnh phải in ra một dòng version, không báo &lt;code&gt;command not found&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nt"&gt;--version&lt;/span&gt;
kind &lt;span class="nt"&gt;--version&lt;/span&gt;
kubectl version &lt;span class="nt"&gt;--client&lt;/span&gt;
node &lt;span class="nt"&gt;--version&lt;/span&gt;
git &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Thời gian
&lt;/h2&gt;

&lt;p&gt;Khoảng &lt;strong&gt;~40 phút&lt;/strong&gt; nếu bạn gõ theo từng bước.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vị trí trong series
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cốt lõi (làm theo thứ tự)
 [Part 1]  Container &amp;amp; Kubernetes local          ← bạn đang ở đây
  Part 2   Config &amp;amp; State (Postgres, ConfigMap, Secret)
  Part 3   Networking (Service &amp;amp; Ingress)
  Part 4   Helm (đóng gói chart)
  Part 5   ArgoCD / GitOps
  Part 6   CI với GitHub Actions
  Part 7   Observability &amp;amp; Bảo mật
  Part 8   Monitoring (Prometheus, Grafana, Alertmanager)

Track nâng cao (làm khi todo-ops còn chạy — trước khi dọn dẹp)
  Part 9   Autoscaling (HPA)
  Part 10  StatefulSet cho DB
  Part 11  Nhiều môi trường (dev/stage/prod)
  Part 12  Secrets production (Sealed / External Secrets)
  Part 13  Logging (Loki)
  Part 14  Tracing (Tempo)

Dọn dẹp (làm cuối cùng)
  Part 15  Dọn dẹp — xoá cluster + mọi tài nguyên, trả máy về sạch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Part 15 xoá cluster todo-ops.&lt;/strong&gt; Nếu định làm track nâng cao (Part 9-14),&lt;br&gt;
làm chúng &lt;strong&gt;trước&lt;/strong&gt; Part 15. Cleanup luôn để &lt;strong&gt;cuối cùng&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Hiểu thêm — Vì sao phải "container hoá"?&lt;/strong&gt;&lt;br&gt;
Cái đau kinh điển của lập trình: &lt;em&gt;"máy tôi chạy được mà!"&lt;/em&gt;. App của bạn cần Node.js phiên bản đúng, đúng thư viện, đúng biến môi trường... Khi mang sang máy khác hay server, thiếu một thứ là hỏng.&lt;br&gt;
&lt;strong&gt;Container&lt;/strong&gt; giải quyết bằng cách đóng gói &lt;strong&gt;app + toàn bộ thứ nó cần để chạy&lt;/strong&gt; vào một "hộp" chuẩn hoá. Hộp đó chạy giống hệt nhau ở mọi nơi có Docker. &lt;strong&gt;Docker image&lt;/strong&gt; là bản đóng gói tĩnh (như file cài đặt); &lt;strong&gt;container&lt;/strong&gt; là một image đang chạy (như tiến trình).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Bước 1 — Tạo project
&lt;/h2&gt;

&lt;p&gt;Tạo thư mục dự án và khởi tạo git.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;todo-ops &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;todo-ops &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mkdir todo-ops&lt;/code&gt; tạo thư mục dự án.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cd todo-ops&lt;/code&gt; bước vào trong đó — từ giờ &lt;strong&gt;mọi lệnh chạy trong thư mục này&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git init&lt;/code&gt; biến thư mục thành một kho git để theo dõi thay đổi (các part sau, đặc biệt Part 6 GitOps, sẽ cần git).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Output mong đợi:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initialized empty Git repository in /.../todo-ops/.git/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ✅ Checkpoint 1
&lt;/h3&gt;

&lt;p&gt;Chạy &lt;code&gt;pwd&lt;/code&gt; — phải thấy đường dẫn kết thúc bằng &lt;code&gt;/todo-ops&lt;/code&gt;. Chạy &lt;code&gt;ls -a&lt;/code&gt; — phải thấy thư mục &lt;code&gt;.git&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nếu sai:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lỡ &lt;code&gt;git init&lt;/code&gt; ở nhầm thư mục cha → xoá thư mục &lt;code&gt;.git&lt;/code&gt; bị tạo nhầm bằng &lt;code&gt;rm -rf .git&lt;/code&gt;, rồi &lt;code&gt;cd&lt;/code&gt; vào đúng &lt;code&gt;todo-ops&lt;/code&gt; và làm lại.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Bước 2 — Viết app Node.js
&lt;/h2&gt;

&lt;p&gt;App của chúng ta là &lt;strong&gt;Todo API&lt;/strong&gt;: một service HTTP đơn giản cho phép xem và thêm todo. Part 1 &lt;strong&gt;chưa có database&lt;/strong&gt; — dữ liệu lưu tạm trong một biến mảng trong bộ nhớ (in-memory). Nghĩa là restart app thì todo mất; đó là chủ ý, Part 2 sẽ thay bằng Postgres.&lt;/p&gt;

&lt;p&gt;Tạo hai file: &lt;code&gt;package.json&lt;/code&gt; (khai báo dự án &amp;amp; thư viện) và &lt;code&gt;server.js&lt;/code&gt; (mã app).&lt;/p&gt;

&lt;h3&gt;
  
  
  2a. &lt;code&gt;package.json&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Tạo file &lt;code&gt;package.json&lt;/code&gt; với nội dung:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"todo-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Todo API - DevOps 101"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"server.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node server.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"express"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.19.2"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Giải thích các field:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt;, &lt;code&gt;version&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt; — thông tin mô tả dự án.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;main&lt;/code&gt; — file khởi động chính.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scripts.start&lt;/code&gt; — định nghĩa lệnh &lt;code&gt;npm start&lt;/code&gt; = chạy &lt;code&gt;node server.js&lt;/code&gt;. Đây là cách chuẩn để "khởi động app" mà không cần nhớ tên file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dependencies.express&lt;/code&gt; — khai báo app cần thư viện &lt;strong&gt;Express&lt;/strong&gt; (framework web cho Node.js). &lt;code&gt;^4.19.2&lt;/code&gt; nghĩa là "phiên bản 4.19.2 trở lên nhưng vẫn trong dòng 4.x".&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2b. &lt;code&gt;server.js&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Tạo file &lt;code&gt;server.js&lt;/code&gt; với nội dung:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Cho phép Express đọc JSON trong body của request (dùng cho POST).&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// "Database" tạm thời: một mảng trong bộ nhớ.&lt;/span&gt;
&lt;span class="c1"&gt;// Restart app là mất — Part 2 sẽ thay bằng Postgres.&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;todos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Đọc DevOps 101 Part 1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;nextId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// GET /todos → trả về toàn bộ danh sách todo dưới dạng JSON.&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/todos&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// POST /todos → thêm một todo mới. Body cần có { "title": "..." }.&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/todos&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;todo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nextId&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// GET /healthz → probe kiểm tra sức khoẻ. Luôn trả 200 nếu app còn sống.&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/healthz&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Lắng nghe trên port 3000, mọi địa chỉ (0.0.0.0) — quan trọng khi chạy trong container.&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Todo API listening on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Giải thích những chỗ quan trọng:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;express.json()&lt;/code&gt; là &lt;strong&gt;middleware&lt;/strong&gt; — nó chạy trước mỗi request và biến JSON trong body thành &lt;code&gt;req.body&lt;/code&gt;. Không có dòng này thì &lt;code&gt;POST&lt;/code&gt; không đọc được &lt;code&gt;title&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;todos&lt;/code&gt; / &lt;code&gt;nextId&lt;/code&gt; là &lt;strong&gt;state in-memory&lt;/strong&gt;. Đơn giản để học; dữ liệu không bền.&lt;/li&gt;
&lt;li&gt;Ba route ứng với ba endpoint trong spec: &lt;code&gt;GET /todos&lt;/code&gt;, &lt;code&gt;POST /todos&lt;/code&gt;, &lt;code&gt;GET /healthz&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/healthz&lt;/code&gt; trả &lt;code&gt;200 OK&lt;/code&gt; — Part 4 sẽ dùng endpoint này làm &lt;strong&gt;health probe&lt;/strong&gt; cho Kubernetes. Đặt sẵn từ giờ để nhất quán.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app.listen(PORT)&lt;/code&gt; — mở cổng và chờ request. &lt;code&gt;console.log&lt;/code&gt; giúp ta biết app đã lên.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ✅ Checkpoint 2
&lt;/h3&gt;

&lt;p&gt;Chạy &lt;code&gt;ls&lt;/code&gt; — phải thấy đủ &lt;code&gt;package.json&lt;/code&gt; và &lt;code&gt;server.js&lt;/code&gt;. Chạy &lt;code&gt;node -c server.js&lt;/code&gt; (kiểm tra cú pháp) — &lt;strong&gt;không in gì&lt;/strong&gt; là đúng.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nếu sai:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;node -c server.js&lt;/code&gt; báo &lt;code&gt;SyntaxError&lt;/code&gt; → có ký tự gõ thiếu (dấu ngoặc, dấu phẩy). Đọc số dòng trong lỗi và sửa.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Bước 3 — Chạy thử local
&lt;/h2&gt;

&lt;p&gt;Trước khi container hoá, hãy chắc app chạy được trên máy trần đã. Đây là thói quen tốt: &lt;strong&gt;cô lập lỗi&lt;/strong&gt; — nếu app hỏng ngay ở đây thì không phải lỗi Docker/K8s.&lt;/p&gt;

&lt;p&gt;Cài thư viện rồi khởi động:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;node server.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;npm install&lt;/code&gt; đọc &lt;code&gt;package.json&lt;/code&gt;, tải Express về thư mục &lt;code&gt;node_modules/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;node server.js&lt;/code&gt; khởi động app. Terminal sẽ "treo" lại (app đang chạy) và in dòng log.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Output mong đợi&lt;/strong&gt; (ở terminal chạy app):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Todo API listening on port 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mở &lt;strong&gt;một terminal thứ hai&lt;/strong&gt; (cửa sổ/tab mới, vẫn ở thư mục &lt;code&gt;todo-ops&lt;/code&gt;) và thử gọi API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl localhost:3000/todos
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output mong đợi:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Đọc DevOps 101 Part 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thêm một todo mới:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"title":"học K8s"}'&lt;/span&gt; localhost:3000/todos
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-X POST&lt;/code&gt; — dùng phương thức POST.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-H 'Content-Type: application/json'&lt;/code&gt; — báo cho server biết body là JSON.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-d '{...}'&lt;/code&gt; — dữ liệu gửi lên.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Output mong đợi:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"học K8s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gọi lại &lt;code&gt;curl localhost:3000/todos&lt;/code&gt; sẽ thấy &lt;strong&gt;cả hai&lt;/strong&gt; todo. Xong thì quay lại terminal thứ nhất, nhấn &lt;code&gt;Ctrl+C&lt;/code&gt; để dừng app.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ Checkpoint 3
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;GET /todos&lt;/code&gt; trả JSON mảng, &lt;code&gt;POST&lt;/code&gt; trả về todo vừa tạo với &lt;code&gt;id&lt;/code&gt; tăng dần.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nếu sai:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;curl&lt;/code&gt; báo &lt;code&gt;Connection refused&lt;/code&gt; → app chưa chạy hoặc đã bị &lt;code&gt;Ctrl+C&lt;/code&gt;. Kiểm tra terminal thứ nhất có dòng &lt;code&gt;listening on port 3000&lt;/code&gt; không.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST&lt;/code&gt; trả &lt;code&gt;{"error":"title is required"}&lt;/code&gt; → thiếu header &lt;code&gt;-H 'Content-Type: application/json'&lt;/code&gt;, nên Express không parse được body.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Cannot find module 'express'&lt;/code&gt; → quên chạy &lt;code&gt;npm install&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Hiểu thêm — Dockerfile là gì?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;Dockerfile&lt;/code&gt; là &lt;strong&gt;công thức&lt;/strong&gt; để Docker xây image. Mỗi dòng là một bước, và Docker chạy chúng theo thứ tự trên xuống. Docker cache lại từng bước, nên ta cố tình sắp xếp: thứ ít đổi (cài thư viện) đặt trên, thứ hay đổi (mã nguồn) đặt dưới — để build lần sau nhanh hơn.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy63gv0i19p1tuzuensgx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy63gv0i19p1tuzuensgx.png" alt="Vì sao COPY package.json trước: khi chỉ sửa mã nguồn, các bước cài thư viện được cache hit; khi sửa package.json thì mới cài lại" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Bước 4 — Đóng gói thành container
&lt;/h2&gt;

&lt;p&gt;Tạo file &lt;code&gt;Dockerfile&lt;/code&gt; (không có đuôi mở rộng) trong thư mục &lt;code&gt;todo-ops&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Bắt đầu từ một image có sẵn Node.js 20 trên nền Alpine Linux (nhẹ).&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;

&lt;span class="c"&gt;# Đặt thư mục làm việc bên trong container. Các lệnh sau chạy trong đây.&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Chỉ copy package.json (và package-lock nếu có) trước.&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;

&lt;span class="c"&gt;# Cài thư viện. Nhờ copy trước, bước này được cache lại khi mã nguồn đổi.&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Bây giờ mới copy toàn bộ mã nguồn còn lại vào /app.&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# Ghi chú rằng container mở port 3000 (tài liệu hoá, không tự mở cổng).&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;

&lt;span class="c"&gt;# Lệnh chạy khi container khởi động.&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Giải thích từng dòng:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FROM node:20-alpine&lt;/code&gt; — nền tảng. Không cần tự cài Node; image này đã có sẵn.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WORKDIR /app&lt;/code&gt; — tạo &amp;amp; vào thư mục &lt;code&gt;/app&lt;/code&gt; trong container.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;COPY package*.json ./&lt;/code&gt; — copy trước file khai báo thư viện. Đây là mẹo tận dụng cache (xem 💡 ở trên).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RUN npm install&lt;/code&gt; — cài Express bên trong image.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;COPY . .&lt;/code&gt; — copy mọi thứ còn lại (gồm &lt;code&gt;server.js&lt;/code&gt;) vào &lt;code&gt;/app&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EXPOSE 3000&lt;/code&gt; — khai báo cổng (mang tính tài liệu).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CMD ["node", "server.js"]&lt;/code&gt; — lệnh chạy app khi container start.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Nên có &lt;code&gt;.dockerignore&lt;/code&gt;:&lt;/strong&gt; để tránh copy &lt;code&gt;node_modules/&lt;/code&gt; local (có thể build cho HĐH khác) và &lt;code&gt;.git&lt;/code&gt; vào image, tạo file &lt;code&gt;.dockerignore&lt;/code&gt;:&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node_modules
.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;p&gt;Bây giờ build image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; todo-api:v1 &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-t todo-api:v1&lt;/code&gt; — đặt &lt;strong&gt;tên:tag&lt;/strong&gt; cho image. Ta dùng đúng &lt;code&gt;todo-api:v1&lt;/code&gt; như spec của series.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.&lt;/code&gt; (dấu chấm cuối) — "build context" là thư mục hiện tại; Docker gửi các file ở đây cho quá trình build.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Output mong đợi&lt;/strong&gt; (rút gọn, kết thúc bằng dòng thành công):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; =&amp;gt; [1/5] FROM docker.io/library/node:20-alpine ...
 =&amp;gt; [4/5] RUN npm install ...
 =&amp;gt; [5/5] COPY . . ...
 =&amp;gt; =&amp;gt; naming to docker.io/library/todo-api:v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ✅ Checkpoint 4
&lt;/h3&gt;

&lt;p&gt;Chạy &lt;code&gt;docker images | grep todo-api&lt;/code&gt; — phải thấy một dòng có &lt;code&gt;todo-api&lt;/code&gt; và tag &lt;code&gt;v1&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nếu sai:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Cannot connect to the Docker daemon&lt;/code&gt; → Docker chưa chạy. Mở Docker Desktop / OrbStack rồi build lại.&lt;/li&gt;
&lt;li&gt;Build dừng ở &lt;code&gt;RUN npm install&lt;/code&gt; → thường do mạng, hoặc &lt;code&gt;package.json&lt;/code&gt; sai cú pháp JSON. Chạy &lt;code&gt;node -c&lt;/code&gt; không giúp cho JSON; kiểm tra dấu phẩy/ngoặc trong &lt;code&gt;package.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Muốn chắc image chạy được, thử luôn: &lt;code&gt;docker run --rm -p 3000:3000 todo-api:v1&lt;/code&gt;, rồi ở terminal khác &lt;code&gt;curl localhost:3000/todos&lt;/code&gt;. &lt;code&gt;Ctrl+C&lt;/code&gt; để dừng.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Hiểu thêm — Kubernetes, cluster, node là gì?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Kubernetes (K8s)&lt;/strong&gt; là hệ điều phối container: bạn khai báo &lt;em&gt;"tôi muốn chạy app này, 1 bản, luôn sống"&lt;/em&gt; và K8s lo phần còn lại (khởi động lại khi chết, phân bổ tài nguyên...).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cluster&lt;/strong&gt; = cả dàn máy do K8s quản lý.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node&lt;/strong&gt; = một máy (thật hoặc ảo) trong cluster, nơi container thực sự chạy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;kind&lt;/code&gt;&lt;/strong&gt; ("Kubernetes IN Docker") giả lập cả một cluster &lt;strong&gt;bên trong Docker&lt;/strong&gt; trên máy bạn — hoàn hảo để học mà không tốn tiền cloud.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Bước 5 — Tạo cluster Kubernetes local bằng kind
&lt;/h2&gt;

&lt;p&gt;Tạo file &lt;code&gt;kind-config.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cluster&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kind.x-k8s.io/v1alpha4&lt;/span&gt;
&lt;span class="na"&gt;nodes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;control-plane&lt;/span&gt;
    &lt;span class="na"&gt;extraPortMappings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
        &lt;span class="na"&gt;hostPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
        &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Giải thích:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;kind: Cluster&lt;/code&gt; / &lt;code&gt;apiVersion&lt;/code&gt; — đây là file cấu hình cho công cụ kind (không phải manifest K8s thường).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nodes&lt;/code&gt; — danh sách máy trong cluster. Ta chỉ cần &lt;strong&gt;một&lt;/strong&gt; node.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;role: control-plane&lt;/code&gt; — node này vừa là "bộ não" (điều khiển cluster) vừa chạy app luôn (đủ cho môi trường học).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;extraPortMappings&lt;/code&gt; — nối &lt;strong&gt;port 80 của máy bạn&lt;/strong&gt; vào &lt;strong&gt;port 80 của node&lt;/strong&gt;. Part 1 &lt;strong&gt;chưa dùng&lt;/strong&gt; cổng này; ta đặt sẵn để &lt;strong&gt;Part 3 (Ingress)&lt;/strong&gt; có thể mở app ra ngoài qua &lt;code&gt;http://localhost&lt;/code&gt;. Đặt từ giờ để không phải tạo lại cluster sau.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tạo cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kind create cluster &lt;span class="nt"&gt;--name&lt;/span&gt; todo-ops &lt;span class="nt"&gt;--config&lt;/span&gt; kind-config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output mong đợi&lt;/strong&gt; (rút gọn):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Creating cluster "todo-ops" ...
 ✓ Ensuring node image ...
 ✓ Preparing nodes ...
 ✓ Starting control-plane ...
Set kubectl context to "kind-todo-ops"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kiểm tra &lt;code&gt;kubectl&lt;/code&gt; đã kết nối được cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output mong đợi:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                      STATUS   ROLES           AGE   VERSION
todo-ops-control-plane    Ready    control-plane   1m    v1.3x.x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo &lt;strong&gt;namespace&lt;/strong&gt; &lt;code&gt;todo&lt;/code&gt; để nhóm tài nguyên của app lại:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create namespace todo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output mong đợi:&lt;/strong&gt; &lt;code&gt;namespace/todo created&lt;/code&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Hiểu thêm — Namespace:&lt;/strong&gt; namespace là "ngăn kéo" logic trong cluster để tách biệt tài nguyên (theo team, theo app, theo môi trường). Ta để mọi thứ của app trong namespace &lt;code&gt;todo&lt;/code&gt; cho gọn, thay vì trộn vào &lt;code&gt;default&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  ✅ Checkpoint 5
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;kubectl get nodes&lt;/code&gt; → node &lt;code&gt;STATUS = Ready&lt;/code&gt;. &lt;code&gt;kubectl get ns&lt;/code&gt; → có dòng &lt;code&gt;todo&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nếu sai:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node kẹt ở &lt;code&gt;NotReady&lt;/code&gt; vài phút → chờ thêm; nếu quá lâu, Docker có thể thiếu RAM (cấp ≥4GB cho Docker Desktop).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;port is already allocated&lt;/code&gt; khi tạo cluster → có gì đó đang chiếm port 80 trên máy. Tắt tiến trình đó, hoặc tạm bỏ khối &lt;code&gt;extraPortMappings&lt;/code&gt; (sẽ xử lý ở Part 3).&lt;/li&gt;
&lt;li&gt;Tạo lỗi và muốn làm lại sạch: &lt;code&gt;kind delete cluster --name todo-ops&lt;/code&gt; rồi tạo lại.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Hiểu thêm — Vì sao phải &lt;code&gt;kind load&lt;/code&gt; image thủ công?&lt;/strong&gt;&lt;br&gt;
Cluster kind chạy &lt;strong&gt;bên trong Docker riêng của nó&lt;/strong&gt;. Image &lt;code&gt;todo-api:v1&lt;/code&gt; bạn vừa build nằm ở Docker của &lt;strong&gt;máy host&lt;/strong&gt; — kind &lt;strong&gt;không tự nhìn thấy&lt;/strong&gt; nó, và ta cũng chưa đẩy image lên registry nào (như Docker Hub). Vì vậy phải "nạp" image từ host vào trong cluster kind bằng tay.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Bước 6 — Nạp image vào cluster kind
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kind load docker-image todo-api:v1 &lt;span class="nt"&gt;--name&lt;/span&gt; todo-ops
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Lệnh này copy image &lt;code&gt;todo-api:v1&lt;/code&gt; từ Docker host vào trong node của cluster &lt;code&gt;todo-ops&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Output mong đợi:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Image: "todo-api:v1" with ID "sha256:..." not yet present on node "todo-ops-control-plane", loading...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ✅ Checkpoint 6
&lt;/h3&gt;

&lt;p&gt;Không có lỗi đỏ. (Nếu lệnh chạy xong im lặng hoặc in dòng &lt;code&gt;loading...&lt;/code&gt; là ổn.)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nếu sai:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;image: "todo-api:v1" not present locally&lt;/code&gt; → chưa build image, hoặc gõ sai tên/tag. Quay lại Bước 4 chạy &lt;code&gt;docker build&lt;/code&gt; lại.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Đây là lỗi hay gặp nhất về sau:&lt;/strong&gt; nếu ở Bước 7 pod báo &lt;code&gt;ErrImagePull&lt;/code&gt; / &lt;code&gt;ImagePullBackOff&lt;/code&gt;, gần như chắc chắn là &lt;strong&gt;quên bước load này&lt;/strong&gt; hoặc load xong nhưng sau đó build lại image mà chưa load lần nữa. Mỗi lần build image mới, phải &lt;code&gt;kind load&lt;/code&gt; lại.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Hiểu thêm — Pod, Deployment, Service khác nhau thế nào?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pod&lt;/strong&gt;: đơn vị chạy nhỏ nhất của K8s — bọc quanh một (hoặc vài) container. Pod dễ chết/bị thay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment&lt;/strong&gt;: người quản lý Pod. Bạn nói &lt;em&gt;"tôi muốn 1 pod luôn sống"&lt;/em&gt; — Deployment đảm bảo điều đó, pod chết thì nó tạo pod mới. &lt;strong&gt;Ta luôn tạo Deployment, không tạo Pod trần.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service&lt;/strong&gt;: địa chỉ mạng ổn định để gọi tới các Pod (vì pod hay đổi). Part 1 &lt;strong&gt;chưa cần&lt;/strong&gt; Service — ta dùng &lt;code&gt;port-forward&lt;/code&gt; để truy cập tạm. Part 3 sẽ thêm Service + Ingress.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv3gf95gydt1vx17al7kb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv3gf95gydt1vx17al7kb.png" alt="Deployment tạo và giám sát Pod; Service cho địa chỉ mạng ổn định để client gọi tới Pod qua label selector" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Hiểu thêm — Vì sao viết YAML thay vì lệnh tắt?&lt;/strong&gt;&lt;br&gt;
K8s có lệnh nhanh như &lt;code&gt;kubectl create deployment ...&lt;/code&gt;. Nhưng cách làm chuẩn của DevOps là &lt;strong&gt;declarative&lt;/strong&gt;: mô tả &lt;em&gt;trạng thái mong muốn&lt;/em&gt; trong file YAML, lưu vào git, rồi &lt;code&gt;kubectl apply&lt;/code&gt;. File YAML là nguồn sự thật, có thể review, tái lập, và &lt;strong&gt;Part 6 (GitOps)&lt;/strong&gt; sẽ tự động apply chúng từ git. Vì thế ngay từ đầu ta học viết YAML.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Bước 7 — Deploy bằng manifest YAML
&lt;/h2&gt;

&lt;p&gt;Tạo thư mục &lt;code&gt;k8s/&lt;/code&gt; và file &lt;code&gt;k8s/deployment.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;k8s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nội dung &lt;code&gt;k8s/deployment.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;todo-api&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;todo-api&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;todo-api&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;todo-api&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;todo-api&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;todo-api:v1&lt;/span&gt;
          &lt;span class="na"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IfNotPresent&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Giải thích các field quan trọng:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apiVersion: apps/v1&lt;/code&gt; — nhóm &amp;amp; phiên bản API cho Deployment. K8s dùng cái này để biết cách hiểu file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kind: Deployment&lt;/code&gt; — loại tài nguyên: một Deployment.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;metadata.name&lt;/code&gt; — tên tài nguyên (&lt;code&gt;todo-api&lt;/code&gt;). &lt;code&gt;labels&lt;/code&gt; — nhãn để phân loại/tìm kiếm.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;spec&lt;/code&gt; — mô tả trạng thái mong muốn:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;replicas: 1&lt;/code&gt; — muốn có &lt;strong&gt;1&lt;/strong&gt; pod chạy.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;selector.matchLabels&lt;/code&gt; — Deployment quản lý các pod có nhãn &lt;code&gt;app: todo-api&lt;/code&gt;. &lt;strong&gt;Phải khớp&lt;/strong&gt; với &lt;code&gt;template.metadata.labels&lt;/code&gt; bên dưới, nếu không K8s báo lỗi.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;template&lt;/code&gt; — "khuôn" để tạo pod:&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;template.metadata.labels&lt;/code&gt; — nhãn dán lên pod (khớp với selector).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;template.spec.containers&lt;/code&gt; — danh sách container trong pod:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt; — tên container.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;image: todo-api:v1&lt;/code&gt; — dùng đúng image ta đã build &amp;amp; load.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;imagePullPolicy: IfNotPresent&lt;/code&gt; — &lt;strong&gt;quan trọng cho kind&lt;/strong&gt;: chỉ kéo image nếu chưa có sẵn trên node. Vì ta đã &lt;code&gt;kind load&lt;/code&gt;, K8s dùng image local thay vì đi tìm trên internet (và fail).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ports.containerPort: 3000&lt;/code&gt; — báo container lắng nghe port 3000.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apply manifest vào namespace &lt;code&gt;todo&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; k8s/deployment.yaml &lt;span class="nt"&gt;-n&lt;/span&gt; todo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output mong đợi:&lt;/strong&gt; &lt;code&gt;deployment.apps/todo-api created&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Xem pod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; todo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output mong đợi&lt;/strong&gt; (chờ ~10-30 giây cho tới khi &lt;code&gt;Running&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                        READY   STATUS    RESTARTS   AGE
todo-api-xxxxxxxxxx-yyyyy   1/1     Running   0          20s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ✅ Checkpoint 7
&lt;/h3&gt;

&lt;p&gt;Pod ở trạng thái &lt;code&gt;Running&lt;/code&gt; và cột &lt;code&gt;READY&lt;/code&gt; là &lt;code&gt;1/1&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nếu sai:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;STATUS = ErrImagePull&lt;/code&gt; hoặc &lt;code&gt;ImagePullBackOff&lt;/code&gt; → K8s không tìm thấy image. Nguyên nhân số 1: &lt;strong&gt;quên &lt;code&gt;kind load&lt;/code&gt;&lt;/strong&gt; (Bước 6), hoặc thiếu &lt;code&gt;imagePullPolicy: IfNotPresent&lt;/code&gt;. Chạy lại &lt;code&gt;kind load docker-image todo-api:v1 --name todo-ops&lt;/code&gt;, rồi &lt;code&gt;kubectl delete pod -n todo -l app=todo-api&lt;/code&gt; để pod được tạo lại.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;STATUS = CrashLoopBackOff&lt;/code&gt; → container khởi động rồi chết liên tục, thường do &lt;strong&gt;lỗi trong &lt;code&gt;server.js&lt;/code&gt;&lt;/strong&gt;. Xem log để biết lý do:&lt;/li&gt;
&lt;/ul&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;  kubectl logs &lt;span class="nt"&gt;-n&lt;/span&gt; todo &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;todo-api
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Log sẽ in đúng lỗi Node.js (ví dụ &lt;code&gt;SyntaxError&lt;/code&gt;, &lt;code&gt;Cannot find module&lt;/code&gt;). Sửa &lt;code&gt;server.js&lt;/code&gt;, &lt;strong&gt;build lại image, &lt;code&gt;kind load&lt;/code&gt; lại&lt;/strong&gt;, rồi &lt;code&gt;kubectl rollout restart deploy/todo-api -n todo&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;error validating data&lt;/code&gt; khi apply → YAML sai thụt lề (YAML cực nhạy với dấu cách; chỉ dùng space, không dùng tab) hoặc &lt;code&gt;selector&lt;/code&gt; không khớp &lt;code&gt;template.labels&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Hiểu thêm — &lt;code&gt;port-forward&lt;/code&gt;:&lt;/strong&gt; app đang chạy &lt;em&gt;bên trong&lt;/em&gt; cluster, máy bạn chưa có đường vào. &lt;code&gt;kubectl port-forward&lt;/code&gt; mở một "đường hầm" tạm: request tới &lt;code&gt;localhost:3000&lt;/code&gt; trên máy bạn được chuyển thẳng vào pod. Đây là cách nhanh để &lt;strong&gt;debug/test&lt;/strong&gt;, không phải cách mở app cho người dùng thật (việc đó là Service + Ingress ở Part 3).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Bước 8 — Truy cập app đang chạy trong cluster
&lt;/h2&gt;

&lt;p&gt;Mở đường hầm tới Deployment (chạy nền với &lt;code&gt;&amp;amp;&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl port-forward &lt;span class="nt"&gt;-n&lt;/span&gt; todo deploy/todo-api 3000:3000 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;deploy/todo-api&lt;/code&gt; — forward tới Deployment &lt;code&gt;todo-api&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;3000:3000&lt;/code&gt; — port máy bạn : port trong pod.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;amp;&lt;/code&gt; — chạy nền để bạn gõ lệnh tiếp trong cùng terminal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Output mong đợi:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Forwarding from 127.0.0.1:3000 -&amp;gt; 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gọi API (giờ là gọi vào app &lt;strong&gt;trong cluster&lt;/strong&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl localhost:3000/todos
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output mong đợi:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Đọc DevOps 101 Part 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Xong thì tắt đường hầm nền:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;kill&lt;/span&gt; %1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;%1&lt;/code&gt; — job chạy nền số 1 (chính là &lt;code&gt;port-forward&lt;/code&gt;). Lệnh này dừng nó.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ✅ Checkpoint 8
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;curl localhost:3000/todos&lt;/code&gt; trả JSON mảng todo — &lt;strong&gt;từ app chạy trong Kubernetes&lt;/strong&gt;. Bạn vừa hoàn thành vòng đời: code → image → cluster → truy cập.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nếu sai:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;curl&lt;/code&gt; báo &lt;code&gt;Connection refused&lt;/code&gt; → đường hầm chưa mở hoặc đã bị &lt;code&gt;kill&lt;/code&gt;. Xem lại có dòng &lt;code&gt;Forwarding from ...&lt;/code&gt; không; nếu terminal đã chạy nền, gõ &lt;code&gt;jobs&lt;/code&gt; để xem job còn sống.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;unable to forward ... pod is not running&lt;/code&gt; → pod chưa &lt;code&gt;Running&lt;/code&gt;. Quay lại Checkpoint 7.&lt;/li&gt;
&lt;li&gt;Nhớ &lt;code&gt;kill %1&lt;/code&gt; sau khi xong; nếu quên, port 3000 vẫn bị chiếm cho lần sau. Có thể tắt bằng &lt;code&gt;kill %1&lt;/code&gt; hoặc đóng terminal.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Bạn đã hiểu gì
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0okdz919ji79f1gcpcwl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0okdz919ji79f1gcpcwl.png" alt="Vòng đời Part 1: mã nguồn + Dockerfile → docker build → image todo-api:v1 → kind load → Deployment/Pod trong cluster → port-forward → curl từ máy bạn" width="800" height="244"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tóm lại, bạn vừa đi trọn con đường từ số 0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App → image:&lt;/strong&gt; viết Todo API Node.js, đóng gói bằng &lt;code&gt;Dockerfile&lt;/code&gt; thành image &lt;code&gt;todo-api:v1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster local:&lt;/strong&gt; dùng &lt;code&gt;kind&lt;/code&gt; tạo cluster &lt;code&gt;todo-ops&lt;/code&gt; chạy trong Docker, tạo namespace &lt;code&gt;todo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nạp image:&lt;/strong&gt; hiểu vì sao phải &lt;code&gt;kind load&lt;/code&gt; (cluster không thấy Docker host).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy declarative:&lt;/strong&gt; viết &lt;code&gt;k8s/deployment.yaml&lt;/code&gt; thật, hiểu &lt;code&gt;apiVersion / kind / metadata / spec / selector / template / containers&lt;/code&gt;, và vì sao &lt;code&gt;imagePullPolicy: IfNotPresent&lt;/code&gt; quan trọng với kind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Khái niệm nền:&lt;/strong&gt; container vs image, Pod vs Deployment vs Service, namespace, port-forward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cách tự sửa lỗi:&lt;/strong&gt; đọc &lt;code&gt;kubectl get pods&lt;/code&gt; (trạng thái) và &lt;code&gt;kubectl logs&lt;/code&gt; (nguyên nhân), phân biệt &lt;code&gt;ImagePullBackOff&lt;/code&gt; với &lt;code&gt;CrashLoopBackOff&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cluster của bạn vẫn đang chạy — &lt;strong&gt;giữ nguyên&lt;/strong&gt;, Part 2 sẽ dùng tiếp. (Nếu cần dọn hẳn: &lt;code&gt;kind delete cluster --name todo-ops&lt;/code&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Tiếp theo
&lt;/h2&gt;

&lt;p&gt;Trong &lt;strong&gt;Part 2 → Config &amp;amp; State&lt;/strong&gt;, ta sẽ:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Thêm &lt;strong&gt;PostgreSQL&lt;/strong&gt; để todo không mất khi restart (thay mảng in-memory).&lt;/li&gt;
&lt;li&gt;Tách cấu hình ra khỏi code bằng &lt;strong&gt;ConfigMap&lt;/strong&gt; và &lt;strong&gt;Secret&lt;/strong&gt; (chuỗi kết nối DB, mật khẩu).&lt;/li&gt;
&lt;li&gt;Cập nhật app đọc config từ biến môi trường thay vì hard-code.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>docker</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
