<?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: itops</title>
    <description>The latest articles on DEV Community by itops (@itops).</description>
    <link>https://dev.to/itops</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%2F4170652%2F5338e322-d77e-4832-bae6-2dbc7835d217.png</url>
      <title>DEV Community: itops</title>
      <link>https://dev.to/itops</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/itops"/>
    <language>en</language>
    <item>
      <title>Docker container keeps restarting? Find out why the loop won't stop</title>
      <dc:creator>itops</dc:creator>
      <pubDate>Sun, 18 Oct 2026 02:42:41 +0000</pubDate>
      <link>https://dev.to/itops/docker-container-keeps-restarting-find-out-why-the-loop-wont-stop-20ab</link>
      <guid>https://dev.to/itops/docker-container-keeps-restarting-find-out-why-the-loop-wont-stop-20ab</guid>
      <description>&lt;p&gt;You deployed to a VPS and &lt;code&gt;docker ps&lt;/code&gt; keeps flipping between &lt;code&gt;Restarting (1)&lt;/code&gt; and a few seconds of &lt;code&gt;Up&lt;/code&gt;. The restart policy keeps bringing the process back, but it never fixes whatever kills it. Before you touch config, add memory or blame the database, find out why the process stops. Everything below is read-only; run it only on servers you're authorized to access, and replace &lt;code&gt;YOUR_CONTAINER&lt;/code&gt; with the real name or ID. Docker access is root-equivalent, so don't expose the API or hand it to unknown helpers while debugging.&lt;/p&gt;

&lt;p&gt;If the container exited once and stayed down, or you just want to decode a specific number, start with &lt;a href="https://dev.to/itops/docker-exit-codes-read-the-number-before-you-restart-anything-3lpl"&gt;Docker exit codes: read the number before you restart anything&lt;/a&gt;. This post is about loops.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Check it's really a loop and write down the state
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'table {{.Names}}\t{{.Status}}\t{{.Image}}'&lt;/span&gt;
docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} finished={{.State.FinishedAt}} restarts={{.RestartCount}}'&lt;/span&gt; YOUR_CONTAINER
docker events &lt;span class="nt"&gt;--since&lt;/span&gt; 30m &lt;span class="nt"&gt;--until&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;container&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_CONTAINER &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;die &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure it's the image and service you meant to deploy, and note the incident time and the last deploy. A &lt;code&gt;restarts&lt;/code&gt; count that grows between runs, or &lt;code&gt;die&lt;/code&gt;/&lt;code&gt;start&lt;/code&gt; pairs repeating in the events, means a loop. &lt;code&gt;FinishedAt&lt;/code&gt; is UTC.&lt;/p&gt;

&lt;p&gt;The number in &lt;code&gt;Restarting (N)&lt;/code&gt; is the last exit code. It points at the next check rather than giving the answer:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Usually means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Command finished; a service may have daemonized into the background&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Application error, read the logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;125 / 126 / 127&lt;/td&gt;
&lt;td&gt;Docker couldn't run it / not executable / not found&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;137&lt;/td&gt;
&lt;td&gt;SIGKILL, not proof of OOM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;139 / 143&lt;/td&gt;
&lt;td&gt;Segfault / stopped with SIGTERM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  2. Restart policy and Docker's backoff
&lt;/h2&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;'policy={{.HostConfig.RestartPolicy.Name}} max_retries={{.HostConfig.RestartPolicy.MaximumRetryCount}}'&lt;/span&gt; YOUR_CONTAINER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Behaviour&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;no&lt;/code&gt; (default)&lt;/td&gt;
&lt;td&gt;Never restarts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;on-failure[:N]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Restarts only on non-zero exit, optionally at most N times, then stays &lt;code&gt;Exited&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;always&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Restarts whenever it stops; after a manual stop, only when the daemon or container is restarted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unless-stopped&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Like &lt;code&gt;always&lt;/code&gt;, but a container you stopped stays stopped across daemon restarts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Docker backs off between attempts: 100 ms, doubling each time, capped at one minute. If the container stays up for at least 10 seconds, the delay resets. That's why a crashing container spends longer and longer in &lt;code&gt;Restarting&lt;/code&gt; and only flashes &lt;code&gt;Up&lt;/code&gt;. In Compose this is the service's &lt;code&gt;restart:&lt;/code&gt; key.&lt;/p&gt;

&lt;p&gt;Changing the policy doesn't fix a broken startup; it only changes how often you see it fail. Also, don't let both Docker and systemd restart the same container.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Read a bounded window of logs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs &lt;span class="nt"&gt;--since&lt;/span&gt; 15m &lt;span class="nt"&gt;--tail&lt;/span&gt; 200 &lt;span class="nt"&gt;--timestamps&lt;/span&gt; YOUR_CONTAINER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for the first error before each stop, not the last line. In a loop it repeats. Empty output doesn't mean healthy: the app may log to files, or the logging driver may not expose stdout.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connection refused to the database:&lt;/strong&gt; go to step 4. Inside a container, &lt;code&gt;localhost&lt;/code&gt; is not the other service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission denied / missing file:&lt;/strong&gt; check mount paths, ownership, the runtime user and startup args. Don't &lt;code&gt;chmod 777&lt;/code&gt; your way out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config or migration failure:&lt;/strong&gt; diff recent changes and work out rollback and data compatibility before editing production config.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Dependencies that aren't ready yet
&lt;/h2&gt;

&lt;p&gt;Classic loop: the app starts before its database, cache or API is ready, fails its first connection, exits, and gets restarted until the dependency happens to be up.&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 ps
docker compose logs &lt;span class="nt"&gt;--since&lt;/span&gt; 15m &lt;span class="nt"&gt;--timestamps&lt;/span&gt; YOUR_DEPENDENCY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Plain &lt;code&gt;depends_on&lt;/code&gt; only waits for the dependency's container to &lt;strong&gt;start&lt;/strong&gt;, not to be ready. &lt;code&gt;condition: service_healthy&lt;/code&gt; waits for its healthcheck, so it only helps if that service has one.&lt;/li&gt;
&lt;li&gt;Check that the app's address matches the dependency's service name and port on the same network.&lt;/li&gt;
&lt;li&gt;If the app gives up after one failed connection, a short retry in the app usually beats relying on restarts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Healthchecks: what they do and don't restart
&lt;/h2&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; YOUR_CONTAINER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On plain Docker Engine, &lt;code&gt;unhealthy&lt;/code&gt; is only a label. Docker does not restart a container because its healthcheck fails. If unhealthy containers keep getting restarted, something else is doing it (Swarm, an orchestrator, an autoheal-style helper), and that tool's logs are where to look.&lt;/p&gt;

