<?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: Kunal Das</title>
    <description>The latest articles on DEV Community by Kunal Das (@drdocker).</description>
    <link>https://dev.to/drdocker</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%2F4206100%2F5e592a4d-ec0b-4692-a61f-e9e8e3ccdec1.png</url>
      <title>DEV Community: Kunal Das</title>
      <link>https://dev.to/drdocker</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/drdocker"/>
    <language>en</language>
    <item>
      <title>Your AI Agent Can Run `rm -rf /`. Put It in a Sandbox.</title>
      <dc:creator>Kunal Das</dc:creator>
      <pubDate>Sun, 25 Oct 2026 14:39:49 +0000</pubDate>
      <link>https://dev.to/drdocker/your-ai-agent-can-run-rm-rf-put-it-in-a-sandbox-2hfl</link>
      <guid>https://dev.to/drdocker/your-ai-agent-can-run-rm-rf-put-it-in-a-sandbox-2hfl</guid>
      <description>&lt;h1&gt;
  
  
  Your AI Agent Can Run &lt;code&gt;rm -rf /&lt;/code&gt;. Put It in a Sandbox.
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Docker Sandboxes&lt;/strong&gt; is Docker's tool for running AI coding agents inside an isolated microVM on your own machine, managed by a standalone CLI called &lt;code&gt;sbx&lt;/code&gt;. It exists because of an uncomfortable default. When you start Claude Code inside a sandbox, it runs with &lt;code&gt;claude --dangerously-skip-permissions&lt;/code&gt; as the standard startup command. Not because someone was reckless, but because that is the mode the sandbox is built to make safe (&lt;a href="https://docs.docker.com/ai/sandboxes/agents/claude-code" rel="noopener noreferrer"&gt;Docker docs, Claude Code agent page&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That flag is the entire problem this post is about. Every coding agent has an equivalent: Codex bypasses approvals, while Cursor, Copilot and Gemini use what their vendors call YOLO mode. The &lt;a href="https://docs.docker.com/get-started/tutorials/run-an-agent/" rel="noopener noreferrer"&gt;official tutorial&lt;/a&gt; is blunt about it. The built-in sandbox integrations start agents "in their full-autonomy mode," and you don't need to add those flags yourself.&lt;/p&gt;

