{"id":"l-dk-3-3","courseId":"docker-kubernetes","lang":"en","kind":"lesson","summary":"Give Postgres and notes-api real healthchecks, make the API wait for a healthy database and a finished migration with <code dir=\"ltr\">depends_on</code> conditions, and keep retry logic in the app anyway.","html":"<p>Every cold start of the notes-api stack had the same flaky first minute: the API started, tried Postgres, got <code dir=\"ltr\">ECONNREFUSED</code>, and crashed. The <code dir=\"ltr\">depends_on: [db]</code> in the file looked like it should have prevented it. It didn’t, because “started” and “ready” are different things, and Compose only knew about the first.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">What depends_on really waits for</h2>\n<p>The short form says: start <code dir=\"ltr\">db</code> before <code dir=\"ltr\">api</code>.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">YAML</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:#116329;--shiki-dark:#7EE787\">services</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  api</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    depends_on</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:#0A3069;--shiki-dark:#A5D6FF\">db</span></span></code></pre></div>\n<p>Compose does exactly that: it starts the <code dir=\"ltr\">db</code> container and then the <code dir=\"ltr\">api</code> container, about a second apart. But Postgres needs several seconds after its process starts to initialise, more on the first run when it creates the database and runs your init scripts. The API connects during that window and is refused.</p>\n<p>To wait for <em>ready</em>, Compose needs a way to test readiness. That’s a healthcheck.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Healthchecks</h2>\n<p>A healthcheck is a command Docker runs inside the container on a schedule. Exit code 0 means healthy; anything else counts as a failure.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">compose.yaml</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:#116329;--shiki-dark:#7EE787\">  db</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    image</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">postgres:17</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    environment</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      POSTGRES_USER</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">notes</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      POSTGRES_PASSWORD</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">notes</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      POSTGRES_DB</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">notes</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    volumes</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:#0A3069;--shiki-dark:#A5D6FF\">pgdata:/var/lib/postgresql/data</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    healthcheck</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"CMD-SHELL\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"pg_isready -U notes -d notes\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      interval</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">10s</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      timeout</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">3s</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      retries</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      start_period</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">30s</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      start_interval</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">1s</span></span></code></pre></div>\n<p><code dir=\"ltr\">pg_isready</code> ships with the Postgres image and exits 0 once the server accepts connections. The timing fields:</p>\n<ul>\n<li><code dir=\"ltr\">interval</code>: how often to run the check once the container is running normally.</li>\n<li><code dir=\"ltr\">timeout</code>: how long one check may take before it counts as failed.</li>\n<li><code dir=\"ltr\">retries</code>: consecutive failures before the container is marked <code dir=\"ltr\">unhealthy</code>.</li>\n<li><code dir=\"ltr\">start_period</code>: a grace period after start during which failures don’t count toward <code dir=\"ltr\">retries</code>.</li>\n<li><code dir=\"ltr\">start_interval</code>: how often to check <em>during</em> the start period, so a fast start is detected quickly instead of waiting a full <code dir=\"ltr\">interval</code>.</li>\n</ul>\n<p><code dir=\"ltr\">docker compose ps</code> now shows <code dir=\"ltr\">(healthy)</code> or <code dir=\"ltr\">(unhealthy)</code> next to the status. When a check fails and you want to know why, the last few results, including their output, are stored on the container:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"2\"><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\"> inspect</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-db-1</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --format</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '{{json .State.Health}}'</span></span></code></pre></div>\n<p>One thing healthchecks don’t do in plain Docker or Compose: they don’t restart an unhealthy container. The status is information, used by <code dir=\"ltr\">depends_on</code> conditions and by you. Kubernetes is different, as you’ll see with liveness probes in section 5, where a failing check does trigger a restart. Knowing which tool acts on a failed check, and which only reports it, matters at 3 a.m.</p>\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>CMD versus CMD-SHELL</span></p>\n<p><code dir=\"ltr\">[&quot;CMD&quot;, &quot;pg_isready -U notes -d notes&quot;]</code> looks right and always fails. With <code dir=\"ltr\">CMD</code>, each list item is one argument, so Docker tries to run a program whose name is the entire string. Either split it, <code dir=\"ltr\">[&quot;CMD&quot;, &quot;pg_isready&quot;, &quot;-U&quot;, &quot;notes&quot;, &quot;-d&quot;, &quot;notes&quot;]</code>, or use <code dir=\"ltr\">CMD-SHELL</code> with one string. The giveaway is a container that is <code dir=\"ltr\">unhealthy</code> forever while the database works perfectly.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Waiting on conditions</h2>\n<p>The long form of <code dir=\"ltr\">depends_on</code> lets you say what to wait for:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">compose.yaml</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:#116329;--shiki-dark:#7EE787\">services</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  migrate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    image</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">notes-api:dev</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    command</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/migrate.js\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    environment</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      DATABASE_URL</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">postgres://notes:notes@db:5432/notes</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    depends_on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      db</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        condition</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">service_healthy</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  api</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    build</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    image</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">notes-api:dev</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    ports</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:#0A3069;--shiki-dark:#A5D6FF\">\"8080:3000\"</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    environment</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      DATABASE_URL</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">postgres://notes:notes@db:5432/notes</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    depends_on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      db</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        condition</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">service_healthy</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        restart</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      migrate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        condition</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">service_completed_successfully</span></span></code></pre></div>\n<p>Three conditions exist: <code dir=\"ltr\">service_started</code> (the short form’s behaviour), <code dir=\"ltr\">service_healthy</code>, and <code dir=\"ltr\">service_completed_successfully</code>, which waits for a one-off container to exit with code 0. Here the migration runs once Postgres is healthy, and the API starts only after the migration succeeded. If the migration fails, <code dir=\"ltr\">api</code> never starts, and you see why in <code dir=\"ltr\">docker compose logs migrate</code>. <code dir=\"ltr\">restart: true</code> means that if Compose restarts <code dir=\"ltr\">db</code> (for example after you change its config), it restarts <code dir=\"ltr\">api</code> too.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 220\" role=\"img\" aria-labelledby=\"l-dk-3-3-t1\"><defs><marker id=\"arrow-l-dk-3-3-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-dk-3-3-t1\">Timeline: db starts and becomes healthy after its checks pass; then migrate runs and exits with code 0; then api starts.</title>\n  <text class=\"d-label-strong\" x=\"20\" y=\"54\">db</text>\n  <text class=\"d-label-strong\" x=\"20\" y=\"114\">migrate</text>\n  <text class=\"d-label-strong\" x=\"20\" y=\"174\">api</text>\n  <rect class=\"d-box-warn\" x=\"110\" y=\"34\" width=\"170\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"195\" y=\"55\" text-anchor=\"middle\">starting</text>\n  <rect class=\"d-box-success\" x=\"280\" y=\"34\" width=\"400\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"480\" y=\"55\" text-anchor=\"middle\">healthy</text>\n  <rect class=\"d-box-accent\" x=\"290\" y=\"94\" width=\"150\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"365\" y=\"115\" text-anchor=\"middle\">running</text>\n  <text class=\"d-code\" x=\"450\" y=\"115\">exit 0</text>\n  <rect class=\"d-box-primary\" x=\"510\" y=\"154\" width=\"170\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"595\" y=\"175\" text-anchor=\"middle\">serving</text>\n  <path class=\"d-arrow\" d=\"M282 68 L292 92\" marker-end=\"url(#arrow-l-dk-3-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M440 128 L508 160\" marker-end=\"url(#arrow-l-dk-3-3-1)\"/>\n  <path class=\"d-line d-dashed\" d=\"M110 200 L680 200\"/>\n  <text class=\"d-label-muted\" x=\"680\" y=\"216\" text-anchor=\"end\">time</text>\n</svg><figcaption>Startup order with conditions</figcaption></figure>\n<p>The API deserves a healthcheck too. The runtime image may be distroless with no <code dir=\"ltr\">curl</code>, but it always has Node, and Node 24 has <code dir=\"ltr\">fetch</code> built in. On <code dir=\"ltr\">node:24-slim</code>, <code dir=\"ltr\">node</code> is on the <code dir=\"ltr\">PATH</code>; on distroless, call it by its full path, <code dir=\"ltr\">/nodejs/bin/node</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">YAML</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:#116329;--shiki-dark:#7EE787\">    healthcheck</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"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\">\"-e\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"fetch('http://localhost:3000/healthz').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      interval</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">10s</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      timeout</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">3s</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      retries</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span></span></code></pre></div>\n<p>In CI, <code dir=\"ltr\">docker compose up -d --wait</code> returns only when every service with a healthcheck is healthy, and fails if one turns unhealthy. That’s the right way to start a stack before integration tests, instead of a <code dir=\"ltr\">sleep 15</code>.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Retries still belong in the app</h2>\n<p>All of this covers the first start. Nothing stops Postgres from restarting at 3 p.m. on a Tuesday, and when you move to Kubernetes in section 4, there’s no <code dir=\"ltr\">depends_on</code> at all: Pods start in whatever order they’re scheduled. So notes-api connects with a retry and backoff, and reports “not ready” on <code dir=\"ltr\">/readyz</code> until the database answers. Healthchecks and conditions make the happy path fast; retries make the unhappy path survivable.</p>\n<aside class=\"callout callout-why\" 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\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">Why this matters</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Readiness is a theme</span></p>\n<p>“Is it started?” versus “can it do its job?” is the same question you’ll answer again with Kubernetes readiness probes in section 5. A Pod that’s running but can’t reach its database should receive no traffic. Getting the idea right here makes the Kubernetes version feel obvious.</p>\n</aside>\n<p>The stack now comes up in the right order every time. The last Compose lesson tidies up configuration and gives you a fast edit-and-reload loop.</p>\n","toc":[{"id":"sec-1","text":"What depends_on really waits for","level":2},{"id":"sec-2","text":"Healthchecks","level":2},{"id":"sec-3","text":"Waiting on conditions","level":2},{"id":"sec-4","text":"Retries still belong in the app","level":2}],"takeaways":["Short-form <code dir=\"ltr\">depends_on</code> only waits for the dependency’s container to start, not for it to be ready to serve.","A <code dir=\"ltr\">healthcheck</code> tells Docker how to test a container; <code dir=\"ltr\">condition: service_healthy</code> makes a dependent service wait for that test to pass.","<code dir=\"ltr\">condition: service_completed_successfully</code> runs one-off jobs such as migrations before the app starts.","In exec form each argument is a separate list item; use <code dir=\"ltr\">CMD-SHELL</code> when the test is a single shell command string.","Startup ordering covers only the first start, so the app must still retry database connections; Kubernetes has no <code dir=\"ltr\">depends_on</code> at all."],"further":[{"title":"Control startup order in Compose","url":"https://docs.docker.com/compose/how-tos/startup-order/"},{"title":"Compose services reference (healthcheck)","url":"https://docs.docker.com/reference/compose-file/services/#healthcheck"},{"title":"docker compose up","url":"https://docs.docker.com/reference/cli/docker/compose/up/"}],"quiz":[{"q":"<p><code dir=\"ltr\">api</code> has <code dir=\"ltr\">depends_on: [db]</code> in short form. On a cold start the API logs <code dir=\"ltr\">ECONNREFUSED</code> once, then works. Why?</p>","options":[{"text":"The short form is ignored unless the services share a network.","why":"All services in the project share the default network, and the short form isn’t ignored. It does exactly what it promises, which is less than people expect."},{"text":"Compose starts dependent services in random order.","why":"Order is deterministic; <code dir=\"ltr\">db</code> does start first. Starting isn’t the same as being ready."},{"text":"Short form waits for <code dir=\"ltr\">db</code>’s container to start, not for Postgres to accept connections.","why":"Correct. Postgres needs a few seconds to initialise. Add a healthcheck and <code dir=\"ltr\">condition: service_healthy</code>."},{"text":"The database password is wrong on the first attempt.","why":"A wrong password gives an authentication error, not a refused connection, and it wouldn’t fix itself."}],"answer":2},{"q":"<p>Which healthcheck correctly tests Postgres readiness in the <code dir=\"ltr\">db</code> service?</p>","options":[{"text":"<code dir=\"ltr\">test: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U notes -d notes&quot;]</code>","why":"Correct. <code dir=\"ltr\">CMD-SHELL</code> runs the string with the container’s shell, and <code dir=\"ltr\">pg_isready</code> exits 0 once the server accepts connections."},{"text":"<code dir=\"ltr\">test: [&quot;CMD&quot;, &quot;pg_isready -U notes -d notes&quot;]</code>","why":"Exec form treats the whole string as the program name, so Docker looks for a binary literally called <code dir=\"ltr\">pg_isready -U notes -d notes</code> and the check always fails."},{"text":"<code dir=\"ltr\">test: [&quot;CMD&quot;, &quot;curl&quot;, &quot;-f&quot;, &quot;http://localhost:5432&quot;]</code>","why":"Postgres doesn’t speak HTTP, and the Postgres image doesn’t ship <code dir=\"ltr\">curl</code>."},{"text":"<code dir=\"ltr\">test: [&quot;CMD&quot;, &quot;ping&quot;, &quot;-c&quot;, &quot;1&quot;, &quot;db&quot;]</code>","why":"A ping proves the network works, not that Postgres is accepting queries."}],"answer":0},{"q":"<p>A <code dir=\"ltr\">migrate</code> service applies schema changes and exits. How should <code dir=\"ltr\">api</code> depend on it?</p>","options":[{"text":"<code dir=\"ltr\">condition: service_healthy</code>","why":"A job that exits never becomes healthy, so <code dir=\"ltr\">api</code> would wait for a state that never comes."},{"text":"<code dir=\"ltr\">condition: service_completed_successfully</code>","why":"Correct. Compose waits for <code dir=\"ltr\">migrate</code> to exit with code 0, and refuses to start <code dir=\"ltr\">api</code> if it fails."},{"text":"<code dir=\"ltr\">condition: service_started</code>","why":"That only waits for the migration to begin, so the API could start against a half-migrated schema."},{"text":"<code dir=\"ltr\">restart: always</code> on <code dir=\"ltr\">migrate</code>","why":"That restarts the job forever after it finishes; it doesn’t order anything."}],"answer":1},{"q":"<p>Your stack uses healthchecks and <code dir=\"ltr\">service_healthy</code>. Why should notes-api still retry failed database connections?</p>","options":[{"text":"Healthchecks only run during <code dir=\"ltr\">docker compose build</code>.","why":"They run continuously while the container is up, at the configured interval."},{"text":"Retries are required for <code dir=\"ltr\">pg_isready</code> to succeed.","why":"<code dir=\"ltr\">pg_isready</code> runs inside the <code dir=\"ltr\">db</code> container and has nothing to do with the API’s connection logic."},{"text":"Compose disables <code dir=\"ltr\">depends_on</code> after the first minute.","why":"Nothing gets disabled. It’s simply a startup-time feature."},{"text":"The ordering only applies at startup; the database can restart later, and Kubernetes doesn’t order startup at all.","why":"Correct. A resilient app retries with backoff; startup ordering is a convenience on top, not a guarantee."}],"answer":3}],"exercise":{"kind":"spot-bug","language":"yaml","code":"services:\n  api:\n    build: .\n    depends_on:\n      - db\n    environment:\n      DATABASE_URL: postgres://notes:notes@db:5432/notes\n  db:\n    image: postgres:17\n    environment:\n      POSTGRES_USER: notes\n      POSTGRES_PASSWORD: notes\n      POSTGRES_DB: notes\n    healthcheck:\n      test: [\"CMD\", \"pg_isready -U notes -d notes\"]\n      interval: 10s\n      retries: 5","bugLines":[5,15],"title":"Healthy on paper","prompt":"<p>With this <code dir=\"ltr\">compose.yaml</code>, <code dir=\"ltr\">db</code> shows <code dir=\"ltr\">(unhealthy)</code> forever even though <code dir=\"ltr\">psql</code> works fine, and the API still crashes on cold starts with <code dir=\"ltr\">ECONNREFUSED</code>. Select the <strong>two</strong> lines to fix.</p>\n","hints":["In exec form (<code dir=\"ltr\">CMD</code>), how many arguments does Docker see in that list?","Which form of <code dir=\"ltr\">depends_on</code> only waits for the container to start?"],"explanation":"<p>Line 15 uses exec form with one string, so Docker looks for a program literally named <code dir=\"ltr\">pg_isready -U notes -d notes</code> and the check never passes. Use <code dir=\"ltr\">CMD-SHELL</code> with the string, or split the arguments.</p>\n<p>Line 5 uses the short form of <code dir=\"ltr\">depends_on</code>, which only waits for <code dir=\"ltr\">db</code> to start. Wait for the healthcheck instead:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">YAML</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:#116329;--shiki-dark:#7EE787\">  api</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    depends_on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      db</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        condition</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">service_healthy</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  db</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    healthcheck</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"CMD-SHELL\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"pg_isready -U notes -d notes\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span></code></pre></div>\n","checkLabels":{}},"words":747,"readMinutes":4}