&lt;p&gt;A check that's too strict, or has no start period for a slow app, can keep dependents waiting on &lt;code&gt;service_healthy&lt;/code&gt; or keep triggering that external tool. Read the check's own output before blaming the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Memory, disk and ports
&lt;/h2&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;'memory_limit_bytes={{.HostConfig.Memory}}'&lt;/span&gt; YOUR_CONTAINER
docker stats &lt;span class="nt"&gt;--no-stream&lt;/span&gt; YOUR_CONTAINER
&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memory:&lt;/strong&gt; &lt;code&gt;OOMKilled=true&lt;/code&gt; is evidence Docker recorded an OOM kill; a 137 alone isn't. &lt;code&gt;docker stats&lt;/code&gt; is a snapshot and won't show the earlier peak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disk:&lt;/strong&gt; a full filesystem breaks databases and apps at startup. Find what filled it before deleting anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ports:&lt;/strong&gt; "port is already allocated" means something else holds it; &lt;code&gt;sudo ss -ltnp&lt;/code&gt; shows what.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Turn evidence into a next step
&lt;/h2&gt;

&lt;p&gt;Write down the symptom, the evidence, the likely cause and the smallest next check. For production, define impact, a maintenance window and rollback first. Deleting data or containers and widening permissions are not universal fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Illustrative example
&lt;/h2&gt;

&lt;p&gt;Illustrative, not a real incident: after a host reboot the app shows &lt;code&gt;Restarting (1)&lt;/code&gt; and its restart count keeps climbing while the database is &lt;code&gt;Up&lt;/code&gt;. The app's first log line after every start is a refused connection to the database, and the database log shows it was still initializing at those times. Compose uses &lt;code&gt;depends_on&lt;/code&gt; without a health condition. That supports "dependency not ready". The fixes, a database healthcheck plus &lt;code&gt;condition: service_healthy&lt;/code&gt;, or a connection retry in the app, are changes to plan, followed by checking that the restart count stops growing.&lt;/p&gt;

&lt;p&gt;A restart policy hides the failure; the exit code, events and the first log error tell you what it is.&lt;/p&gt;




&lt;p&gt;I work on OpsMate, which puts an SSH terminal and an AI assistant on one page, so you can run the commands above yourself or ask the AI to compare the logs, restart count, policy, health and dependency status for you. Low-risk actions such as service restarts and log rotation run automatically by default, dangerous commands are blocked, and every step is logged. Each time you ask the AI for help, recent terminal output is sent to the cloud AI along with your message, so remove sensitive information first. You get 500 free AI requests per month. Read the full guide and &lt;a href="https://www.itops.sh/en/guides/docker-container-keeps-restarting/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=sprint_w1&amp;amp;utm_content=docker-container-keeps-restarting" rel="noopener noreferrer"&gt;start free on itops.sh&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Drafted with AI assistance and reviewed before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>linux</category>
      <category>containers</category>
    </item>
    <item>
      <title>Exit code 137: was it really out of memory? Find who sent the SIGKILL</title>
      <dc:creator>itops</dc:creator>
      <pubDate>Sun, 11 Oct 2026 18:40:39 +0000</pubDate>
      <link>https://dev.to/itops/exit-code-137-was-it-really-out-of-memory-find-who-sent-the-sigkill-d59</link>
      <guid>https://dev.to/itops/exit-code-137-was-it-really-out-of-memory-find-who-sent-the-sigkill-d59</guid>
      <description>&lt;p&gt;&lt;code&gt;docker ps -a&lt;/code&gt; says &lt;code&gt;Exited (137)&lt;/code&gt; or &lt;code&gt;Restarting (137)&lt;/code&gt;, the app's logs stop mid-line with no error, and the box is a small 1–2 GB VPS running several services. It looks like "out of memory", and often it is. But 137 only tells you the main process received SIGKILL. The real question is who sent it. Everything below is read-only; run it only on servers you're authorized to access, and replace &lt;code&gt;YOUR_CONTAINER&lt;/code&gt; with the real name or ID. Kernel log checks need &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 137, and why the logs are silent
&lt;/h2&gt;

&lt;p&gt;137 is 128 + 9: the process was ended by signal 9. SIGKILL can't be caught or delayed, so the app never gets to log a goodbye. A log that just stops fits a 137. It doesn't clear the app.&lt;/p&gt;

&lt;p&gt;Several senders produce the same number:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Who sent SIGKILL&lt;/th&gt;
&lt;th&gt;Where the evidence usually is&lt;/th&gt;
&lt;th&gt;&lt;code&gt;OOMKilled&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kernel OOM killer, container's own memory limit reached&lt;/td&gt;
&lt;td&gt;Kernel: &lt;code&gt;Memory cgroup out of memory: Killed process …&lt;/code&gt;; an &lt;code&gt;oom&lt;/code&gt; event in &lt;code&gt;docker events&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Usually &lt;code&gt;true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel OOM killer, whole host out of memory&lt;/td&gt;
&lt;td&gt;Kernel: &lt;code&gt;Out of memory: Killed process …&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;, depending on cgroup version and Docker/containerd versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Userspace OOM daemon (systemd-oomd, earlyoom)&lt;/td&gt;
&lt;td&gt;That daemon's journal; nothing from the kernel&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;docker kill&lt;/code&gt; / &lt;code&gt;docker rm -f&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kill&lt;/code&gt; event with &lt;code&gt;signal=9&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;docker stop&lt;/code&gt;, &lt;code&gt;compose down&lt;/code&gt; or a redeploy, app ignored SIGTERM for the whole grace period (10 s default)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kill&lt;/code&gt; &lt;code&gt;signal=15&lt;/code&gt;, then &lt;code&gt;signal=9&lt;/code&gt; about 10 s later&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A script, agent or person running &lt;code&gt;kill -9&lt;/code&gt; on the host&lt;/td&gt;
&lt;td&gt;Only a &lt;code&gt;die&lt;/code&gt; event; maybe that tool's own logs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Memory is the first suspect, not the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Capture the exit state before a restart wipes it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'table {{.Names}}\t{{.Status}}\t{{.Image}}'&lt;/span&gt;
docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} started={{.State.StartedAt}} finished={{.State.FinishedAt}} restarts={{.RestartCount}} policy={{.HostConfig.RestartPolicy.Name}}'&lt;/span&gt; YOUR_CONTAINER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A restart resets the evidence.&lt;/strong&gt; When the container starts again, Docker sets &lt;code&gt;ExitCode&lt;/code&gt; back to 0 and &lt;code&gt;OOMKilled&lt;/code&gt; back to &lt;code&gt;false&lt;/code&gt;. With a restart policy you'll often see &lt;code&gt;status=running exit=0 oom=false restarts=5&lt;/code&gt;, which looks clean but isn't. The previous exit then lives only in &lt;code&gt;docker events&lt;/code&gt; (step 2) and the kernel log (step 4).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exited / 137 / oom=true&lt;/code&gt;: the container's own limit is the lead hypothesis. Still confirm with steps 3 and 4.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exited / 137 / oom=false&lt;/code&gt;: memory isn't ruled out, but check Docker's events first.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;oom=true&lt;/code&gt; on a running container, or with another exit code: in recent Docker versions the flag is set when the kernel kills &lt;em&gt;any&lt;/em&gt; process in the container. A killed worker can leave the container up, or the main process may exit later with its own code.&lt;/li&gt;
&lt;li&gt;Memory trouble doesn't always end in 137. If a runtime hits its own heap limit first, it logs an error and exits another way: Node.js prints &lt;code&gt;JavaScript heap out of memory&lt;/code&gt; and often exits 134; a JVM logs &lt;code&gt;java.lang.OutOfMemoryError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;finished&lt;/code&gt; is UTC (ends in &lt;code&gt;Z&lt;/code&gt;). Convert before comparing with local host logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Ask Docker whether it sent the signal
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker events &lt;span class="nt"&gt;--since&lt;/span&gt; 24h &lt;span class="nt"&gt;--until&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;container&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_CONTAINER &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;oom &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;die &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;start
docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'stop_signal={{.Config.StopSignal}} stop_timeout={{.Config.StopTimeout}}'&lt;/span&gt; YOUR_CONTAINER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repeated &lt;code&gt;event=&lt;/code&gt; filters are OR'd. Patterns to look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;oom&lt;/code&gt; → &lt;code&gt;die exitCode=137&lt;/code&gt;: kernel killed a process under the container's limit, main process died.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;oom&lt;/code&gt; with no &lt;code&gt;die&lt;/code&gt;: something inside was killed, main process survived. Look for a dead worker in the app logs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kill signal=15&lt;/code&gt; → ~10 s → &lt;code&gt;kill signal=9&lt;/code&gt; → &lt;code&gt;die exitCode=137&lt;/code&gt;: a stop that ran out of grace period. Not memory.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kill signal=9&lt;/code&gt; → &lt;code&gt;die&lt;/code&gt;: someone ran &lt;code&gt;docker kill&lt;/code&gt; or &lt;code&gt;docker rm -f&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;die exitCode=137&lt;/code&gt; with no &lt;code&gt;kill&lt;/code&gt; before it: the signal came from outside Docker, usually the kernel. Go to step 4.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;start&lt;/code&gt; after every &lt;code&gt;die&lt;/code&gt; is the restart policy. Newer Docker adds &lt;code&gt;execDuration&lt;/code&gt; to &lt;code&gt;die&lt;/code&gt;; if the exits are memory kills, similar values each time suggest something that grows until it's killed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Docker keeps only recent events in memory (the last 256), and they're gone after a daemon restart, so check soon.&lt;/p&gt;

