{"id":"l-dk-1-2","courseId":"docker-kubernetes","lang":"en","kind":"lesson","summary":"Write a working Dockerfile for notes-api, build it into a tagged image, run it with a published port, and know what each instruction does.","html":"<p>You can describe notes-api’s runtime in one sentence: “Node.js 24, our compiled code, our dependencies, start with <code dir=\"ltr\">node dist/server.js</code> on port 3000.” A Dockerfile is that sentence written so a machine can repeat it exactly, on any laptop or build server, forever.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The app you’re packaging</h2>\n<p>The repository looks like this:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"text\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Text</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code>notes-api/\n  package.json\n  package-lock.json\n  tsconfig.json\n  src/\n    server.ts\n    db.ts</code></pre></div>\n<p><code dir=\"ltr\">npm run build</code> compiles <code dir=\"ltr\">src/</code> into <code dir=\"ltr\">dist/</code> with <code dir=\"ltr\">tsc</code>, and <code dir=\"ltr\">npm start</code> runs <code dir=\"ltr\">node dist/server.js</code>. Here is the part of the server that matters for containers:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">src/server.ts</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> express </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'express'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { pool } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './db.js'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> app</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> express</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">app.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">use</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(express.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">json</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">());</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">app.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">get</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/healthz'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">_req</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">res</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> res.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">send</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'ok'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">app.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">get</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/readyz'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">_req</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">res</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  try</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pool.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">query</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'SELECT 1'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    res.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">send</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'ready'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">catch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    res.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">status</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">503</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">send</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'database unreachable'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// …notes routes</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> port</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(process.env.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">PORT</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ??</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 3000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">app.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">listen</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(port, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'0.0.0.0'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`notes-api listening on ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">port</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">));</span></span></code></pre></div>\n<p>Note the <code dir=\"ltr\">'0.0.0.0'</code>. Hold that thought.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">A first Dockerfile</h2>\n<p>Create <code dir=\"ltr\">Dockerfile</code> (capital D, no extension) at the repository root:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"dockerfile\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">Dockerfile</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># syntax=docker/dockerfile:1</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> node:24-slim</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WORKDIR</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /app</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> . .</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">RUN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> npm ci</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">RUN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> npm run build</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">EXPOSE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> 3000</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">CMD</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"node\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"dist/server.js\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span></code></pre></div>\n<p>Line by line:</p>\n<ul>\n<li><code dir=\"ltr\"># syntax=docker/dockerfile:1</code> pins the Dockerfile frontend to the latest 1.x, which gives you current features such as build secrets and cache mounts.</li>\n<li><code dir=\"ltr\">FROM node:24-slim</code> starts from the official Node.js 24 image on a trimmed Debian base. Every image starts from another image.</li>\n<li><code dir=\"ltr\">WORKDIR /app</code> creates <code dir=\"ltr\">/app</code> if needed and makes it the working directory for every instruction after it, and for the running container.</li>\n<li><code dir=\"ltr\">COPY . .</code> copies the build context into <code dir=\"ltr\">/app</code>.</li>\n<li><code dir=\"ltr\">RUN npm ci</code> installs exactly what <code dir=\"ltr\">package-lock.json</code> says. Use <code dir=\"ltr\">ci</code>, not <code dir=\"ltr\">install</code>, in images: it fails if the lockfile and <code dir=\"ltr\">package.json</code> disagree instead of quietly rewriting the lockfile.</li>\n<li><code dir=\"ltr\">RUN npm run build</code> compiles TypeScript to <code dir=\"ltr\">dist/</code>.</li>\n<li><code dir=\"ltr\">EXPOSE 3000</code> records that the app listens on 3000. It publishes nothing.</li>\n<li><code dir=\"ltr\">CMD</code> is the default command when a container starts.</li>\n</ul>\n<p>Why <code dir=\"ltr\">node:24-slim</code>? Node.js 24 is a long-term-support line with security fixes into 2028, and the official image is rebuilt whenever Node or Debian ships a patch. The <code dir=\"ltr\">-slim</code> variant leaves out compilers, man pages and dozens of packages that the full <code dir=\"ltr\">node:24</code> image includes for building native modules. You don’t need them to run JavaScript, and every package you don’t ship is one the security scanner can’t complain about. If a dependency ever needs to compile native code, you’ll handle that in a separate build stage in the next section.</p>\n<p>This file works, and it has a performance problem you’ll fix in the next lesson. Get it running first.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Build and run</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> build</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -t</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api:dev</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> .</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> run</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --rm</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -p</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 3000:3000</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api:dev</span></span></code></pre></div>\n<p><code dir=\"ltr\">-t notes-api:dev</code> names the image (<code dir=\"ltr\">repository:tag</code>). The trailing <code dir=\"ltr\">.</code> is the <strong>build context</strong>: the directory Docker hands to the builder. <code dir=\"ltr\">COPY</code> can only read files inside it, which is why <code dir=\"ltr\">COPY ../shared .</code> fails.</p>\n<p><code dir=\"ltr\">docker run</code> creates a container from the image. <code dir=\"ltr\">--rm</code> deletes it when it stops, so you don’t collect dead containers. <code dir=\"ltr\">-p 3000:3000</code> maps port 3000 on your machine to port 3000 in the container. In another terminal:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">curl</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> localhost:3000/healthz</span></span></code></pre></div>\n<p>You get <code dir=\"ltr\">ok</code>. <code dir=\"ltr\">/readyz</code> returns 503 because there’s no database yet; Compose fixes that in section 3.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Exec form, and why it matters</h2>\n<p><code dir=\"ltr\">CMD</code> has two forms. Exec form, <code dir=\"ltr\">CMD [&quot;node&quot;, &quot;dist/server.js&quot;]</code>, runs <code dir=\"ltr\">node</code> directly as PID 1. Shell form, <code dir=\"ltr\">CMD node dist/server.js</code>, runs <code dir=\"ltr\">/bin/sh -c &quot;node dist/server.js&quot;</code>, and the Dockerfile reference warns that a command started that way won’t receive Unix signals. When Docker or Kubernetes asks your app to stop, it sends <code dir=\"ltr\">SIGTERM</code> to PID 1. If PID 1 is a shell that doesn’t pass it on, your app never hears it, gets killed hard after the timeout, and drops whatever requests it was serving.</p>\n<p><code dir=\"ltr\">CMD npm start</code> is the same problem with an extra process in the chain. Run <code dir=\"ltr\">node</code> directly.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Let the builder review you</span></p>\n<p>Run <code dir=\"ltr\">docker build --check .</code> to evaluate the Dockerfile against Docker’s build checks without building. It flags shell-form <code dir=\"ltr\">CMD</code> as <code dir=\"ltr\">JSONArgsRecommended</code>, along with mismatched stage names, legacy <code dir=\"ltr\">KEY value</code> env syntax and more. It takes a second; add it to CI.</p>\n</aside>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Listening on localhost inside the container</span></p>\n<p>Many dev servers default to <code dir=\"ltr\">127.0.0.1</code>. Inside a container that’s the container’s own loopback, and published ports arrive on its network interface instead. The symptom is maddening: the port is mapped, the app logs “listening”, and every request gets <code dir=\"ltr\">connection reset</code>. Bind to <code dir=\"ltr\">0.0.0.0</code>.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">Look at what you built</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"5\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> image</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ls</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> image</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> inspect</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api:dev</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --format</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '{{.Config.Cmd}} {{.Config.ExposedPorts}}'</span></span></code></pre></div>\n<p><code dir=\"ltr\">CMD</code> is only a default. Anything you type after the image name replaces it, which is handy for one-off checks:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"6\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> run</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --rm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api:dev</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> node</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --version</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> run</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --rm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api:dev</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ls</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> dist</span></span></code></pre></div>\n<p>There’s also <code dir=\"ltr\">ENTRYPOINT</code>, which fixes the executable and treats <code dir=\"ltr\">CMD</code> as its default arguments. Some images use it (you’ll meet one in the next section whose entrypoint is <code dir=\"ltr\">node</code> itself). For your own app images, a plain exec-form <code dir=\"ltr\">CMD</code> is the simplest thing that works, and it keeps overrides like the ones above easy.</p>\n<p>The image is several hundred megabytes: Node.js, the Debian base, your dev dependencies (TypeScript included) and your source. Section 2 cuts that down. First, though, every change to <code dir=\"ltr\">server.ts</code> currently reinstalls all your dependencies, and that’s the next lesson.</p>\n","toc":[{"id":"sec-1","text":"The app you’re packaging","level":2},{"id":"sec-2","text":"A first Dockerfile","level":2},{"id":"sec-3","text":"Build and run","level":2},{"id":"sec-4","text":"Exec form, and why it matters","level":2},{"id":"sec-5","text":"Look at what you built","level":2}],"takeaways":["A Dockerfile is a recipe of instructions; each <code dir=\"ltr\">FROM</code>, <code dir=\"ltr\">COPY</code> and <code dir=\"ltr\">RUN</code> produces a filesystem layer in the image.","The build context is the directory you pass to <code dir=\"ltr\">docker build</code>, and <code dir=\"ltr\">COPY</code> can only see files inside it.","<code dir=\"ltr\">EXPOSE</code> only documents a port; <code dir=\"ltr\">-p host:container</code> on <code dir=\"ltr\">docker run</code> is what makes it reachable.","Write <code dir=\"ltr\">CMD</code> in exec form (<code dir=\"ltr\">[&quot;node&quot;, &quot;dist/server.js&quot;]</code>) so your app is PID 1 and receives stop signals.","A server inside a container must listen on <code dir=\"ltr\">0.0.0.0</code>, not <code dir=\"ltr\">127.0.0.1</code>, or published ports can’t reach it."],"further":[{"title":"Dockerfile reference","url":"https://docs.docker.com/reference/dockerfile/"},{"title":"Writing a Dockerfile","url":"https://docs.docker.com/get-started/docker-concepts/building-images/writing-a-dockerfile/"},{"title":"Build checks","url":"https://docs.docker.com/build/checks/"}],"quiz":[{"q":"<p>You run <code dir=\"ltr\">docker build -t notes-api:dev .</code> from the repository root. What does the final <code dir=\"ltr\">.</code> mean?</p>","options":[{"text":"Tag the image with the name of the current directory.","why":"The tag comes from <code dir=\"ltr\">-t</code>. The trailing argument is about files, not names."},{"text":"Use the current directory as the build context, the set of files <code dir=\"ltr\">COPY</code> can read.","why":"Correct. Docker sends this directory to the builder; <code dir=\"ltr\">COPY</code> paths are relative to it, and nothing outside it is visible."},{"text":"Write the finished image into the current directory.","why":"Images go into Docker’s local image store, not your working directory. <code dir=\"ltr\">docker image ls</code> lists them."},{"text":"Look for the Dockerfile in the current directory but take files from <code dir=\"ltr\">/</code>.","why":"The context and the Dockerfile location are separate (<code dir=\"ltr\">-f</code> sets the Dockerfile), but <code dir=\"ltr\">COPY</code> never reads from the host’s <code dir=\"ltr\">/</code>."}],"answer":1},{"q":"<p>The Dockerfile has <code dir=\"ltr\">EXPOSE 3000</code>. You run <code dir=\"ltr\">docker run notes-api:dev</code> and <code dir=\"ltr\">curl localhost:3000</code> on your laptop fails. Why?</p>","options":[{"text":"<code dir=\"ltr\">EXPOSE</code> needs the <code dir=\"ltr\">tcp</code> suffix to take effect.","why":"<code dir=\"ltr\">tcp</code> is the default protocol. Adding it changes nothing about reachability."},{"text":"The container needs a restart before exposed ports open.","why":"Restarting doesn’t publish anything. Ports are published when the container is created."},{"text":"Port 3000 is reserved by Docker on the host.","why":"Docker reserves no such port. The problem is that nothing was published."},{"text":"<code dir=\"ltr\">EXPOSE</code> is documentation; you still need <code dir=\"ltr\">-p 3000:3000</code> to publish the port to the host.","why":"Correct. <code dir=\"ltr\">-p host:container</code> creates the actual port mapping. <code dir=\"ltr\">EXPOSE</code> tells readers and tools which port the app uses."}],"answer":3},{"q":"<p>Which <code dir=\"ltr\">CMD</code> lets the Node.js process receive <code dir=\"ltr\">SIGTERM</code> directly when the container is stopped?</p>","options":[{"text":"<code dir=\"ltr\">CMD [&quot;node&quot;, &quot;dist/server.js&quot;]</code>","why":"Correct. Exec form runs <code dir=\"ltr\">node</code> as PID 1 with no shell in between, so signals go straight to your app."},{"text":"<code dir=\"ltr\">CMD node dist/server.js</code>","why":"Shell form wraps the command in <code dir=\"ltr\">/bin/sh -c</code>, and the shell doesn’t pass signals on. Docker’s build checks flag it for this reason."},{"text":"<code dir=\"ltr\">CMD [&quot;sh&quot;, &quot;-c&quot;, &quot;node dist/server.js&quot;]</code>","why":"This is exec form, but the program it execs is a shell, so you’re back to a shell sitting between Docker and Node."},{"text":"<code dir=\"ltr\">CMD npm start</code>","why":"Shell form plus npm puts two processes between the signal and your server. Run <code dir=\"ltr\">node</code> directly."}],"answer":0},{"q":"<p>notes-api logs <code dir=\"ltr\">listening on 127.0.0.1:3000</code> inside the container. You published <code dir=\"ltr\">-p 3000:3000</code>, but every request gets <code dir=\"ltr\">connection reset</code>. What’s the fix?</p>","options":[{"text":"Publish the port as <code dir=\"ltr\">-p 127.0.0.1:3000:3000</code>.","why":"That restricts which host address accepts connections; the app inside still only listens on the container’s loopback, which the port mapping can’t reach."},{"text":"Add <code dir=\"ltr\">EXPOSE 3000/tcp</code> to the Dockerfile.","why":"<code dir=\"ltr\">EXPOSE</code> doesn’t change where the app listens."},{"text":"Make the app listen on <code dir=\"ltr\">0.0.0.0</code> so it accepts connections from the container’s network interface.","why":"Correct. Published traffic arrives on the container’s network interface, not its loopback. Listening on all interfaces fixes it."},{"text":"Run the container with <code dir=\"ltr\">--network host</code> permanently.","why":"That works around the symptom by dropping network isolation. Fix the listen address instead."}],"answer":2}],"exercise":{"kind":"spot-bug","language":"dockerfile","code":"# syntax=docker/dockerfile:1\nFROM node:24-slim\nRUN mkdir /app\nRUN cd /app\nCOPY . .\nRUN npm ci\nRUN npm run build\nEXPOSE 3000\nCMD npm start","bugLines":[4,9],"title":"Review a teammate's first Dockerfile","prompt":"<p>This Dockerfile builds, but the files end up in the wrong place and the app is killed hard every time the container stops. Select the <strong>two</strong> lines that cause these problems.</p>\n","hints":["Each <code dir=\"ltr\">RUN</code> executes in a fresh shell. Does a <code dir=\"ltr\">cd</code> in one <code dir=\"ltr\">RUN</code> affect the next instruction?","Which instruction decides what becomes PID 1, and in which form is it written?"],"explanation":"<p>Line 4: <code dir=\"ltr\">RUN cd /app</code> changes directory only inside that one <code dir=\"ltr\">RUN</code> step’s shell, so <code dir=\"ltr\">COPY . .</code> and <code dir=\"ltr\">npm ci</code> still run in <code dir=\"ltr\">/</code>, dumping the project into the image root. Use <code dir=\"ltr\">WORKDIR /app</code>, which also creates the directory (so line 3 can go too).</p>\n<p>Line 9: <code dir=\"ltr\">CMD npm start</code> is shell form, so <code dir=\"ltr\">/bin/sh -c</code> and then npm sit between Docker and Node. <code dir=\"ltr\">SIGTERM</code> doesn’t reach the server, and after the grace period it is killed mid-request. Use exec form and run Node directly:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"dockerfile\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Dockerfile</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WORKDIR</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /app</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># …</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">CMD</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"node\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"dist/server.js\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span></code></pre></div>\n","checkLabels":{}},"words":761,"readMinutes":4}