&lt;p&gt;Full autonomy is genuinely useful. At the same time, it is exactly the blast radius you would never hand to a shell prompt that answers to a language model. Docker's own launch post for the microVM release puts the trade in one line: "Since they can't harm your computer, let them run free" (&lt;a href="https://www.docker.com/blog/docker-sandboxes-run-claude-code-and-other-coding-agents-unsupervised-but-safely/" rel="noopener noreferrer"&gt;Docker blog, January 2026&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;In this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why a container is not enough anymore&lt;/li&gt;
&lt;li&gt;What actually crosses the trust boundary?&lt;/li&gt;
&lt;li&gt;The 10-minute setup&lt;/li&gt;
&lt;li&gt;How do you control what the agent can reach?&lt;/li&gt;
&lt;li&gt;Which sandbox mode fits your workflow?&lt;/li&gt;
&lt;li&gt;FAQ&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker Sandboxes (&lt;code&gt;sbx&lt;/code&gt;) runs each coding agent in its own microVM with its own kernel, filesystem, and Docker daemon. The agent gets sudo inside the VM and cannot reach your host outside the files you explicitly share (&lt;a href="https://docs.docker.com/ai/sandboxes/security/" rel="noopener noreferrer"&gt;security model&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Your workspace directory is the exception: a direct mount is read-write, so the agent can still delete your working tree. Use &lt;code&gt;--clone&lt;/code&gt; when that matters.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;sbx&lt;/code&gt; CLI and local compute are free, including commercial use, and do not require Docker Desktop (&lt;a href="https://www.docker.com/products/docker-sandboxes/" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Network policy defaults matter more than the VM itself: with the Locked Down preset, even your model provider's API is blocked until you allow it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why a container is not enough anymore
&lt;/h2&gt;

&lt;p&gt;The usual answer to "my agent is dangerous" is "put it in a container." That answer was reasonable when the dangerous thing was a build script. A coding agent, however, is a different animal, for one reason: it holds a conversation with you, decides what to run next, and keeps going while you are getting coffee.&lt;/p&gt;

&lt;p&gt;Two container-era assumptions break down. First, containers share the host kernel. Namespaces and cgroups give you a lot, but a kernel-level isolation boundary is not a hardware-level one. &lt;strong&gt;A microVM&lt;/strong&gt; is a lightweight virtual machine with its own kernel, booted under a hypervisor, which gives the agent a hardware-enforced boundary instead. Docker describes the microVM as the "primary trust boundary" in its &lt;a href="https://docs.docker.com/ai/sandboxes/security/" rel="noopener noreferrer"&gt;security model&lt;/a&gt;. The agent has full control inside the VM, including sudo, and the VM boundary is what stops it from reaching anything else.&lt;/p&gt;

&lt;p&gt;Second, an agent that can build containers is an agent that wants the Docker socket. This is the part people forget. I wrote about this class of problem in my &lt;a href="https://dev.to/drdocker/docker-compose-healthchecks-make-dependson-actually-wait-for-postgres-24bn"&gt;post on Compose healthchecks&lt;/a&gt;: a tool with host Docker access is a tool with root on the host, because &lt;code&gt;docker run -v /:/host&lt;/code&gt; is always sitting there. Sandboxes solve it in an unusual way. Each sandbox runs a private Docker daemon inside the VM, so the agent can build images, pull, and run containers to its heart's content, with zero access to your host daemon. Docker claims this is unique among sandboxing options, and I have not found a counterexample (&lt;a href="https://www.docker.com/blog/docker-sandboxes-run-claude-code-and-other-coding-agents-unsupervised-but-safely/" rel="noopener noreferrer"&gt;Docker blog&lt;/a&gt;, &lt;a href="https://docs.docker.com/ai/sandboxes/architecture/" rel="noopener noreferrer"&gt;architecture docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;One sandbox, meanwhile, is a real Linux environment. The agent installs packages, edits files, and starts services. When a run goes sideways, you delete the whole thing and start a fresh one in seconds. That disposal loop, more than any single feature, is what changes how much autonomy you can comfortably grant.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually crosses the trust boundary?
&lt;/h2&gt;

&lt;p&gt;This is the part worth internalizing, because "it's sandboxed" is doing a lot of vague work in most conversations. The &lt;a href="https://docs.docker.com/ai/sandboxes/security/" rel="noopener noreferrer"&gt;security model&lt;/a&gt; page is unusually specific about what crosses in, what crosses back, and what never crosses at all. I found it the most useful page in the whole docs set, and it is the reason this post exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Into the VM:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your workspace directory&lt;/strong&gt;, when you run &lt;code&gt;sbx run&lt;/code&gt; from a project folder or pass a path. A direct mount is read-write, and the agent's edits land in your working tree in real time. With &lt;code&gt;--clone&lt;/code&gt;, the repo is mounted read-only and the agent works on a private clone inside the sandbox. A mountless sandbox shares no host directory at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credentials, but not really.&lt;/strong&gt; A host-side proxy injects authentication headers into outbound requests. The raw credential values never enter the VM. This is a genuinely clever design: the agent uses your GitHub or Anthropic auth without ever holding the token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outbound TCP to allowed destinations&lt;/strong&gt;, proxied through the host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A shared skills store&lt;/strong&gt;, mounted read-only by default at the agent's skills directory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Back to the host:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workspace file changes&lt;/strong&gt; (instant, on direct mounts).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outbound connections&lt;/strong&gt; you allowed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skill store writes&lt;/strong&gt;, only if you flipped the skills mount to read-write.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Never:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your host filesystem outside the workspace and skills store.&lt;/li&gt;
&lt;li&gt;Your host Docker daemon.&lt;/li&gt;
&lt;li&gt;Your host network interfaces directly. Sandboxes cannot talk to each other over the network. Outbound UDP is blocked unless you turn on an experimental flag, and ICMP is blocked outright.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F258heizyg7pj7emsttvt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F258heizyg7pj7emsttvt.webp" alt="Cutaway diagram of the microVM trust boundary: what crosses in, what crosses back, what never leaves the sandbox" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The one honest caveat, and the reason clone mode exists: &lt;strong&gt;a direct mount is read-write&lt;/strong&gt;. The docs say it plainly. Your project directory is the exception to the sandbox boundary, and the agent can modify or delete its files (&lt;a href="https://docs.docker.com/get-started/tutorials/run-an-agent/" rel="noopener noreferrer"&gt;tutorial&lt;/a&gt;). Andrew Lock's hands-on writeup of running Claude Code this way reached the same conclusion. In his words, the sandbox only has access to your working directory, "so yes, it could delete your git repo," and that is basically the extent of the damage it can do (&lt;a href="https://andrewlock.net/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/" rel="noopener noreferrer"&gt;andrewlock.net&lt;/a&gt;). Therefore, keep your work under version control, or use clone mode, or both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 10-minute setup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;sbx&lt;/strong&gt; is the standalone CLI that creates and manages sandboxes. It does not require Docker Desktop or Docker Engine on the host, and local sandbox compute is free including for commercial work (&lt;a href="https://docs.docker.com/ai/sandboxes/install/" rel="noopener noreferrer"&gt;install docs&lt;/a&gt;, &lt;a href="https://www.docker.com/products/docker-sandboxes/" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Install for your OS. macOS uses Homebrew, Windows uses winget, and Ubuntu installs the &lt;code&gt;docker-sbx&lt;/code&gt; package from Docker's apt repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;brew trust docker/tap &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; brew &lt;span class="nb"&gt;install &lt;/span&gt;docker/tap/sbx        &lt;span class="c"&gt;# macOS&lt;/span&gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;winget &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; Docker.sbx                                 &lt;span class="c"&gt;# Windows&lt;/span&gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.docker.com | &lt;span class="nb"&gt;sudo &lt;/span&gt;&lt;span class="nv"&gt;REPO_ONLY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 sh      &lt;span class="c"&gt;# Ubuntu, repo only&lt;/span&gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;docker-sbx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then sign in once with Docker OAuth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sbx login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, start an agent from a project directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/my-project
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sbx run &lt;span class="nt"&gt;--name&lt;/span&gt; my-sandbox claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first run pulls the agent template (&lt;code&gt;docker/sandbox-templates:claude-code&lt;/code&gt;), which takes a moment. Afterwards, it prompts you to choose a default network policy for all your sandboxes: &lt;strong&gt;Open&lt;/strong&gt; (everything allowed), &lt;strong&gt;Balanced&lt;/strong&gt; (default deny, common dev destinations allowed), or &lt;strong&gt;Locked Down&lt;/strong&gt; (nothing allowed until you say so). The &lt;a href="https://docs.docker.com/ai/sandboxes/get-started/" rel="noopener noreferrer"&gt;get-started guide&lt;/a&gt; recommends Balanced as the starting point.&lt;/p&gt;

&lt;p&gt;Claude Code then starts inside the sandbox. If you have a Claude subscription, type &lt;code&gt;/login&lt;/code&gt; inside the session. The OAuth token stays on your host and is never stored inside the sandbox. If you use an API key instead, &lt;code&gt;sbx secret set anthropic&lt;/code&gt; stores it on the host side where the proxy can use it without exposing it to the agent.&lt;/p&gt;

&lt;p&gt;Give the agent a real task. While it runs, open a second terminal and look around:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sbx &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;span class="go"&gt;SANDBOX      AGENT    STATUS      PORTS   WORKSPACE      CREATED
my-sandbox   claude   ● running           ~/my-project   2m
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That workspace column is the entire filesystem the agent can see of your machine. To drop into a shell inside the VM yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sbx &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; my-sandbox bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You land in the primary workspace, inside a Linux machine the agent has sudo on. Poke around, run &lt;code&gt;ps&lt;/code&gt;, look at what the agent installed. This is the moment the model clicks: it is a computer, not a chroot.&lt;/p&gt;

&lt;p&gt;When you are done, the lifecycle commands do what their names say. &lt;code&gt;sbx stop&lt;/code&gt; pauses the sandbox and preserves everything inside it across restarts. &lt;code&gt;sbx rm&lt;/code&gt; deletes the sandbox and everything in it, while your host working tree stays untouched. Meanwhile, running &lt;code&gt;sbx&lt;/code&gt; with no arguments opens an interactive dashboard with live CPU and memory per sandbox, a network panel, and keyboard shortcuts to attach, shell in, stop, or remove.&lt;/p&gt;

&lt;p&gt;Replacing &lt;code&gt;claude&lt;/code&gt; with &lt;code&gt;codex&lt;/code&gt;, &lt;code&gt;copilot&lt;/code&gt;, &lt;code&gt;cursor&lt;/code&gt;, &lt;code&gt;gemini&lt;/code&gt;, &lt;code&gt;opencode&lt;/code&gt;, &lt;code&gt;kiro&lt;/code&gt;, or &lt;code&gt;droid&lt;/code&gt; works the same way. The full supported list is on the &lt;a href="https://docs.docker.com/ai/sandboxes/agents/" rel="noopener noreferrer"&gt;agents page&lt;/a&gt;, along with a &lt;code&gt;shell&lt;/code&gt; agent-less template for manual setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you control what the agent can reach?
&lt;/h2&gt;

&lt;p&gt;Filesystem isolation is half the story. The other half is network egress, and this is where the preset you picked during first run earns its keep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network policy&lt;/strong&gt; is the allowlist the sandbox applies to outbound connections. Check the rules in effect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sbx policy &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Allow one more host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sbx policy allow network registry.npmjs.org
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three presets differ in what they block by default, and the differences are the entire security posture of a run:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Preset&lt;/th&gt;
&lt;th&gt;Outbound traffic&lt;/th&gt;
&lt;th&gt;Model provider API&lt;/th&gt;
&lt;th&gt;Good for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Open&lt;/td&gt;
&lt;td&gt;Everything allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;Trusted tasks on machines with nothing to protect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Balanced&lt;/td&gt;
&lt;td&gt;Denied by default, common dev sites allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;Daily development; the docs' recommended start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locked Down&lt;/td&gt;
&lt;td&gt;Everything blocked&lt;/td&gt;
&lt;td&gt;Blocked until allowed&lt;/td&gt;
&lt;td&gt;Untrusted or unattended tasks; allow exactly what the job needs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href="https://docs.docker.com/ai/sandboxes/get-started/" rel="noopener noreferrer"&gt;get-started guide&lt;/a&gt; includes a detail I appreciate: with Locked Down, even your model provider's API is blocked until you explicitly allow it. That point is the actual security model in miniature. Locked Down means an agent that goes rogue cannot phone home anywhere, exfiltrate your workspace to a paste site, or quietly download a second stage, unless you allowed that destination. For a task you do not fully trust, the workflow is: locked down, then allow exactly &lt;code&gt;api.anthropic.com&lt;/code&gt; (or your provider's endpoint) plus the package registries the task needs, and nothing else. An allowlist you can read in one command beats a permission dialog you learned to click through.&lt;/p&gt;

&lt;p&gt;Two related controls are worth knowing. First, &lt;strong&gt;secrets, not environment variables&lt;/strong&gt;: &lt;code&gt;-e VAR=value&lt;/code&gt; on &lt;code&gt;sbx run&lt;/code&gt; works, but the docs warn that environment variables are readable by processes inside the sandbox, so for API keys &lt;code&gt;sbx secret set&lt;/code&gt; keeps the value on the host and has the proxy inject it into outbound requests instead. Second, &lt;strong&gt;registry credentials for builds&lt;/strong&gt;: the sandbox's Docker daemon pulls from Docker Hub using your &lt;code&gt;sbx login&lt;/code&gt; session, but for other registries you must store pull credentials first. For example, &lt;code&gt;gh auth token | sbx secret set --registry ghcr.io --password-stdin&lt;/code&gt;. Otherwise, private image pulls fail as anonymous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which sandbox mode fits your workflow?
&lt;/h2&gt;

&lt;p&gt;The default (&lt;code&gt;sbx run claude&lt;/code&gt; from a project directory, read-write mount) is the right choice most days: you watch the agent's edits appear in your working tree as a normal Git diff and commit what you want. The &lt;a href="https://docs.docker.com/ai/sandboxes/usage/" rel="noopener noreferrer"&gt;usage guide&lt;/a&gt; documents the rest of the toolbox:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqya2ek4r9ntxgkrwwclm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqya2ek4r9ntxgkrwwclm.webp" alt="Isometric diagram of the four sandbox working modes: direct mount, clone, mountless, and detached with published ports" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--clone&lt;/code&gt;&lt;/strong&gt; mounts your repo read-only and hands the agent a private clone. This is the mode for running several agents against one repository, and the mode for the days you do not trust the diff. The clone lives inside the sandbox; remove the sandbox and the clone is gone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mountless sandboxes&lt;/strong&gt; (&lt;code&gt;sbx create --name scratch claude&lt;/code&gt;, no path) share no host directory at all. The agent works in &lt;code&gt;/home/agent/workspace&lt;/code&gt; inside the VM, files persist across stops, and &lt;code&gt;sbx cp&lt;/code&gt; moves things in and out. Good for experiments you want fully disposable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--rm&lt;/code&gt;&lt;/strong&gt; makes the whole session a throwaway: the sandbox is deleted the moment the agent exits, and &lt;code&gt;sbx run&lt;/code&gt; returns the agent's exit status. This is the CI shape of the tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detached sandboxes&lt;/strong&gt; (&lt;code&gt;sbx run -d&lt;/code&gt;) keep running after your sessions end, which pairs with published ports when the agent is running a service you want to poke at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sbx move&lt;/code&gt;&lt;/strong&gt; recreates a sandbox's filesystem on Docker-managed cloud infrastructure, or back. This is how a heavy build migrates off your laptop without changing anything about the environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That list is also the honest sizing of the tool. Sandboxes are a laptop-and-workstation product with a cloud escape hatch, not a fleet-management layer. What you get locally is the free tier, and it is the part that matters: a hard boundary between "things the agent can ruin" and "everything else."&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I need Docker Desktop to use sbx?
&lt;/h3&gt;

&lt;p&gt;No. The CLI installs standalone and local sandbox compute is free, including for commercial work. Cloud compute is a separate pay-as-you-go subscription, and organization-wide policy governance is a separate paid plan (&lt;a href="https://www.docker.com/products/docker-sandboxes/" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Can the sandbox agent still destroy my code?
&lt;/h3&gt;

&lt;p&gt;Inside a direct mount, yes: the workspace is shared read-write and the agent can delete files there. That is why the docs tell you to keep your work under version control, and why &lt;code&gt;--clone&lt;/code&gt; exists when you want the repo protected.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is this different from just running the agent in a container?
&lt;/h3&gt;

&lt;p&gt;A container shares your host kernel and, if you give it the Docker socket, effectively owns the host. A sandbox is a microVM with its own kernel and its own Docker daemon, so "build and run containers" and "isolate from the host" stop being mutually exclusive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can the agent reach the internet?
&lt;/h3&gt;

&lt;p&gt;Only through the network policy you allow. Sandboxes cannot reach your host network directly, cannot talk to each other, outbound UDP is blocked unless you enable an experimental feature, and ICMP is blocked.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where do my API keys live?
&lt;/h3&gt;

&lt;p&gt;On the host. A proxy injects credentials into outbound requests, and the raw values never enter the VM. Environment variables, by contrast, are readable inside the sandbox, so use secrets for anything sensitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five commands that matter
&lt;/h2&gt;

&lt;p&gt;Install, log in, run, inspect, delete: &lt;code&gt;brew install docker/tap/sbx&lt;/code&gt; (or your OS equivalent), &lt;code&gt;sbx login&lt;/code&gt;, &lt;code&gt;sbx run claude&lt;/code&gt;, &lt;code&gt;sbx ls&lt;/code&gt;, &lt;code&gt;sbx rm my-sandbox&lt;/code&gt;. Everything else in this post is nuance on top of that loop.&lt;/p&gt;

&lt;p&gt;The trade you are making is explicit now instead of implied. The agent runs at full autonomy, with sudo, on a machine it cannot escape, that you can delete with one command when the run goes wrong. The alternative is a permission prompt you have trained yourself to approve without reading, on a machine that has your SSH keys on it. I know which one I let run overnight.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Verified against docs.docker.com (Sandboxes overview, install, get-started, usage, security model, architecture, agents, Claude Code agent page), the Docker Sandboxes product page, and the Docker launch blog post, all retrieved 2026-10-25. Every factual claim links to its source. I did not run the walkthrough on this machine; instead, each claim traces to the linked documentation, and I flag anything I could not verify.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;Every claim above traces to these primary sources. Where I could not verify something hands-on, the post says so.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/ai/sandboxes/agents/claude-code" rel="noopener noreferrer"&gt;Docker docs, Claude Code agent page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/get-started/tutorials/run-an-agent/" rel="noopener noreferrer"&gt;official tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.docker.com/blog/docker-sandboxes-run-claude-code-and-other-coding-agents-unsupervised-but-safely/" rel="noopener noreferrer"&gt;Docker blog, January 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/ai/sandboxes/security/" rel="noopener noreferrer"&gt;security model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.docker.com/products/docker-sandboxes/" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/ai/sandboxes/architecture/" rel="noopener noreferrer"&gt;architecture docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/kunalworldwide/docker-blog-media/main/sandbox-boundary.webp" rel="noopener noreferrer"&gt;Cutaway diagram of the microVM trust boundary: what crosses in, what crosses back, what ne&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://andrewlock.net/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/" rel="noopener noreferrer"&gt;andrewlock.net&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/ai/sandboxes/install/" rel="noopener noreferrer"&gt;install docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/ai/sandboxes/get-started/" rel="noopener noreferrer"&gt;get-started guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/ai/sandboxes/agents/" rel="noopener noreferrer"&gt;agents page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/ai/sandboxes/usage/" rel="noopener noreferrer"&gt;usage guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/kunalworldwide/docker-blog-media/main/sandbox-modes.webp" rel="noopener noreferrer"&gt;Isometric diagram of the four sandbox working modes: direct mount, clone, mountless, and d&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kunalworldwide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;Kunal Das writes operations engineering notes as he learns them, always against primary sources. Contact: &lt;a href="https://github.com/kunalworldwide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and &lt;a href="https://dev.to/drdocker"&gt;Dev.to&lt;/a&gt;. Follow the &lt;a href="https://dev.to/t/docker"&gt;Docker tag&lt;/a&gt; for the next posts in this series.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>ai</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Docker Compose healthchecks: make depends_on actually wait for Postgres</title>
      <dc:creator>Kunal Das</dc:creator>
      <pubDate>Sun, 25 Oct 2026 12:59:08 +0000</pubDate>
      <link>https://dev.to/drdocker/docker-compose-healthchecks-make-dependson-actually-wait-for-postgres-24bn</link>
      <guid>https://dev.to/drdocker/docker-compose-healthchecks-make-dependson-actually-wait-for-postgres-24bn</guid>
      <description>&lt;h1&gt;
  
  
  Docker Compose healthchecks: make depends_on actually wait for Postgres
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Docker Compose&lt;/strong&gt; is a tool for defining and running multi-container applications with one YAML file. Its &lt;code&gt;depends_on&lt;/code&gt; field is the most misunderstood keyword in that file. If your Compose stack has a bare &lt;code&gt;depends_on:&lt;/code&gt; list in front of Postgres, your app has never actually waited for the database.&lt;/p&gt;

&lt;p&gt;Here is the fix, in three parts: the &lt;code&gt;healthcheck&lt;/code&gt; plus &lt;code&gt;condition: service_healthy&lt;/code&gt; setup, the socket trap that makes naive Postgres probes lie, and the two failure modes the gate hides.&lt;/p&gt;

&lt;p&gt;In this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What does &lt;code&gt;depends_on&lt;/code&gt; actually guarantee?&lt;/li&gt;
&lt;li&gt;How do you wire the gate?&lt;/li&gt;
&lt;li&gt;Where does the socket trap bite?&lt;/li&gt;
&lt;li&gt;What are the two failure modes the gate hides?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You bring a stack up cold. The database container starts, the app container starts a beat later, and the app tries to open a connection before Postgres is finished initializing. The app crashes. This is a known failure class in microservice deployments: when a dependent service is unready, calls to it fail and the app misbehaves until the dependency becomes available (&lt;a href="https://doi.org/10.1109/issrew.2019.00068" rel="noopener noreferrer"&gt;ISSREW 2019, "Avoiding Faults due to Dangling Dependencies by Synchronization in Microservices Applications"&lt;/a&gt;). If you set &lt;code&gt;restart: unless-stopped&lt;/code&gt;, Compose quietly restarts it a few seconds later, by which point the database happens to be ready, and the stack looks fine. It is not fine. The restart policy papered over a race instead of fixing it, and the same stack can crash-loop the morning the host reboots.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The short &lt;code&gt;depends_on&lt;/code&gt; list only orders container start and stop. Compose explicitly "does not wait until a container is 'ready', only until it's running" (&lt;a href="https://docs.docker.com/compose/how-tos/startup-order/" rel="noopener noreferrer"&gt;Docker docs&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Waiting for readiness means a &lt;code&gt;healthcheck&lt;/code&gt; on the dependency plus &lt;code&gt;condition: service_healthy&lt;/code&gt; in the long &lt;code&gt;depends_on&lt;/code&gt; form.&lt;/li&gt;
&lt;li&gt;On the official Postgres image, run &lt;code&gt;pg_isready&lt;/code&gt; against TCP (&lt;code&gt;-h 127.0.0.1&lt;/code&gt; or the service name), not the default Unix socket, or the probe can pass while the real server is still down (&lt;a href="https://github.com/docker-library/postgres/issues/146" rel="noopener noreferrer"&gt;docker-library/postgres issue 146&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;The gate is a startup gate, not a monitor. If Postgres dies or restarts later, your app still needs its own retry logic.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What does depends_on actually guarantee?
&lt;/h2&gt;

&lt;p&gt;The short syntax, the one most quick-start guides show, does exactly two things: Compose creates and starts the dependency container before the dependent one, and removes it in reverse order on shutdown. That is the entire contract. The &lt;a href="https://docs.docker.com/compose/how-tos/startup-order/" rel="noopener noreferrer"&gt;startup order page&lt;/a&gt; in the Compose documentation states it plainly: "does not wait until a container is 'ready', only until it's running" (Docker Compose startup-order docs, 2026). For a database, running and ready are different states separated by seconds of work.&lt;/p&gt;

&lt;p&gt;The long form adds a &lt;code&gt;condition&lt;/code&gt; per dependency, and there are three to choose from in the &lt;a href="https://docs.docker.com/reference/compose-file/services/#depends_on" rel="noopener noreferrer"&gt;compose-file services reference&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;service_started&lt;/code&gt;: identical behavior to the short syntax.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;service_healthy&lt;/code&gt;: the dependency's healthcheck must report healthy before the dependent service starts. This is the readiness gate.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;service_completed_successfully&lt;/code&gt;: the dependency must run to completion with exit code 0, which is what you want in front of a one-shot migration job, not in front of a database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is a fourth long-form field people miss: &lt;code&gt;restart: true&lt;/code&gt;, which makes Compose restart the dependent service when an explicit Compose operation restarts the dependency, so the app re-establishes its connections instead of pointing at a stale database session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does Postgres race ahead of its own readiness?
&lt;/h2&gt;

&lt;p&gt;The official Postgres image does real work between "container running" and "accepting your queries." On a first boot with an empty data directory, the &lt;a href="https://github.com/docker-library/postgres/blob/master/docker-entrypoint.sh" rel="noopener noreferrer"&gt;entrypoint script&lt;/a&gt; runs &lt;code&gt;initdb&lt;/code&gt; to create the cluster. It then starts a temporary Postgres server to set up roles and run anything you mounted into &lt;code&gt;/docker-entrypoint-initdb.d/&lt;/code&gt;. Only after that does it stop the temporary server and exec the real one as PID 1.&lt;/p&gt;

&lt;p&gt;The detail that matters for healthchecks: that temporary server is started with &lt;code&gt;listen_addresses=''&lt;/code&gt;, so it listens on the Unix socket only and not on TCP. The image is designed this way on purpose. Back in 2016, &lt;a href="https://github.com/docker-library/postgres/issues/146" rel="noopener noreferrer"&gt;yosifkit put it&lt;/a&gt; as: "when it is listening on the network port via its non-loopback interface, it is ready to accept connections." During initialization the container is running, Postgres is running, and nothing on the network will answer. From the outside, the container looks started the whole time, which is exactly the state the short &lt;code&gt;depends_on&lt;/code&gt; waits for and then stops waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually ran
&lt;/h2&gt;

&lt;p&gt;To check that the fix works the way the docs describe, I tested this with a real cold start on my own host: &lt;strong&gt;Docker Compose v5.1.4&lt;/strong&gt;, a &lt;code&gt;postgres:16-alpine&lt;/code&gt; database with a TCP healthcheck, and an &lt;code&gt;alpine&lt;/code&gt; app that prints one line after starting. The app declares &lt;code&gt;depends_on: db: condition: service_healthy&lt;/code&gt;. From my testing, the sequence below is what actually happened, in this order.&lt;/p&gt;

&lt;p&gt;For example, here is the exact output I watched during the cold start.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fatms0hziqzj90r9tm4yr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fatms0hziqzj90r9tm4yr.webp" alt="Green container passing through an open gate at a busy container port at dusk"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Container compose-test-db-1 Starting
 Container compose-test-db-1 Started
 Container compose-test-db-1 Waiting
 Container compose-test-db-1 Healthy
 Container compose-test-app-1 Starting
 Container compose-test-app-1 Started
app-1  | APP-STARTED-WITH-db-healthy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the order: the app container is not even created until &lt;code&gt;db&lt;/code&gt; reports Healthy. The diagram below is the same flow the output shows, with the gate drawn in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fouf3ylhnfekwaa94qa3n.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fouf3ylhnfekwaa94qa3n.webp" alt="Anatomy of the readiness gate: the app waits while pg_isready probes Postgres; the socket path is a dead end, the TCP path db:5432 passes"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before adding the gate, the same file started both containers at once, and the app's very first connection attempt hit &lt;code&gt;connection refused&lt;/code&gt;. In my experience, that is the failure most people blame on "the database being slow" before they find out the ordering was never there. The gate removes this specific race; it cannot remove every failure mode, which is the subject of the last sections.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you wire the gate?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;healthcheck&lt;/strong&gt; is a container configuration that makes the runtime probe your service at a fixed interval and report its state as &lt;code&gt;starting&lt;/code&gt;, &lt;code&gt;healthy&lt;/code&gt;, or &lt;code&gt;unhealthy&lt;/code&gt;. &lt;strong&gt;service_healthy&lt;/strong&gt; is the &lt;code&gt;depends_on&lt;/code&gt; condition that waits for that state. Here is a runnable Compose file with both:&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;services&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="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
        &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://app:apppassword@db:5432/appdb&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&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;postgres:18&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apppassword&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appdb&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-h&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$${POSTGRES_USER}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$${POSTGRES_DB}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this file, &lt;code&gt;docker compose up&lt;/code&gt; starts &lt;code&gt;db&lt;/code&gt;, probes it every 5 seconds, and does not create the &lt;code&gt;app&lt;/code&gt; container until the healthcheck passes. The &lt;code&gt;$$&lt;/code&gt; doubling is Compose escaping: a single &lt;code&gt;$&lt;/code&gt; in a Compose file is interpolated by Compose itself, so &lt;code&gt;$$&lt;/code&gt; is what reaches the shell inside the container as a literal &lt;code&gt;$&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pg_isready&lt;/strong&gt; is the Postgres client's readiness probe; it exits 0 when the server accepts connections. The five timing knobs below use the defaults from the &lt;a href="https://docs.docker.com/reference/dockerfile/#healthcheck" rel="noopener noreferrer"&gt;Dockerfile HEALTHCHECK reference&lt;/a&gt;. The Compose &lt;code&gt;healthcheck&lt;/code&gt; block &lt;a href="https://docs.docker.com/reference/compose-file/services/#healthcheck" rel="noopener noreferrer"&gt;works the same way&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;interval&lt;/code&gt; (default 30s): how often the probe runs once the container is up.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;timeout&lt;/code&gt; (default 30s): how long one probe may run before it counts as a failure.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retries&lt;/code&gt; (default 3): consecutive failures needed to mark the container unhealthy.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;start_period&lt;/code&gt; (default 0s): grace window where failures don't count toward &lt;code&gt;retries&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;start_interval&lt;/code&gt; (default 5s): probe cadence during &lt;code&gt;start_period&lt;/code&gt; only, needs Docker Engine 25.0 or later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a local dev stack, &lt;code&gt;interval: 5s&lt;/code&gt; with &lt;code&gt;start_period: 10s&lt;/code&gt; is plenty. In CI on a cold runner, or with a big &lt;code&gt;initdb.d&lt;/code&gt; directory, give yourself more headroom: &lt;code&gt;start_period: 30s&lt;/code&gt; and &lt;code&gt;retries: 10&lt;/code&gt; is a common starting point. Without a &lt;code&gt;start_period&lt;/code&gt;, a database that takes 15 seconds to initialize can burn through its failure budget before it ever had a chance — for example, my own cold start took roughly eight seconds across initdb and the temporary server, inside the 10s window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where does the socket trap bite?
&lt;/h2&gt;

&lt;p&gt;Copy-paste healthchecks for Postgres usually look like &lt;code&gt;pg_isready -U postgres&lt;/code&gt;. That version can report healthy while the real server is still down, because &lt;code&gt;pg_isready&lt;/code&gt; inside the container connects to the Unix socket by default, and the temporary initialization server answers on the Unix socket.&lt;/p&gt;

&lt;p&gt;The maintainers' guidance is to force TCP. &lt;a href="https://github.com/docker-library/postgres/issues/146" rel="noopener noreferrer"&gt;tianon's comment&lt;/a&gt; on the long-running readiness issue: "Simply adding &lt;code&gt;-h 127.0.0.1&lt;/code&gt; to the &lt;code&gt;pg_isready&lt;/code&gt; command line should be enough to detect readiness correctly." (docker-library/postgres issue #146.) The &lt;a href="https://github.com/docker-library/healthcheck/blob/master/postgres/docker-healthcheck" rel="noopener noreferrer"&gt;docker-library healthcheck script&lt;/a&gt; does the same thing in a heavier form: it resolves the container's own hostname and connects with &lt;code&gt;psql&lt;/code&gt;, explicitly "to not use the local unix socket."&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Probe shape&lt;/th&gt;
&lt;th&gt;What it detects&lt;/th&gt;
&lt;th&gt;When it lies&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pg_isready -U postgres&lt;/code&gt; (socket)&lt;/td&gt;
&lt;td&gt;The Unix socket answers&lt;/td&gt;
&lt;td&gt;During init: the temp server answers while the real one is down&lt;/td&gt;
&lt;td&gt;Nothing in a multi-container app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pg_isready -h 127.0.0.1 -U ...&lt;/code&gt; (TCP, loopback)&lt;/td&gt;
&lt;td&gt;TCP port 5432 accepts connections&lt;/td&gt;
&lt;td&gt;Never on readiness; still blind to init scripts&lt;/td&gt;
&lt;td&gt;The default choice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pg_isready -h db -U ...&lt;/code&gt; (TCP, service name)&lt;/td&gt;
&lt;td&gt;The exact address other containers use&lt;/td&gt;
&lt;td&gt;Never on readiness; still blind to init scripts&lt;/td&gt;
&lt;td&gt;When you want the probe to mirror app traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;psql -c "SELECT 1"&lt;/code&gt; (real login)&lt;/td&gt;
&lt;td&gt;A role and database actually answer&lt;/td&gt;
&lt;td&gt;Can fail for reasons unrelated to readiness (connection limits, &lt;code&gt;pg_hba.conf&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;When you need objects from &lt;code&gt;initdb.d&lt;/code&gt; to exist first&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One nuance from the same issue threads: &lt;code&gt;pg_isready&lt;/code&gt; confirms the server accepts connections. It does not confirm that a role or database created by an init script exists yet. If your app needs objects created in &lt;code&gt;/docker-entrypoint-initdb.d/&lt;/code&gt;, a TCP probe says "Postgres is up" while those scripts may still be running.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3efamu9r655ncesdatmc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3efamu9r655ncesdatmc.webp" alt="Two probe paths out of the Postgres container: the socket probe answers during init, the TCP probe mirrors app traffic"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the two failure modes the gate hides?
&lt;/h2&gt;

&lt;p&gt;Adding a readiness gate converts one vague failure (intermittent connection refused) into two specific ones. Knowing both is the difference between a five minute fix and an afternoon of log spelunking.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F52wxtno3aqj6b610hu2k.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F52wxtno3aqj6b610hu2k.webp" alt="The two failure modes: an unhealthy gate blocks the app loudly (left), a passed gate still needs app-level retries for later blips (right)"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure mode one: "dependency failed to start: container db is unhealthy"
&lt;/h3&gt;

&lt;p&gt;Your healthcheck runs, exhausts its retries, and Compose gives up on the whole chain instead of starting the app against a database it knows is not ready. It looks like the gate broke your stack. It didn't. This is the gate doing its job, loudly, at a point where you would otherwise be reading crash-loop logs from the app.&lt;/p&gt;

&lt;p&gt;Debug it in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;docker inspect --format '{{json .State.Health}}' &amp;lt;container&amp;gt;&lt;/code&gt; — the Engine stores the last few probe results, stdout and stderr included, so you can see the actual command output that failed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker compose logs db&lt;/code&gt; — the usual suspects: the probe binary does not exist in the image (curl on an image that ships wget), a typo'd user or database name, or timing values too tight for a cold start.&lt;/li&gt;
&lt;li&gt;If the error instead says the dependency "didn't complete successfully" or the container never leaves &lt;code&gt;starting&lt;/code&gt;, check that the dependency actually defines a healthcheck. &lt;code&gt;condition: service_healthy&lt;/code&gt; with no healthcheck defined means the condition can never be satisfied and the dependent service is never created.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Failure mode two: the gate passes, the app still crashes
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;service_healthy&lt;/code&gt; is evaluated once, at startup. It is not a runtime monitor. If Postgres restarts two hours later, or the bridge network hiccups under load, Compose does not restart your app to re-run the gate. The app still needs to handle a connection that drops mid-session.&lt;/p&gt;

&lt;p&gt;This one-shot behavior is not unique to Compose. Orchestration research on Kubernetes health monitoring shows that how and how often you probe matters: a 2025 study of poll-based container monitoring found it "requires careful tuning," can erroneously detect failures, and that erroneous detections alone reduced service availability by 4% compared with signal-based monitoring (&lt;a href="https://arxiv.org/abs/2507.02158" rel="noopener noreferrer"&gt;arXiv:2507.02158&lt;/a&gt;). The practical reading for a Compose stack: treat the startup gate as your first line, and build the retry into the application, because nobody re-probes for you later.&lt;/p&gt;

&lt;p&gt;The belt-and-braces pattern is a retry with backoff at connection time, something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;connect_with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_delay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The healthcheck removes the common cold-start race. Only the application can ride out a dependency that blips later, after &lt;code&gt;docker compose up&lt;/code&gt; has long finished and moved on.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you verify the gate works?
&lt;/h2&gt;

&lt;p&gt;Watch the health column during a cold start (the exact sequence is above):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want to see &lt;code&gt;db&lt;/code&gt; sit in a &lt;code&gt;starting&lt;/code&gt; health state briefly, flip to &lt;code&gt;healthy&lt;/code&gt;, and only then see the &lt;code&gt;app&lt;/code&gt; container appear. If &lt;code&gt;app&lt;/code&gt; starts at the same moment as &lt;code&gt;db&lt;/code&gt;, the condition is not wired: check for a typo in the YAML or a missing healthcheck block on the target service.&lt;/p&gt;

&lt;p&gt;Two commands worth memorizing for the moment something goes sideways:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{json .State.Health}}'&lt;/span&gt; db
docker compose logs db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And for CI pipelines that need a hard wait without writing your own polling loop, &lt;code&gt;docker compose up --wait&lt;/code&gt; blocks until every service with a healthcheck reports healthy or the timeout hits. It is a much cleaner signal than grepping logs for a "ready" string.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why does my app still crash if I copied the healthcheck from a tutorial?&lt;/strong&gt;&lt;br&gt;
The tutorial probably missed &lt;code&gt;condition: service_healthy&lt;/code&gt; in the long &lt;code&gt;depends_on&lt;/code&gt; form. Or its probe talks to the Unix socket instead of TCP. Both are covered above; the table in "Where does the socket trap bite?" is the quick checklist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does &lt;code&gt;depends_on&lt;/code&gt; wait for the database by itself?&lt;/strong&gt;&lt;br&gt;
No. The short list only orders container startup. Readiness requires a healthcheck plus &lt;code&gt;condition: service_healthy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is &lt;code&gt;pg_isready -h 127.0.0.1&lt;/code&gt; enough, or do I need &lt;code&gt;psql&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
For "is Postgres accepting connections", &lt;code&gt;pg_isready&lt;/code&gt; over TCP is enough and is what the image maintainers recommend. Use a &lt;code&gt;psql&lt;/code&gt; login probe only when you also need init scripts to have finished.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will the healthcheck protect the app if Postgres restarts later?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;service_healthy&lt;/code&gt; runs once at startup. The application needs its own connection retry logic for post-startup failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What should I use in CI instead of polling?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;docker compose up --wait&lt;/code&gt; blocks until healthy services report healthy, which replaces hand-rolled polling loops.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the five lines that matter?
&lt;/h2&gt;

&lt;p&gt;Strip the whole article down and this is what ships: a &lt;code&gt;healthcheck&lt;/code&gt; on the database that probes TCP readiness, and a &lt;code&gt;depends_on&lt;/code&gt; with &lt;code&gt;condition: service_healthy&lt;/code&gt; on the app. Everything else is tuning. Start with &lt;code&gt;interval: 5s&lt;/code&gt;, &lt;code&gt;retries: 5&lt;/code&gt;, &lt;code&gt;start_period: 10s&lt;/code&gt; locally, loosen them for CI, and add app-level retries because the gate only protects the startup sequence, not the runtime.&lt;/p&gt;

&lt;p&gt;If your current compose file has a bare &lt;code&gt;depends_on:&lt;/code&gt; list in front of Postgres, that is the line to change today. It was never waiting for the database, and the first cold boot on a slow disk will be the one that reminds you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Verified against Docker Compose v5.1.4 and the &lt;a href="https://github.com/compose-spec/compose-spec" rel="noopener noreferrer"&gt;compose-spec reference&lt;/a&gt; on 2026-10-25, and fact-checked against every linked source (docs.docker.com, docker-library/postgres issue #146, the Dockerfile reference and the compose-file reference). I flag anything I could not reproduce.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;Every claim above traces to these primary sources. Where I could not verify something hands-on, the post says so.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://doi.org/10.1109/issrew.2019.00068" rel="noopener noreferrer"&gt;ISSREW 2019, "Avoiding Faults due to Dangling Dependencies by Synchronization in Microserv&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/compose/how-tos/startup-order/" rel="noopener noreferrer"&gt;Docker docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/docker-library/postgres/issues/146" rel="noopener noreferrer"&gt;docker-library/postgres issue 146&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/reference/compose-file/services/#depends_on" rel="noopener noreferrer"&gt;compose-file services reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/docker-library/postgres/blob/master/docker-entrypoint.sh" rel="noopener noreferrer"&gt;entrypoint script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/kunalworldwide/docker-blog-media/main/hero-v2.webp" rel="noopener noreferrer"&gt;Green container passing through an open gate at a busy container port at dusk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/kunalworldwide/docker-blog-media/main/diagram-gate-anatomy.webp" rel="noopener noreferrer"&gt;Anatomy of the readiness gate: the app waits while pg_isready probes Postgres; the socket &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/reference/dockerfile/#healthcheck" rel="noopener noreferrer"&gt;Dockerfile HEALTHCHECK reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/reference/compose-file/services/#healthcheck" rel="noopener noreferrer"&gt;works the same way&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/docker-library/healthcheck/blob/master/postgres/docker-healthcheck" rel="noopener noreferrer"&gt;docker-library healthcheck script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/kunalworldwide/docker-blog-media/main/diagram-socket-v2.webp" rel="noopener noreferrer"&gt;Two probe paths out of the Postgres container: the socket probe answers during init, the T&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/kunalworldwide/docker-blog-media/main/diagram-failures-v3.webp" rel="noopener noreferrer"&gt;The two failure modes: an unhealthy gate blocks the app loudly (left), a passed gate still&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2507.02158" rel="noopener noreferrer"&gt;arXiv:2507.02158&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/compose-spec/compose-spec" rel="noopener noreferrer"&gt;compose-spec reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kunalworldwide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;Kunal Das writes operations engineering notes as he learns them, always against primary sources. Contact: &lt;a href="https://github.com/kunalworldwide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and &lt;a href="https://dev.to/drdocker"&gt;Dev.to&lt;/a&gt;. Follow the &lt;a href="https://dev.to/t/docker"&gt;Docker tag&lt;/a&gt; for the next posts in this series.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>dockercompose</category>
      <category>postgres</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