&lt;p&gt;For the stop pattern: empty &lt;code&gt;stop_signal&lt;/code&gt; and &lt;code&gt;stop_timeout=&amp;lt;nil&amp;gt;&lt;/code&gt; mean the defaults (SIGTERM, 10 s). If PID 1 is a shell wrapper that doesn't forward SIGTERM, or the app has no SIGTERM handler, every stop ends in SIGKILL.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Check the limit and who is using memory
&lt;/h2&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;'memory_limit_bytes={{.HostConfig.Memory}} memory_swap_bytes={{.HostConfig.MemorySwap}}'&lt;/span&gt; YOUR_CONTAINER
docker stats &lt;span class="nt"&gt;--no-stream&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}'&lt;/span&gt;
free &lt;span class="nt"&gt;-h&lt;/span&gt;
ps &lt;span class="nt"&gt;-eo&lt;/span&gt; pid,user,rss,comm &lt;span class="nt"&gt;--sort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;-rss&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;memory_limit_bytes=0&lt;/code&gt;: no limit, so the container can eat host memory until the whole host runs short. A host-wide OOM is then the likelier path.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory_swap_bytes&lt;/code&gt; is memory + swap. Equal to the limit means no swap for this container; &lt;code&gt;-1&lt;/code&gt; means unlimited.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker stats&lt;/code&gt; only covers running containers. Without a limit, the limit column shows host RAM. Several unlimited containers on a 2 GB host are fighting over the same memory.&lt;/li&gt;
&lt;li&gt;In &lt;code&gt;free -h&lt;/code&gt;, read &lt;code&gt;available&lt;/code&gt;, not &lt;code&gt;free&lt;/code&gt;, and check whether there's any swap.&lt;/li&gt;
&lt;li&gt;In a host-wide OOM the kernel picks its victim mostly by size and &lt;code&gt;oom_score_adj&lt;/code&gt;, so &lt;strong&gt;the process that got killed isn't always the one that caused the pressure&lt;/strong&gt;. A neighbour can grow and get your container killed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are snapshots of now, not of the kill. To tell a spike from steady growth, sample after the container comes back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in &lt;/span&gt;1 2 3 4 5 6 7 8 9 10&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%H:%M:%S'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; docker stats &lt;span class="nt"&gt;--no-stream&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Name}} {{.MemUsage}}'&lt;/span&gt; YOUR_CONTAINER&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;60&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Steady climb that never levels off → leak, or an unbounded cache or queue. Jumps during specific requests or jobs → peak workload. Ten minutes is a hint; a real trend needs hours or days of monitoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Read the kernel log, and know when OOMKilled lies
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dmesg &lt;span class="nt"&gt;-T&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'oom-kill|out of memory|killed process'&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 20
&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"2026-09-29 14:00"&lt;/span&gt; &lt;span class="nt"&gt;--until&lt;/span&gt; &lt;span class="s2"&gt;"2026-09-29 14:30"&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'oom-kill|out of memory|killed process'&lt;/span&gt;
docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Id}}'&lt;/span&gt; YOUR_CONTAINER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Pick a window around &lt;code&gt;finished&lt;/code&gt; (converted to local time) or a &lt;code&gt;die&lt;/code&gt; time. &lt;code&gt;journalctl&lt;/code&gt; timestamps are usually more reliable than &lt;code&gt;dmesg -T&lt;/code&gt;'s converted ones.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dmesg&lt;/code&gt; only covers the current boot. After a reboot, &lt;code&gt;sudo journalctl -k -b -1&lt;/code&gt; shows the previous one, if the journal is persistent.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Memory cgroup out of memory: Killed process 2314 (node)&lt;/code&gt; → a cgroup limit, normally the container's. &lt;code&gt;Out of memory: Killed process …&lt;/code&gt; without "Memory cgroup" → the whole host.&lt;/li&gt;
&lt;li&gt;Newer kernels add an &lt;code&gt;oom-kill:&lt;/code&gt; line first: &lt;code&gt;constraint=CONSTRAINT_MEMCG&lt;/code&gt; for a cgroup limit, &lt;code&gt;CONSTRAINT_NONE&lt;/code&gt; plus &lt;code&gt;global_oom&lt;/code&gt; for host-wide. &lt;code&gt;task_memcg=&lt;/code&gt; usually contains the container ID (e.g. &lt;code&gt;/system.slice/docker-&amp;lt;id&amp;gt;.scope&lt;/code&gt;); search for its first 12 characters.&lt;/li&gt;
&lt;li&gt;The PID is the host PID. &lt;code&gt;anon-rss&lt;/code&gt; is roughly what the process held when killed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No matching kernel line? Check for a userspace OOM daemon, which kills without the kernel logging an OOM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl is-active systemd-oomd earlyoom
&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; systemd-oomd &lt;span class="nt"&gt;-u&lt;/span&gt; earlyoom &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"2026-09-29 14:00"&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Memory was the cause but &lt;code&gt;OOMKilled&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;.&lt;/strong&gt; How Docker learns about an OOM kill depends on the kernel, cgroup version and containerd/Docker versions. Things to check, not rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The container restarted before you inspected it.&lt;/li&gt;
&lt;li&gt;A userspace daemon did the killing, not the kernel.&lt;/li&gt;
&lt;li&gt;The whole host ran out. On cgroup v1 the event Docker listens for is tied to the container's own limit, so host-wide kills often leave the flag &lt;code&gt;false&lt;/code&gt;. On cgroup v2 the kernel counts OOM kills per cgroup, so the flag is more likely set, though older containerd/Docker versions had gaps.&lt;/li&gt;
&lt;li&gt;On Docker Desktop, memory runs out inside Docker's VM, invisible to Mac/Windows tools.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker info &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'cgroup={{.CgroupVersion}} driver={{.CgroupDriver}}'&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;YOUR_CONTAINER &lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/memory.events   &lt;span class="c"&gt;# cgroup v2, if the image has cat&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A nonzero &lt;code&gt;oom_kill&lt;/code&gt; means the kernel killed a process in this container since it last started.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Pick a next step that matches the evidence
&lt;/h2&gt;

&lt;p&gt;Each of these is a change to plan, with a way back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Container limit hit, workload really needs more.&lt;/strong&gt; Raise &lt;code&gt;mem_limit&lt;/code&gt; / &lt;code&gt;deploy.resources.limits.memory&lt;/code&gt; / &lt;code&gt;--memory&lt;/code&gt;, after checking the host has room for all limits plus the OS. &lt;code&gt;docker update --memory&lt;/code&gt; changes a running container, but the next Compose recreate reverts to the file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No limit, host ran out.&lt;/strong&gt; Limits keep one container's growth inside that container, so it dies instead of the database next door. If the services together need more than the host has, it's a bigger instance or fewer services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory climbs after every start.&lt;/strong&gt; Looks like a leak or an unbounded cache/queue; a higher limit only delays the kill. Compare with the last deploy and use the runtime's profilers. Recycling workers is a stopgap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime heap settings.&lt;/strong&gt; Keep the runtime's own limit below the container's so failure shows up as a logged error, not a silent 137. JVM (JDK 10+, 8u191+) reads the container limit and typically caps heap at a quarter of it; set &lt;code&gt;-XX:MaxRAMPercentage&lt;/code&gt; or &lt;code&gt;-Xmx&lt;/code&gt;, leaving headroom for non-heap memory. Node.js: &lt;code&gt;--max-old-space-size=&amp;lt;MB&amp;gt;&lt;/code&gt;. Multi-worker servers multiply per-worker memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swap, with caveats.&lt;/strong&gt; Absorbs short peaks, but under sustained pressure everything slows instead of failing fast, and leaks hide longer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It was a stop, not memory.&lt;/strong&gt; Handle SIGTERM, use exec-form &lt;code&gt;CMD&lt;/code&gt;/&lt;code&gt;ENTRYPOINT&lt;/code&gt; or &lt;code&gt;exec&lt;/code&gt; in entrypoint scripts so the app is PID 1, or add &lt;code&gt;--init&lt;/code&gt; / &lt;code&gt;init: true&lt;/code&gt;. Raise the grace period only if clean shutdown genuinely takes longer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Something else on the host.&lt;/strong&gt; Find the script, agent or person from the event timestamp. Memory settings won't fix this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before touching production, write down the evidence, the change, how you'll verify it and how you'll roll it back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Illustrative example
&lt;/h2&gt;

&lt;p&gt;Illustrative example (not a real case): a 2 GB VPS runs &lt;code&gt;api&lt;/code&gt; (Node.js), &lt;code&gt;postgres&lt;/code&gt; and &lt;code&gt;nginx&lt;/code&gt; with Compose. The API drops out for a few seconds several times a day. &lt;code&gt;docker ps -a&lt;/code&gt; shows &lt;code&gt;api&lt;/code&gt; as &lt;code&gt;Up 12 minutes&lt;/code&gt;; inspect says &lt;code&gt;status=running exit=0 oom=false restarts=5&lt;/code&gt;, clean only because the last start reset it. &lt;code&gt;docker events --since 24h&lt;/code&gt; shows five &lt;code&gt;die exitCode=137&lt;/code&gt;, each followed by &lt;code&gt;start&lt;/code&gt;, no &lt;code&gt;kill&lt;/code&gt; before any of them, so not &lt;code&gt;docker kill&lt;/code&gt; or &lt;code&gt;docker stop&lt;/code&gt;. &lt;code&gt;memory_limit_bytes=0&lt;/code&gt;. One &lt;code&gt;die&lt;/code&gt; is at 06:05:41 UTC, 14:05:41 host time (UTC+8). &lt;code&gt;journalctl -k&lt;/code&gt; at that minute has an &lt;code&gt;oom-kill:&lt;/code&gt; line with &lt;code&gt;global_oom&lt;/code&gt; and a &lt;code&gt;task_memcg&lt;/code&gt; path containing the &lt;code&gt;api&lt;/code&gt; container ID, then &lt;code&gt;Out of memory: Killed process 2314 (node) … anon-rss:1398212kB&lt;/code&gt;. Ten &lt;code&gt;docker stats&lt;/code&gt; samples after restart show &lt;code&gt;api&lt;/code&gt; rising about 15 MB a minute while &lt;code&gt;postgres&lt;/code&gt; stays flat.&lt;/p&gt;

&lt;p&gt;Together: the host ran out, the kernel picked the Node process in &lt;code&gt;api&lt;/code&gt;, and &lt;code&gt;api&lt;/code&gt; grows steadily after each start, which fits a leak or unbounded cache better than a one-off peak. Not proven: which code path grows. Next steps are a person's call: give &lt;code&gt;api&lt;/code&gt; a limit, set the Node heap below it so failures get logged, and hunt the growth starting from the last deploy.&lt;/p&gt;

&lt;p&gt;137 is a clue and &lt;code&gt;OOMKilled&lt;/code&gt; is evidence, not proof. Conclude from the exit state, Docker events and kernel lines with matching timestamps.&lt;/p&gt;




&lt;p&gt;I work on OpsMate, which puts an SSH terminal and an AI assistant on one page, so you can run the commands above yourself or describe the exit in plain language and check against the raw output. Each time you ask the AI for help, recent terminal output is sent to the cloud AI along with your message, so remove sensitive information first. Every action is sorted into one of three risk tiers. Low-risk actions (such as a confident service restart or log rotation) run automatically. Medium-risk actions (such as changing config or recreating a container) are sent to you for approval first, and run automatically if no one responds within 30 minutes. High-risk actions (database schema, kernel parameter, network and firewall changes) only raise an alert and are never executed. When the system isn't sure, it treats the action as the next tier up; dangerous commands are always blocked. The full guide is &lt;a href="https://www.itops.sh/en/guides/oom-killed-exit-137/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=sprint_w2&amp;amp;utm_content=oom-killed-exit-137" rel="noopener noreferrer"&gt;on itops.sh&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Drafted with AI assistance and reviewed before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>linux</category>
      <category>containers</category>
    </item>
    <item>
      <title>Docker exit codes: read the number before you restart anything</title>
      <dc:creator>itops</dc:creator>
      <pubDate>Sun, 11 Oct 2026 07:36:08 +0000</pubDate>
      <link>https://dev.to/itops/docker-exit-codes-read-the-number-before-you-restart-anything-3lpl</link>
      <guid>https://dev.to/itops/docker-exit-codes-read-the-number-before-you-restart-anything-3lpl</guid>
      <description>&lt;p&gt;CI says the deploy succeeded, then &lt;code&gt;docker ps -a&lt;/code&gt; shows &lt;code&gt;Exited (1)&lt;/code&gt; or &lt;code&gt;Exited (127)&lt;/code&gt;. Don't restart yet: get the exit code, check what it usually means (and does not prove), then read the first log error. Read-only; authorized servers only. Replace &lt;code&gt;YOUR_CONTAINER&lt;/code&gt; with the real name or ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Get the exit code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'table {{.Names}}\t{{.Status}}\t{{.Image}}'&lt;/span&gt;
docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.Error}}'&lt;/span&gt; YOUR_CONTAINER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Compose, &lt;code&gt;docker compose ps -a&lt;/code&gt; includes stopped ones; name is usually &lt;code&gt;&amp;lt;project&amp;gt;-&amp;lt;service&amp;gt;-1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Exited (1) 8 seconds ago&lt;/code&gt; is a single exit with code 1. &lt;code&gt;Restarting (1) 3 seconds ago&lt;/code&gt; means a restart policy is looping; the bracketed number is from the last exit.&lt;/p&gt;

&lt;p&gt;Inspect prints exit code, &lt;code&gt;OOMKilled&lt;/code&gt; (&lt;code&gt;true&lt;/code&gt;/&lt;code&gt;false&lt;/code&gt;), and &lt;code&gt;State.Error&lt;/code&gt;. Keep them apart. &lt;code&gt;State.Error&lt;/code&gt; is set when Docker couldn't start the process; empty for most app crashes. &lt;code&gt;Created&lt;/code&gt; means it never ran — usually a run-command or image problem.&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;'started={{.State.StartedAt}} finished={{.State.FinishedAt}} restarts={{.RestartCount}} policy={{.HostConfig.RestartPolicy.Name}}'&lt;/span&gt; YOUR_CONTAINER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few seconds between &lt;code&gt;started&lt;/code&gt; and &lt;code&gt;finished&lt;/code&gt; is the "exits right after deploy" pattern. Timestamps are UTC (&lt;code&gt;Z&lt;/code&gt;); convert before comparing with local logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Exit codes at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Usual meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Main process finished successfully&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Generic application error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Shell syntax error, wrong usage, or an app-defined error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;125&lt;/td&gt;
&lt;td&gt;Docker itself failed to run the container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;126&lt;/td&gt;
&lt;td&gt;Command found but not executable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;127&lt;/td&gt;
&lt;td&gt;Command not found&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;132&lt;/td&gt;
&lt;td&gt;Illegal instruction, SIGILL (128 + 4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;137&lt;/td&gt;
&lt;td&gt;Killed with SIGKILL (128 + 9)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;139&lt;/td&gt;
&lt;td&gt;Segmentation fault, SIGSEGV (128 + 11)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;143&lt;/td&gt;
&lt;td&gt;Stopped with SIGTERM (128 + 15)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;td&gt;Exit status −1, a failed &lt;code&gt;ssh&lt;/code&gt;, or Docker lost the process&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Above 128 usually means "killed by signal (code − 128)". &lt;code&gt;kill -l 137&lt;/code&gt; prints &lt;code&gt;KILL&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. What each code means
&lt;/h2&gt;

&lt;h3&gt;
  
  
  0 — finished successfully
&lt;/h3&gt;

&lt;p&gt;Expected for a migration or backup. A web service exiting 0 usually backgrounded itself or ran a one-shot. Check entrypoint/cmd:&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;'entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}}'&lt;/span&gt; YOUR_CONTAINER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;0 only means the main process ended cleanly.&lt;/p&gt;

&lt;h3&gt;
  
  
  1 — generic application error
&lt;/h3&gt;

&lt;p&gt;Most runtimes exit 1 for any uncaught exception or failed startup check. Read the logs (section 4) for the &lt;strong&gt;first&lt;/strong&gt; error, then compare with this deploy's changes (image, env, config, migrations). The number alone does not name the error.&lt;/p&gt;

&lt;h3&gt;
  
  
  2 — syntax, usage, or app-defined error
&lt;/h3&gt;

&lt;p&gt;No Docker-specific meaning — whatever the main process returned. Common sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shell script that doesn't parse.&lt;/strong&gt; Bash/dash exit 2 on syntax errors. An entrypoint with Windows (CRLF) endings often shows &lt;code&gt;syntax error: unexpected end of file&lt;/code&gt; (bash) or &lt;code&gt;Syntax error: end of file unexpected&lt;/code&gt; (dash). Outside &lt;code&gt;if&lt;/code&gt;/&lt;code&gt;case&lt;/code&gt;, CRLF can instead yield &lt;code&gt;not found&lt;/code&gt; / exit 127.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong builtin usage.&lt;/strong&gt; Bash builtins return 2 for an invalid option or missing argument (e.g. &lt;code&gt;source&lt;/code&gt; with no filename).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Program rejecting arguments.&lt;/strong&gt; Python &lt;code&gt;argparse&lt;/code&gt; → &lt;code&gt;unrecognized arguments&lt;/code&gt;; Go &lt;code&gt;flag&lt;/code&gt; → &lt;code&gt;flag provided but not defined&lt;/code&gt;. Compose &lt;code&gt;command:&lt;/code&gt; typos show up here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing file.&lt;/strong&gt; &lt;code&gt;python3 /app/main.py&lt;/code&gt; exits 2 with &lt;code&gt;can't open file ... No such file or directory&lt;/code&gt; if the path is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unrecovered Go panic.&lt;/strong&gt; Prints &lt;code&gt;panic:&lt;/code&gt; plus a stack trace, then exits 2.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs &lt;span class="nt"&gt;--timestamps&lt;/span&gt; YOUR_CONTAINER 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 30
bash &lt;span class="nt"&gt;-n&lt;/span&gt; entrypoint.sh          &lt;span class="c"&gt;# parse only&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt; entrypoint.sh    &lt;span class="c"&gt;# 0 = LF line endings&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CRLF fix: &lt;code&gt;sed -i 's/\r$//' entrypoint.sh&lt;/code&gt;, add &lt;code&gt;*.sh text eol=lf&lt;/code&gt; to &lt;code&gt;.gitattributes&lt;/code&gt;, rebuild. The log line decides the cause.&lt;/p&gt;

&lt;h3&gt;
  
  
  125 — Docker could not run the container
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;docker run&lt;/code&gt; or the daemon rejected the request; the process usually never started. Read the CI/deploy error and &lt;code&gt;State.Error&lt;/code&gt;. Typical: unknown flag, pull denied, port already allocated (&lt;code&gt;sudo ss -ltnp&lt;/code&gt;). Not an app bug. When Docker can't start it, status is often &lt;code&gt;Created&lt;/code&gt;; when a shell inside hits 126/127, look in &lt;code&gt;docker logs&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  126 — found but not executable
&lt;/h3&gt;

&lt;p&gt;Check the command and permissions inside the image. A bind-mounted script keeps the host execute bit. Compare &lt;code&gt;docker image inspect --format '{{.Os}}/{{.Architecture}}' YOUR_IMAGE&lt;/code&gt; with &lt;code&gt;uname -m&lt;/code&gt;. Mismatch → usually &lt;code&gt;exec format error&lt;/code&gt;. 126 = can't execute; 127 = can't find.&lt;/p&gt;

&lt;h3&gt;
  
  
  127 — command not found
&lt;/h3&gt;

&lt;p&gt;The program never started. Confirm the binary exists &lt;strong&gt;in this image&lt;/strong&gt; and on &lt;code&gt;PATH&lt;/code&gt;. Slimmer bases and Compose &lt;code&gt;command:&lt;/code&gt; typos are common. Does not prove a crash after startup.&lt;/p&gt;

&lt;h3&gt;
  
  
  132 — illegal instruction (SIGILL)
&lt;/h3&gt;

&lt;p&gt;132 is 128 + 4 (&lt;code&gt;kill -l 132&lt;/code&gt; → &lt;code&gt;ILL&lt;/code&gt;). The CPU hit an instruction it couldn't run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Binary needs features this host lacks (AVX/AVX2/AVX-512, x86-64-v3); MongoDB 5.0+ on x86_64 documents AVX.&lt;/li&gt;
&lt;li&gt;VM hides features behind a basic hypervisor CPU model.&lt;/li&gt;
&lt;li&gt;QEMU emulating another arch hits unimplemented instructions.&lt;/li&gt;
&lt;li&gt;Deliberate trap (&lt;code&gt;ud2&lt;/code&gt; / &lt;code&gt;__builtin_trap()&lt;/code&gt;) — a bug, not a CPU mismatch.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs &lt;span class="nt"&gt;--timestamps&lt;/span&gt; YOUR_CONTAINER 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 20
&lt;span class="nb"&gt;sudo &lt;/span&gt;dmesg &lt;span class="nt"&gt;-T&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'invalid opcode'&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 10
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-m1&lt;/span&gt; &lt;span class="s1"&gt;'^flags'&lt;/span&gt; /proc/cpuinfo | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'avx|avx2|avx512f'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A restart policy won't help. Rule out the trap case before buying a new server.&lt;/p&gt;

&lt;h3&gt;
  
  
  137 — SIGKILL
&lt;/h3&gt;

&lt;p&gt;137 is 128 + 9. Check &lt;code&gt;OOMKilled&lt;/code&gt;: &lt;code&gt;true&lt;/code&gt; → memory lead; &lt;code&gt;false&lt;/code&gt; → &lt;code&gt;docker kill&lt;/code&gt;, timed-out &lt;code&gt;docker stop&lt;/code&gt;, or another host process. &lt;strong&gt;137 alone is not proof of OOM.&lt;/strong&gt; Check kernel log, memory limits, and &lt;code&gt;docker events&lt;/code&gt; before raising limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  139 — SIGSEGV
&lt;/h3&gt;

&lt;p&gt;Invalid memory access; kernel sent signal 11. Read logs around the exit, then &lt;code&gt;sudo dmesg -T | grep -i segfault | tail -n 20&lt;/code&gt;. Note native-dependency or runtime upgrades. Kernel lines only count if their time matches &lt;code&gt;finished&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  143 — SIGTERM
&lt;/h3&gt;

&lt;p&gt;A normal stop. Find who asked: redeploy, &lt;code&gt;docker stop&lt;/code&gt;, Compose recreate, host shutdown, orchestrator. Docker keeps only recent events (last 256):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker events &lt;span class="nt"&gt;--since&lt;/span&gt; 1h &lt;span class="nt"&gt;--until&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;container&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_CONTAINER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a crash — who sent SIGTERM, and did the replacement come up?&lt;/p&gt;

&lt;h3&gt;
  
  
  255 — exit(−1), failed ssh, or Docker lost the process
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Program returned −1.&lt;/strong&gt; Only the lowest 8 bits reach the parent, so &lt;code&gt;exit(-1)&lt;/code&gt; shows as 255 (bash/Python/Node/Go).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failed &lt;code&gt;ssh&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;ssh&lt;/code&gt; exits 255 when ssh itself fails (refused, auth, host key), not when the remote command fails. Backup/deploy jobs that end in &lt;code&gt;ssh&lt;/code&gt; exit 255 on connection failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker lost the process.&lt;/strong&gt; After a host crash, hard reboot or daemon crash, dockerd finds a container recorded as running with no saved exit status and marks it &lt;code&gt;Exited (255)&lt;/code&gt; — Docker's bookkeeping, not the app's verdict.
&lt;/li&gt;
&lt;/ul&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;'exit={{.State.ExitCode}} started={{.State.StartedAt}} finished={{.State.FinishedAt}} policy={{.HostConfig.RestartPolicy.Name}}'&lt;/span&gt; YOUR_CONTAINER
docker logs &lt;span class="nt"&gt;--timestamps&lt;/span&gt; YOUR_CONTAINER 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 30
&lt;span class="nb"&gt;uptime&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; docker.service &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"24 hours ago"&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'starting up|completed initialization'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ssh:&lt;/code&gt; errors in the last lines → fix key / &lt;code&gt;known_hosts&lt;/code&gt; / network. &lt;code&gt;finished&lt;/code&gt; matching boot (&lt;code&gt;uptime -s&lt;/code&gt;) or a dockerd &lt;code&gt;Starting up&lt;/code&gt; line, with a clean log stop → why did the host/daemon restart? &lt;code&gt;finished&lt;/code&gt; is UTC; convert first.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Find the first error in the logs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs &lt;span class="nt"&gt;--since&lt;/span&gt; 30m &lt;span class="nt"&gt;--timestamps&lt;/span&gt; YOUR_CONTAINER 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 80
docker logs &lt;span class="nt"&gt;--since&lt;/span&gt; 30m &lt;span class="nt"&gt;--timestamps&lt;/span&gt; YOUR_CONTAINER 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'error|fatal|panic|exception|denied|not found|refused'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works on a stopped container until removed (&lt;code&gt;--rm&lt;/code&gt; takes logs with it). Use &lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt; — most errors are on stderr. Read the &lt;strong&gt;first&lt;/strong&gt; error after deploy, not only the last line.&lt;/p&gt;

&lt;p&gt;Common first lines (hypotheses until checked):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;connection refused&lt;/code&gt;: dependency down or wrong address; inside a container, &lt;code&gt;localhost&lt;/code&gt; is itself.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;permission denied&lt;/code&gt;: mount ownership or runtime user.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;no such file or directory&lt;/code&gt; for a script that exists: often CRLF on the shebang, or a missing interpreter.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exec format error&lt;/code&gt;: wrong arch (see 126).&lt;/li&gt;
&lt;li&gt;Missing env/config key: compare key &lt;strong&gt;names&lt;/strong&gt; with the last good deploy; don't print secrets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Redact passwords, tokens, connection strings and customer data before pasting logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Illustrative example
&lt;/h2&gt;

&lt;p&gt;Illustrative example (not a real case): after a slimmer base image, &lt;code&gt;docker ps -a&lt;/code&gt; shows &lt;code&gt;Restarting (127) 4 seconds ago&lt;/code&gt;. Inspect: &lt;code&gt;127 false&lt;/code&gt;, empty error — process started, not OOM. &lt;code&gt;started&lt;/code&gt;/&lt;code&gt;finished&lt;/code&gt; ~1 s apart. First log line: &lt;code&gt;/app/start.sh: 5: exec: gunicorn: not found&lt;/code&gt;. Entrypoint runs; new base lacks the app server. Restart policy or more memory won't help. Next: roll back the image tag, or rebuild with the dependency, then watch whether it stays up.&lt;/p&gt;

&lt;p&gt;An exit code is a clue, not a diagnosis — combine the code, log lines and matching timestamps.&lt;/p&gt;




&lt;p&gt;I work on OpsMate, which puts an SSH terminal and an AI assistant on one page, so you can run the commands above yourself or describe the exit in plain language and check against the raw output. Each time you ask the AI for help, recent terminal output is sent to the cloud AI along with your message, so remove sensitive information first. Every action is sorted into one of three risk tiers. Low-risk actions (such as a confident service restart or log rotation) run automatically. Medium-risk actions (such as changing config or recreating a container) are sent to you for approval first, and run automatically if no one responds within 30 minutes. High-risk actions (database schema, kernel parameter, network and firewall changes) only raise an alert and are never executed. When the system isn't sure, it treats the action as the next tier up; dangerous commands are always blocked. The full guide is &lt;a href="https://www.itops.sh/en/guides/docker-exit-codes/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=sprint_w2&amp;amp;utm_content=docker-exit-codes" rel="noopener noreferrer"&gt;on itops.sh&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Drafted with AI assistance and reviewed before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>linux</category>
      <category>containers</category>
    </item>
    <item>
      <title>Nginx 502 Bad Gateway: read one log line before you touch anything</title>
      <dc:creator>itops</dc:creator>
      <pubDate>Fri, 09 Oct 2026 07:30:27 +0000</pubDate>
      <link>https://dev.to/itops/nginx-502-bad-gateway-read-one-log-line-before-you-touch-anything-16jo</link>
      <guid>https://dev.to/itops/nginx-502-bad-gateway-read-one-log-line-before-you-touch-anything-16jo</guid>
      <description>&lt;p&gt;When a site starts returning &lt;strong&gt;502 Bad Gateway&lt;/strong&gt;, the instinct is to restart nginx, bump a timeout, or reboot the box. Most of the time none of that is needed, because nginx is the one part that's working. A 502 means nginx took the request, passed it to the upstream (your Node app, Gunicorn, PHP-FPM, a container), and got nothing usable back.&lt;/p&gt;

&lt;p&gt;The fastest way out is to read the one log line nginx wrote at the moment of the failure. This post is the short version of how I do that on Ubuntu 22.04/24.04. Everything until the last section only reads state, and all of it is for servers you're authorized to access.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Make sure the 502 is really from your nginx
&lt;/h2&gt;

&lt;p&gt;If you sit behind a CDN, its 502 page looks a lot like nginx's. Check from your laptop, then from the server itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-D&lt;/span&gt; - https://example.com/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(HTTP|server|cf-ray)'&lt;/span&gt;
curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt; &lt;span class="nt"&gt;--resolve&lt;/span&gt; example.com:443:127.0.0.1 https://example.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;server: cloudflare&lt;/code&gt; or &lt;code&gt;cf-ray&lt;/code&gt; header means the CDN answered. If the second command (run on the server, straight to local nginx) returns 200, the problem is between the CDN and your server, not behind nginx. If it's also 502, keep going.&lt;/p&gt;

&lt;p&gt;Then confirm nginx itself is healthy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status nginx &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;active (running)&lt;/code&gt; plus &lt;code&gt;test is successful&lt;/code&gt; means stop suspecting nginx.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Find the line that matches the failing request
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 50 /var/log/nginx/error.log
&lt;span class="nb"&gt;sudo grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'upstream|connect\(\)'&lt;/span&gt; /var/log/nginx/error.log | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a server block has its own log, &lt;code&gt;sudo nginx -T 2&amp;gt;/dev/null | grep -n -E 'error_log|proxy_pass|fastcgi_pass|upstream'&lt;/code&gt; shows where it goes and where each location proxies. (Don't paste &lt;code&gt;nginx -T&lt;/code&gt; output anywhere public; it can contain credentials.)&lt;/p&gt;

&lt;p&gt;A typical line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[error] connect() failed (111: Connection refused) while connecting to upstream, client: 203.0.113.10, server: example.com, request: "GET /api/orders HTTP/1.1", upstream: "http://127.0.0.1:3000/api/orders"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;upstream:&lt;/code&gt; field is the exact address nginx tried. Comparing it with where the app actually listens solves a surprising number of 502s on the spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Map the message to a cause
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;error.log says&lt;/th&gt;
&lt;th&gt;What it usually means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(111: Connection refused)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nothing listening at that address: app stopped, crashed, still starting, or on another port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unix:...sock failed (2: No such file or directory)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Socket file doesn't exist: service stopped, or nginx points at an old path (classic after a PHP upgrade)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unix:...sock failed (13: Permission denied)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;www-data&lt;/code&gt; can't open the socket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unix:...sock failed (11: Resource temporarily unavailable)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All workers busy, socket backlog overflowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;upstream prematurely closed connection&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;App accepted the request then died: crash, killed worker, app-server timeout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(104: Connection reset by peer)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Process restarted or worker killed, or a reused keepalive connection was already closed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;upstream sent too big header&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Response headers (often a growing &lt;code&gt;Set-Cookie&lt;/code&gt;) don't fit the buffer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SSL_do_handshake() failed ... wrong version number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;proxy_pass https://&lt;/code&gt; pointed at a plain-HTTP port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no live upstreams&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Every server in the &lt;code&gt;upstream {}&lt;/code&gt; group was marked failed earlier; scroll up for why&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;upstream timed out (110: ...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;That's a &lt;strong&gt;504&lt;/strong&gt;, not a 502: the app is slow, not gone&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things that look related but aren't 502s: &lt;code&gt;host not found in upstream&lt;/code&gt; is logged at &lt;code&gt;[emerg]&lt;/code&gt; when nginx loads config, so nginx won't start or reload at all. And &lt;code&gt;(13: Permission denied)&lt;/code&gt; on a &lt;strong&gt;TCP&lt;/strong&gt; address is the SELinux case on RHEL-family systems; stock Ubuntu nginx isn't confined by AppArmor, so there it's a plain file-permission problem.&lt;/p&gt;

&lt;p&gt;If you saw "The proxy server received an invalid response from an upstream server", that page is Apache &lt;code&gt;mod_proxy&lt;/code&gt;, not nginx.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Confirm it on the upstream side
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is the service up, and what did it log?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status YOUR_APP &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; YOUR_APP &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"30 min ago"&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 50
docker ps &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'table {{.Names}}\t{{.Status}}\t{{.Ports}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Is something listening where nginx connects?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ss &lt;span class="nt"&gt;-ltnp&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ss &lt;span class="nt"&gt;-lxp&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'php|gunicorn|uwsgi|\.sock'&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /run/php/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three mismatches I see a lot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App moved to 3001 after a deploy, nginx still sends to 3000.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;proxy_pass http://localhost:3000&lt;/code&gt; while the app only listens on 127.0.0.1. nginx may resolve &lt;code&gt;localhost&lt;/code&gt; to both 127.0.0.1 and &lt;code&gt;[::1]&lt;/code&gt;, and the IPv6 attempt is refused; the log shows &lt;code&gt;upstream: "http://[::1]:3000/..."&lt;/code&gt;. Writing &lt;code&gt;127.0.0.1&lt;/code&gt; removes the ambiguity.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;php8.1-fpm.sock&lt;/code&gt; in the site config, but only &lt;code&gt;php8.3-fpm.sock&lt;/code&gt; exists after an Ubuntu 22.04 to 24.04 upgrade.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Does the app answer if you skip nginx?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code} %{time_total}s\n'&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 10 &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Host: example.com'&lt;/span&gt; http://127.0.0.1:3000/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Refused here too means the app isn't serving. 200 here but 502 through nginx means look at the address mismatch, socket permissions, or the response headers. (PHP-FPM speaks FastCGI, so curl can't test it directly.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Was a worker killed?&lt;/strong&gt; For &lt;code&gt;prematurely closed&lt;/code&gt; or &lt;code&gt;reset by peer&lt;/code&gt;, check the same minute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"1 hour ago"&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'out of memory|killed process'&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; YOUR_APP &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"1 hour ago"&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'worker timeout|killed|signal'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gunicorn's &lt;code&gt;[CRITICAL] WORKER TIMEOUT&lt;/code&gt; means Gunicorn killed the worker after its own &lt;code&gt;--timeout&lt;/code&gt; (30 s default). Raising nginx timeouts won't help, because the app server cut the request first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Docker gotcha
&lt;/h2&gt;

&lt;p&gt;Inside an nginx container, &lt;code&gt;proxy_pass http://127.0.0.1:3000;&lt;/code&gt; points at the nginx container itself. Proxy to the Compose service name (&lt;code&gt;http://app:3000&lt;/code&gt;), make the app listen on &lt;code&gt;0.0.0.0&lt;/code&gt; inside its container, or, with nginx on the host, publish the port to loopback (&lt;code&gt;127.0.0.1:3000:3000&lt;/code&gt;). Also remember nginx resolves hostnames at config load: if the app container is recreated with a new IP, nginx can keep using the old one until reload.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the log points at buffers or keepalive
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;upstream sent too big header&lt;/code&gt; needs bigger buffers, and you have to raise them together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_buffer_size&lt;/span&gt; &lt;span class="mi"&gt;16k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_buffers&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="mi"&gt;16k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Raising only &lt;code&gt;proxy_buffer_size&lt;/code&gt; makes &lt;code&gt;nginx -t&lt;/code&gt; fail on &lt;code&gt;proxy_busy_buffers_size&lt;/code&gt;. For PHP-FPM the equivalents are &lt;code&gt;fastcgi_buffer_size&lt;/code&gt; / &lt;code&gt;fastcgi_buffers&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Occasional 502s with an &lt;code&gt;upstream {}&lt;/code&gt; block that uses &lt;code&gt;keepalive&lt;/code&gt;: if the app closes idle connections sooner than nginx expects, a request can land on a just-closed connection. Node.js closes idle keep-alive connections after 5 s by default (&lt;code&gt;server.keepAliveTimeout&lt;/code&gt;), so make the app's idle timeout longer than nginx's upstream &lt;code&gt;keepalive_timeout&lt;/code&gt; (60 s default), or lower the latter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changing things safely
&lt;/h2&gt;

&lt;p&gt;Only now do we change anything, and only the one thing the log pointed at:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Back up the file (&lt;code&gt;sudo cp /etc/nginx/sites-available/example.com /root/example.com.bak&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Make the single change.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sudo nginx -t&lt;/code&gt;, then &lt;code&gt;sudo systemctl reload nginx&lt;/code&gt; (reload keeps connections; restart drops them).&lt;/li&gt;
&lt;li&gt;Repeat the failing request with &lt;code&gt;sudo tail -f /var/log/nginx/error.log&lt;/code&gt; open.&lt;/li&gt;
&lt;li&gt;No better? Restore the backup, test, reload.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Don't raise every timeout "just in case", and don't &lt;code&gt;chmod 0666&lt;/code&gt; a socket. Fix what the log names.&lt;/p&gt;




&lt;p&gt;I work on OpsMate, which puts an SSH terminal and an AI assistant on one page, so you can run the commands above yourself or describe the 502 in plain language and check the answer against the raw output. Each time you ask the AI for help, recent terminal output is sent to the cloud AI along with your message, so remove sensitive information first. Every action is sorted into one of three risk tiers. Low-risk actions (such as a confident service restart or log rotation) run automatically. Medium-risk actions (such as changing config or recreating a container) are sent to you for approval first, and run automatically if no one responds within 30 minutes. High-risk actions (database schema, kernel parameter, network and firewall changes) only raise an alert and are never executed. When the system isn't sure, it treats the action as the next tier up; dangerous commands are always blocked. The full guide, with every error message and the PHP-FPM and socket-permission details, is &lt;a href="https://www.itops.sh/en/guides/nginx-502-bad-gateway/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=sprint_w2" rel="noopener noreferrer"&gt;on itops.sh&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Drafted with AI assistance and reviewed before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nginx</category>
      <category>devops</category>
      <category>linux</category>
      <category>ubuntu</category>
    </item>
  </channel>
</rss>
