<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Docker Archives | Abdul Wahab Junaid</title>
	<atom:link href="https://awjunaid.com/category/docker/feed/" rel="self" type="application/rss+xml" />
	<link>https://awjunaid.com/category/docker/</link>
	<description>Offensive Security Researcher &#38; Quantum Cryptography Analyst</description>
	<lastBuildDate>Wed, 29 Jul 2026 21:49:50 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://i0.wp.com/awjunaid.com/wp-content/uploads/2023/06/cropped-1668274976669.jpeg?fit=32%2C32&#038;ssl=1</url>
	<title>Docker Archives | Abdul Wahab Junaid</title>
	<link>https://awjunaid.com/category/docker/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">220030102</site>	<item>
		<title>How to Dynamically Configure a Load Balancer for a MySQL Galera Cluster: Complete Guide</title>
		<link>https://awjunaid.com/docker/how-to-dynamically-configure-a-load-balancer-for-a-mysql-galera-cluster-complete-guide/</link>
					<comments>https://awjunaid.com/docker/how-to-dynamically-configure-a-load-balancer-for-a-mysql-galera-cluster-complete-guide/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sat, 02 Sep 2023 06:13:09 +0000</pubDate>
				<category><![CDATA[Docker]]></category>
		<category><![CDATA[docker]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=4673</guid>

					<description><![CDATA[<p>If you&#8217;ve ever run a Galera cluster in production, you already know the database part is only half&#8230;</p>
<p>The post <a href="https://awjunaid.com/docker/how-to-dynamically-configure-a-load-balancer-for-a-mysql-galera-cluster-complete-guide/">How to Dynamically Configure a Load Balancer for a MySQL Galera Cluster: Complete Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">If you&#8217;ve ever run a Galera cluster in production, you already know the database part is only half the battle. The other half — the part that quietly determines whether your &#8220;highly available&#8221; cluster actually behaves that way during a failover — is the load balancer sitting in front of it. I&#8217;ve been burned before by a static load balancer config that kept sending traffic to a node that had silently dropped out of the cluster. That single mistake taught me why <em>dynamic</em> health-aware load balancing isn&#8217;t optional for Galera; it&#8217;s the whole point.</p>



<p class="wp-block-paragraph">In this guide I&#8217;ll walk through why Galera needs special load balancing logic, how to set it up with HAProxy using <code>clustercheck</code>/<code>xinetd</code> health checks, and how to make the whole thing reconfigure itself automatically as nodes join, leave, or go into a non-primary state.</p>



<h2 class="wp-block-heading">Why Galera Load Balancing Is Different From Regular MySQL</h2>



<p class="wp-block-paragraph">A standard master-replica MySQL setup usually routes writes to one master and reads to replicas. Galera is a multi-master, synchronous (virtually synchronous) cluster — every node can technically accept writes. But &#8220;can&#8221; isn&#8217;t &#8220;should&#8221;:</p>



<ul class="wp-block-list">
<li><strong>Write conflicts</strong>: If you write to multiple nodes simultaneously, you increase the chance of certification conflicts and rollbacks under load.</li>



<li><strong>Flow control</strong>: A lagging node can throttle the whole cluster, so blindly balancing writes across all nodes evenly isn&#8217;t ideal.</li>



<li><strong>Node state matters</strong>: A node can be <code>Synced</code>, <code>Donor/Desynced</code>, or <code>Joining</code>. Sending traffic to a <code>Joining</code> node means talking to a node with stale or incomplete data.</li>
</ul>



<p class="wp-block-paragraph">The standard pattern is: send <strong>all writes</strong> to a single node at a time (or a small write pool), and <strong>distribute reads</strong> across all healthy, synced nodes. The load balancer needs to know, in near real time, which nodes are actually fit to serve traffic.</p>



<h2 class="wp-block-heading">Architecture Overview</h2>



<pre class="wp-block-code"><code>                +----------------+
   Clients ---&gt; |    HAProxy     |
                +----------------+
                 |      |      |
             node1   node2   node3
            (Galera) (Galera) (Galera)
</code></pre>



<p class="wp-block-paragraph">HAProxy polls each node&#8217;s health-check port. That port is served by <code>clustercheck</code>, a small script that queries <code>wsrep_local_state</code> and returns HTTP 200 (healthy) or 503 (unhealthy) accordingly. This is what makes the balancing &#8220;dynamic&#8221; — HAProxy doesn&#8217;t need a static list of &#8220;good&#8221; nodes; it discovers node health continuously.</p>



<h2 class="wp-block-heading">Step 1: Install <code>clustercheck</code> on Each Galera Node</h2>



<p class="wp-block-paragraph"><code>clustercheck</code> ships with Percona XtraDB Cluster and MariaDB Galera packages. If you&#8217;re using it standalone:</p>



<pre class="wp-block-code"><code># On each Galera node
sudo apt-get install -y percona-xtradb-cluster-client xinetd
</code></pre>



<p class="wp-block-paragraph">Create a MySQL user the health check can use:</p>



<pre class="wp-block-code"><code>CREATE USER 'clustercheckuser'@'localhost' IDENTIFIED BY 'ClusterCheckPassword123!';
GRANT PROCESS ON *.* TO 'clustercheckuser'@'localhost';
FLUSH PRIVILEGES;
</code></pre>



<p class="wp-block-paragraph">Edit <code>/etc/sysconfig/clustercheck</code> (or <code>/etc/default/clustercheck</code> on Debian-based systems):</p>



<pre class="wp-block-code"><code>MYSQL_USERNAME="clustercheckuser"
MYSQL_PASSWORD="ClusterCheckPassword123!"
MYSQL_HOST="localhost"
MYSQL_PORT="3306"
AVAILABLE_WHEN_DONOR=0
AVAILABLE_WHEN_READONLY=1
AVAILABLE_WHEN_DONOR_IS_MASTER=0
</code></pre>



<p class="wp-block-paragraph"><code>AVAILABLE_WHEN_DONOR=0</code> is important — you generally don&#8217;t want a node acting as an SST/IST donor to also be taking production traffic, since it&#8217;s under extra load and possibly desynced.</p>



<p class="wp-block-paragraph">Configure <code>xinetd</code> to expose this as an HTTP-style health check on port 9200:</p>



<pre class="wp-block-code"><code>cat &lt;&lt;'EOF' | sudo tee /etc/xinetd.d/mysqlchk
service mysqlchk
{
  disable         = no
  flags           = REUSE
  socket_type     = stream
  port            = 9200
  wait            = no
  user            = nobody
  server          = /usr/bin/clustercheck
  log_on_failure  += USERID
  only_from       = 0.0.0.0/0
  per_source      = UNLIMITED
}
EOF

sudo systemctl restart xinetd
sudo systemctl enable xinetd
</code></pre>



<p class="wp-block-paragraph">Test it locally:</p>



<pre class="wp-block-code"><code>curl -i http://localhost:9200/
</code></pre>



<p class="wp-block-paragraph">Expected output for a healthy, synced node:</p>



<pre class="wp-block-code"><code>HTTP/1.1 200 OK
Content-Type: text/plain
Connection: close
Content-Length: 40

Percona XtraDB Cluster Node is synced.
</code></pre>



<p class="wp-block-paragraph">An unhealthy node returns <code>HTTP/1.1 503 Service Unavailable</code>.</p>



<h2 class="wp-block-heading">Step 2: Configure HAProxy for Dynamic Health-Based Routing</h2>



<p class="wp-block-paragraph">Install HAProxy on your load balancer host:</p>



<pre class="wp-block-code"><code>sudo apt-get install -y haproxy
</code></pre>



<p class="wp-block-paragraph"><code>/etc/haproxy/haproxy.cfg</code>:</p>



<pre class="wp-block-code"><code>global
    log /dev/log local0
    maxconn 4096

defaults
    log     global
    mode    tcp
    option  tcplog
    timeout connect 5s
    timeout client  30s
    timeout server  30s
    retries 3

# Write traffic: single active node, automatic failover
frontend galera_write_front
    bind *:3307
    default_backend galera_write_back

backend galera_write_back
    option httpchk
    http-check expect status 200
    default-server port 9200 inter 2s fall 3 rise 2
    server node1 10.0.0.11:3306 check
    server node2 10.0.0.12:3306 check backup
    server node3 10.0.0.13:3306 check backup

# Read traffic: distributed across all synced nodes
frontend galera_read_front
    bind *:3308
    default_backend galera_read_back

backend galera_read_back
    balance leastconn
    option httpchk
    http-check expect status 200
    default-server port 9200 inter 2s fall 3 rise 2
    server node1 10.0.0.11:3306 check
    server node2 10.0.0.12:3306 check
    server node3 10.0.0.13:3306 check

listen stats
    bind *:8404
    stats enable
    stats uri /stats
    stats refresh 5s
</code></pre>



<p class="wp-block-paragraph">What&#8217;s happening here:</p>



<ul class="wp-block-list">
<li><strong>Write backend</strong>: <code>node1</code> is primary; <code>node2</code> and <code>node3</code> are <code>backup</code>, only receiving traffic if <code>node1</code> fails its health check. This avoids multi-node write conflicts.</li>



<li><strong>Read backend</strong>: <code>balance leastconn</code> spreads read connections across whichever nodes are currently passing the <code>clustercheck</code> health probe.</li>



<li><code>inter 2s fall 3 rise 2</code> means HAProxy checks every 2 seconds, marks a server down after 3 consecutive failures, and marks it back up after 2 consecutive successes — this is the &#8220;dynamic&#8221; part: no manual intervention needed when a node drops or rejoins.</li>
</ul>



<p class="wp-block-paragraph">Reload HAProxy after any config change:</p>



<pre class="wp-block-code"><code>sudo systemctl reload haproxy
</code></pre>



<p class="wp-block-paragraph">Verify with the built-in stats page at <code>http://&lt;lb-ip&gt;:8404/stats</code> — you&#8217;ll see live UP/DOWN status per backend server.</p>



<h2 class="wp-block-heading">Step 3: Simulate a Failover to Prove It&#8217;s Dynamic</h2>



<p class="wp-block-paragraph">Stop MySQL on the current write-primary:</p>



<pre class="wp-block-code"><code># on node1
sudo systemctl stop mysql
</code></pre>



<p class="wp-block-paragraph">Within a few seconds, <code>curl http://node1:9200/</code> will return connection refused, HAProxy will mark <code>node1</code> down, and traffic on port 3307 will automatically shift to <code>node2</code> (the next available backup server) — no config reload needed.</p>



<p class="wp-block-paragraph">Check HAProxy logs to confirm:</p>



<pre class="wp-block-code"><code>sudo tail -f /var/log/haproxy.log
</code></pre>



<p class="wp-block-paragraph">Expected output snippet:</p>



<pre class="wp-block-code"><code>Server galera_write_back/node1 is DOWN, reason: Layer4 connection problem
Server galera_write_back/node2 is UP
</code></pre>



<p class="wp-block-paragraph">Bring <code>node1</code> back and let Galera perform IST/SST to resync it — once <code>clustercheck</code> reports 200 again, HAProxy will automatically re-add it to the pool.</p>



<h2 class="wp-block-heading">Alternative: ProxySQL for Smarter Query-Aware Routing</h2>



<p class="wp-block-paragraph">HAProxy operates at TCP/HTTP-check level and doesn&#8217;t understand SQL. For more advanced routing (query splitting, read/write splitting based on query pattern, connection multiplexing), <strong>ProxySQL</strong> is the more common modern choice for Galera:</p>



<pre class="wp-block-code"><code>docker run -d --name proxysql \
  -p 6033:6033 -p 6032:6032 \
  -v proxysql-data:/var/lib/proxysql \
  proxysql/proxysql:latest
</code></pre>



<p class="wp-block-paragraph">ProxySQL maintains a <code>mysql_galera_hostgroups</code> table that natively understands <code>wsrep_local_state</code>, so it can dynamically detect donor/joining/synced states without needing an external xinetd health-check port at all. If you&#8217;re building a new deployment, it&#8217;s worth evaluating instead of HAProxy — but HAProxy remains the simpler, more transparent, and widely documented option, which is why it&#8217;s the default in most tutorials.</p>



<h2 class="wp-block-heading">Security Considerations</h2>



<ul class="wp-block-list">
<li>Restrict the <code>clustercheck</code> health-check port (9200) to only the load balancer&#8217;s IP using firewall rules (<code>ufw</code>, <code>iptables</code>, or security groups) — don&#8217;t expose it publicly.</li>



<li>Use a dedicated, minimally privileged MySQL user for health checks (<code>PROCESS</code> privilege only, no data access).</li>



<li>Terminate TLS at HAProxy if clients connect over the internet, and use <code>sslmode=require</code> (or stricter) for the backend MySQL connections if they traverse untrusted networks.</li>



<li>Keep the HAProxy stats page behind authentication:</li>
</ul>



<pre class="wp-block-code"><code>listen stats
    bind *:8404
    stats enable
    stats uri /stats
    stats auth admin:StrongAdminPassword123!
</code></pre>



<h2 class="wp-block-heading">Monitoring and Troubleshooting</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Symptom</th><th>Likely Cause</th><th>Fix</th></tr></thead><tbody><tr><td>All backends show DOWN</td><td><code>xinetd</code>/<code>clustercheck</code> not running or firewalled</td><td><code>systemctl status xinetd</code>, check port 9200 reachability</td></tr><tr><td>Writes fail after node1 restarts</td><td>Node still in <code>Joining</code> state (SST in progress)</td><td>Wait for SST/IST to finish; check <code>wsrep_local_state_comment</code></td></tr><tr><td>Reads return stale data</td><td><code>AVAILABLE_WHEN_DONOR=1</code> allowing donor traffic</td><td>Set <code>AVAILABLE_WHEN_DONOR=0</code></td></tr><tr><td>HAProxy flapping a node</td><td><code>inter</code>/<code>fall</code>/<code>rise</code> too aggressive for network jitter</td><td>Increase <code>inter</code> to 3-5s, <code>fall</code> to 3-5</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">For ongoing monitoring, feed HAProxy&#8217;s stats socket into Prometheus using the <code>haproxy_exporter</code>, and track <code>wsrep_cluster_size</code>, <code>wsrep_local_state</code>, and <code>wsrep_flow_control_paused</code> from each node via <code>mysqld_exporter</code> for Galera-specific metrics.</p>



<h2 class="wp-block-heading">Real-World Deployment Notes</h2>



<p class="wp-block-paragraph">In practice, I&#8217;ve found the single-writer pattern shown above works well for most OLTP workloads under moderate write volume, but it does concentrate write load on one box. For higher write throughput, some teams route writes to all three nodes and accept occasional certification-conflict rollbacks at the application layer (with retry logic on deadlock/conflict errors), trading a bit of complexity for horizontal write scaling. Whichever pattern you pick, keep the health-check interval tight enough to fail over quickly but not so tight that transient network blips cause flapping — 2 seconds with a 3-strike fall threshold has been a reliable middle ground across the clusters I&#8217;ve run. It&#8217;s also worth putting HAProxy itself behind Keepalived or a cloud load balancer with a floating VIP, so the load balancer isn&#8217;t a new single point of failure sitting in front of a cluster you built specifically to avoid single points of failure.</p>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph">Dynamically load balancing a Galera cluster comes down to one core idea: never route traffic based on a static assumption of node health. By exposing <code>wsrep_local_state</code> through a lightweight <code>clustercheck</code> HTTP endpoint and letting HAProxy poll it continuously, your load balancer reacts to real cluster state — donor nodes get excluded from traffic, failed nodes get removed within seconds, and recovered nodes get automatically re-added. Combine that with a single-writer, multi-reader traffic split and you get a setup that survives node failures without a 2am page.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://galeracluster.com/library/documentation/">Galera Cluster Documentation</a></li>



<li><a href="https://docs.percona.com/percona-xtradb-cluster/8.0/failover.html">Percona XtraDB Cluster – clustercheck</a></li>



<li><a href="https://docs.haproxy.org/">HAProxy Configuration Manual</a></li>



<li><a href="https://proxysql.com/documentation/">ProxySQL Documentation</a></li>



<li><a href="https://mariadb.com/kb/en/galera-cluster/">MariaDB Galera Cluster Docs</a></li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/docker/how-to-dynamically-configure-a-load-balancer-for-a-mysql-galera-cluster-complete-guide/">How to Dynamically Configure a Load Balancer for a MySQL Galera Cluster: Complete Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/docker/how-to-dynamically-configure-a-load-balancer-for-a-mysql-galera-cluster-complete-guide/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4673</post-id>	</item>
		<item>
		<title>How to Build a MySQL Galera Cluster on a Docker Network: Step-by-Step Setup Guide</title>
		<link>https://awjunaid.com/docker/how-to-build-a-mysql-galera-cluster-on-a-docker-network-step-by-step-setup-guide/</link>
					<comments>https://awjunaid.com/docker/how-to-build-a-mysql-galera-cluster-on-a-docker-network-step-by-step-setup-guide/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sat, 02 Sep 2023 06:10:48 +0000</pubDate>
				<category><![CDATA[Docker]]></category>
		<category><![CDATA[docker]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=4670</guid>

					<description><![CDATA[<p>The first time I tried running Galera in Docker, I made the classic mistake: I treated it like&#8230;</p>
<p>The post <a href="https://awjunaid.com/docker/how-to-build-a-mysql-galera-cluster-on-a-docker-network-step-by-step-setup-guide/">How to Build a MySQL Galera Cluster on a Docker Network: Step-by-Step Setup Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">The first time I tried running Galera in Docker, I made the classic mistake: I treated it like any other stateless container and expected <code>docker run</code> three times with the same image to just work. It doesn&#8217;t — Galera has strict requirements around cluster bootstrapping order, persistent storage, and network addressing that trip up a lot of people moving from bare-metal or VM deployments into containers. This guide walks through building a real three-node Galera cluster on a user-defined Docker network, the right way.</p>



<h2 class="wp-block-heading">What You&#8217;ll Need to Understand First</h2>



<p class="wp-block-paragraph">Before touching Docker, it helps to know what makes Galera different from vanilla MySQL replication:</p>



<ul class="wp-block-list">
<li><strong>Synchronous multi-master replication</strong>: every node in the cluster certifies transactions before committing, using the Write-Set Replication (wsrep) API.</li>



<li><strong>Cluster bootstrap</strong>: the very first node must be started with <code>--wsrep-new-cluster</code> (or <code>wsrep_cluster_address=gcomm://</code>), because it has nothing to join. Every subsequent node joins by pointing at existing members.</li>



<li><strong>State Snapshot Transfer (SST)</strong>: when a new node joins, it needs a full copy of the dataset. This typically happens via <code>mariabackup</code>/<code>xtrabackup</code> (physical) or <code>mysqldump</code> (logical).</li>



<li><strong>Quorum</strong>: Galera needs an odd number of nodes (3, 5, &#8230;) to avoid split-brain — with only 2 nodes, a single node failure can&#8217;t determine if it&#8217;s the majority.</li>
</ul>



<p class="wp-block-paragraph">In Docker terms, this means node startup order matters, and each node needs a stable, resolvable hostname — which is exactly what a user-defined Docker network gives you via Docker&#8217;s embedded DNS.</p>



<h2 class="wp-block-heading">Step 1: Create a User-Defined Docker Network</h2>



<p class="wp-block-paragraph">Docker&#8217;s default bridge network doesn&#8217;t provide automatic DNS resolution between containers by name — a user-defined network does. This is the foundation the whole cluster depends on.</p>



<pre class="wp-block-code"><code>docker network create --driver bridge galera-net
</code></pre>



<p class="wp-block-paragraph">Verify:</p>



<pre class="wp-block-code"><code>docker network inspect galera-net
</code></pre>



<p class="wp-block-paragraph">Expected output (trimmed):</p>



<pre class="wp-block-code"><code>&#91;
    {
        "Name": "galera-net",
        "Driver": "bridge",
        "IPAM": {
            "Config": &#91;{"Subnet": "172.20.0.0/16"}]
        },
        "Containers": {}
    }
]
</code></pre>



<h2 class="wp-block-heading">Step 2: Create Persistent Volumes for Each Node</h2>



<p class="wp-block-paragraph">Never rely on container-internal storage for database data — a container recreation will wipe it. Create a named volume per node:</p>



<pre class="wp-block-code"><code>docker volume create galera-node1-data
docker volume create galera-node2-data
docker volume create galera-node3-data
</code></pre>



<h2 class="wp-block-heading">Step 3: Write a Shared Galera Configuration File</h2>



<p class="wp-block-paragraph">Create a local directory with a <code>galera.cnf</code> that all three nodes will mount:</p>



<pre class="wp-block-code"><code>mkdir -p ~/galera-cluster/conf
</code></pre>



<p class="wp-block-paragraph"><code>~/galera-cluster/conf/galera.cnf</code>:</p>



<pre class="wp-block-code"><code>&#91;mysqld]
binlog_format=ROW
default-storage-engine=InnoDB
innodb_autoinc_lock_mode=2
bind-address=0.0.0.0

wsrep_on=ON
wsrep_provider=/usr/lib/galera/libgalera_smm.so
wsrep_cluster_name="docker_galera_cluster"
wsrep_cluster_address="gcomm://galera-node1,galera-node2,galera-node3"
wsrep_sst_method=mariabackup
wsrep_sst_auth="sstuser:SstPassword123!"
</code></pre>



<p class="wp-block-paragraph">Note the <code>wsrep_cluster_address</code> uses the container <strong>names</strong> — this works because Docker&#8217;s embedded DNS resolves them automatically inside <code>galera-net</code>.</p>



<h2 class="wp-block-heading">Step 4: Start Node 1 — Bootstrap the Cluster</h2>



<p class="wp-block-paragraph">The first node has no cluster to join, so it starts a brand-new one:</p>



<pre class="wp-block-code"><code>docker run -d \
  --name galera-node1 \
  --network galera-net \
  --hostname galera-node1 \
  -e MYSQL_ROOT_PASSWORD=RootPassword123! \
  -v galera-node1-data:/var/lib/mysql \
  -v ~/galera-cluster/conf/galera.cnf:/etc/mysql/conf.d/galera.cnf \
  -p 3306:3306 \
  mariadb:11.4 \
  --wsrep-new-cluster
</code></pre>



<p class="wp-block-paragraph">Check that it started cleanly:</p>



<pre class="wp-block-code"><code>docker logs -f galera-node1
</code></pre>



<p class="wp-block-paragraph">Expected output includes:</p>



<pre class="wp-block-code"><code>&#91;Note] WSREP: Synchronized with group, ready for connections
&#91;Note] mariadbd: ready for connections.
</code></pre>



<p class="wp-block-paragraph">Create the SST user (required for future nodes to sync from this one):</p>



<pre class="wp-block-code"><code>docker exec -it galera-node1 mariadb -uroot -pRootPassword123! -e \
  "CREATE USER 'sstuser'@'%' IDENTIFIED BY 'SstPassword123!'; \
   GRANT RELOAD, LOCK TABLES, PROCESS, REPLICATION CLIENT ON *.* TO 'sstuser'@'%'; \
   FLUSH PRIVILEGES;"
</code></pre>



<h2 class="wp-block-heading">Step 5: Start Nodes 2 and 3 — Join the Cluster</h2>



<p class="wp-block-paragraph">These nodes join normally (no <code>--wsrep-new-cluster</code> flag):</p>



<pre class="wp-block-code"><code>docker run -d \
  --name galera-node2 \
  --network galera-net \
  --hostname galera-node2 \
  -e MYSQL_ROOT_PASSWORD=RootPassword123! \
  -v galera-node2-data:/var/lib/mysql \
  -v ~/galera-cluster/conf/galera.cnf:/etc/mysql/conf.d/galera.cnf \
  mariadb:11.4

docker run -d \
  --name galera-node3 \
  --network galera-net \
  --hostname galera-node3 \
  -e MYSQL_ROOT_PASSWORD=RootPassword123! \
  -v galera-node3-data:/var/lib/mysql \
  -v ~/galera-cluster/conf/galera.cnf:/etc/mysql/conf.d/galera.cnf \
  mariadb:11.4
</code></pre>



<p class="wp-block-paragraph">Watch node2 perform its SST:</p>



<pre class="wp-block-code"><code>docker logs -f galera-node2
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>&#91;Note] WSREP: State transfer required: Group state: ..., Local state: ...
&#91;Note] WSREP: Requesting state transfer: success
&#91;Note] WSREP: SST complete, seqno: X
&#91;Note] mariadbd: ready for connections.
</code></pre>



<h2 class="wp-block-heading">Step 6: Verify Cluster Membership</h2>



<p class="wp-block-paragraph">From any node:</p>



<pre class="wp-block-code"><code>docker exec -it galera-node1 mariadb -uroot -pRootPassword123! -e \
  "SHOW STATUS LIKE 'wsrep_cluster_size'; SHOW STATUS LIKE 'wsrep_local_state_comment';"
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| wsrep_cluster_size | 3     |
+--------------------+-------+
+----------------------------+--------+
| Variable_name              | Value  |
+----------------------------+--------+
| wsrep_local_state_comment  | Synced |
+----------------------------+--------+
</code></pre>



<p class="wp-block-paragraph">Test replication by writing to node1 and reading from node3:</p>



<pre class="wp-block-code"><code>docker exec -it galera-node1 mariadb -uroot -pRootPassword123! -e \
  "CREATE DATABASE testdb; USE testdb; CREATE TABLE t1 (id INT PRIMARY KEY); INSERT INTO t1 VALUES (1);"

docker exec -it galera-node3 mariadb -uroot -pRootPassword123! -e \
  "SELECT * FROM testdb.t1;"
</code></pre>



<p class="wp-block-paragraph">Expected output on node3:</p>



<pre class="wp-block-code"><code>+----+
| id |
+----+
|  1 |
+----+
</code></pre>



<p class="wp-block-paragraph">If that row appears on node3 instantly, replication across the Docker network is working.</p>



<h2 class="wp-block-heading">Doing It the Right Way: Docker Compose</h2>



<p class="wp-block-paragraph">Manually running three <code>docker run</code> commands doesn&#8217;t scale for real environments. Here&#8217;s the same cluster as a Compose file:</p>



<pre class="wp-block-code"><code># docker-compose.yml
version: "3.8"

networks:
  galera-net:
    driver: bridge

volumes:
  node1-data:
  node2-data:
  node3-data:

services:
  galera-node1:
    image: mariadb:11.4
    container_name: galera-node1
    hostname: galera-node1
    networks:
      - galera-net
    environment:
      MYSQL_ROOT_PASSWORD: RootPassword123!
    volumes:
      - node1-data:/var/lib/mysql
      - ./conf/galera.cnf:/etc/mysql/conf.d/galera.cnf
    command: &#91;"--wsrep-new-cluster"]
    ports:
      - "3306:3306"

  galera-node2:
    image: mariadb:11.4
    container_name: galera-node2
    hostname: galera-node2
    networks:
      - galera-net
    environment:
      MYSQL_ROOT_PASSWORD: RootPassword123!
    volumes:
      - node2-data:/var/lib/mysql
      - ./conf/galera.cnf:/etc/mysql/conf.d/galera.cnf
    depends_on:
      - galera-node1

  galera-node3:
    image: mariadb:11.4
    container_name: galera-node3
    hostname: galera-node3
    networks:
      - galera-net
    environment:
      MYSQL_ROOT_PASSWORD: RootPassword123!
    volumes:
      - node3-data:/var/lib/mysql
      - ./conf/galera.cnf:/etc/mysql/conf.d/galera.cnf
    depends_on:
      - galera-node1
</code></pre>



<p class="wp-block-paragraph">Important caveat: <code>depends_on</code> only waits for the container to <em>start</em>, not for MySQL/Galera to be ready. In production Compose or Swarm setups, add a healthcheck and <code>condition: service_healthy</code>:</p>



<pre class="wp-block-code"><code>    healthcheck:
      test: &#91;"CMD", "mariadb-admin", "ping", "-uroot", "-pRootPassword123!"]
      interval: 5s
      timeout: 5s
      retries: 10
</code></pre>



<p class="wp-block-paragraph">Bring it up:</p>



<pre class="wp-block-code"><code>docker compose up -d
docker compose logs -f
</code></pre>



<h2 class="wp-block-heading">Internal Architecture: What&#8217;s Actually Happening on the Network</h2>



<p class="wp-block-paragraph">Each container gets its own network namespace on <code>galera-net</code>, with an internal IP in the subnet Docker assigned (e.g., <code>172.20.0.2</code>). Docker runs an embedded DNS server at <code>127.0.0.11</code> inside each container, which resolves <code>galera-node1</code>, <code>galera-node2</code>, <code>galera-node3</code> to those internal IPs — this is what lets <code>wsrep_cluster_address</code> use hostnames instead of hardcoded IPs, which would break every time you recreate the containers.</p>



<p class="wp-block-paragraph">Galera itself uses several ports:</p>



<ul class="wp-block-list">
<li><strong>3306</strong>: standard MySQL client connections</li>



<li><strong>4567</strong>: Galera Cluster replication traffic (gcomm)</li>



<li><strong>4568</strong>: Incremental State Transfer (IST)</li>



<li><strong>4444</strong>: State Snapshot Transfer (SST)</li>
</ul>



<p class="wp-block-paragraph">All of these need to be reachable <em>between containers</em> (handled automatically by the bridge network) but generally should <strong>not</strong> be exposed to the host/public internet except 3306, and even that ideally only through your load balancer.</p>



<h2 class="wp-block-heading">Storage and Data Persistence</h2>



<p class="wp-block-paragraph">Named volumes (<code>galera-node1-data</code>, etc.) live under <code>/var/lib/docker/volumes/</code> on the host and persist independently of container lifecycle. This means you can <code>docker compose down</code> and <code>docker compose up -d</code> again without losing data — but be careful: <code>docker compose down -v</code> <strong>deletes volumes</strong>, which will destroy your cluster&#8217;s data permanently. Always back up before running destructive commands.</p>



<h2 class="wp-block-heading">Security Best Practices</h2>



<ul class="wp-block-list">
<li>Never bake passwords into the image; use environment variables or Docker secrets (<code>docker secret create</code> in Swarm mode).</li>



<li>Restrict the SST user&#8217;s grants to only what&#8217;s required (<code>RELOAD, LOCK TABLES, PROCESS, REPLICATION CLIENT</code>), not <code>ALL PRIVILEGES</code>.</li>



<li>Don&#8217;t expose ports 4567/4568/4444 outside the Docker network — only container-to-container traffic needs them.</li>



<li>Use <code>docker network create --internal galera-net</code> if you want a fully isolated network with no external routing, and put a reverse proxy/load balancer container on a second, external-facing network instead.</li>
</ul>



<h2 class="wp-block-heading">Troubleshooting</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Problem</th><th>Cause</th><th>Resolution</th></tr></thead><tbody><tr><td>Node2/3 stuck in &#8220;Joining&#8221;</td><td>SST failed or <code>sstuser</code> grants missing</td><td>Check <code>docker logs galera-node2</code> for SST errors; verify <code>sstuser</code> credentials match <code>galera.cnf</code></td></tr><tr><td><code>wsrep_cluster_size</code> = 1 on each node</td><td>Nodes can&#8217;t resolve each other&#8217;s hostnames</td><td>Confirm all containers are on the same user-defined network, not default bridge</td></tr><tr><td>Cluster won&#8217;t bootstrap after crash</td><td>Old <code>grastate.dat</code> marks cluster as unsafe to bootstrap</td><td>Inspect <code>/var/lib/mysql/grastate.dat</code>; if <code>safe_to_bootstrap: 0</code>, manually set to <code>1</code> on the node with the most recent data only</td></tr><tr><td>Port 3306 already in use on host</td><td>Another MySQL instance running locally</td><td>Change host port mapping, e.g. <code>-p 3307:3306</code></td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Real-World Deployment Notes</h2>



<p class="wp-block-paragraph">For anything beyond local testing, I&#8217;d strongly recommend running each Galera node on a <strong>separate host</strong> with Docker installed, connected via an overlay network (Docker Swarm mode or a CNI-based setup) rather than three containers on one machine — colocating all three nodes on a single host defeats the fault-tolerance purpose of Galera entirely, since one host failure takes down the whole cluster. In Swarm mode, <code>docker network create --driver overlay --attachable galera-net</code> gives you the same hostname-resolution behavior across hosts that the single-host bridge network gives you locally. I&#8217;ve also found it worth pinning exact image digests (not just <code>mariadb:11.4</code>) in any environment where reproducibility matters, since a silent minor-version bump across node restarts can occasionally introduce subtle SST incompatibilities between nodes running different patch versions.</p>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph">Running Galera in Docker isn&#8217;t fundamentally different from running it on bare metal — the wsrep protocol doesn&#8217;t care what&#8217;s hosting it — but Docker&#8217;s networking model changes <em>how</em> nodes discover each other, and container lifecycle changes <em>how</em> you think about data persistence. Get the user-defined network, named volumes, and bootstrap ordering right, and the rest is standard Galera administration.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://docs.docker.com/network/">Docker Networking Overview</a></li>



<li><a href="https://galeracluster.com/library/documentation/">Galera Cluster Documentation</a></li>



<li><a href="https://hub.docker.com/_/mariadb">MariaDB Official Docker Image</a></li>



<li><a href="https://docs.docker.com/compose/compose-file/">Docker Compose File Reference</a></li>



<li><a href="https://mariadb.com/kb/en/getting-started-with-mariadb-galera-cluster/">MariaDB Galera Cluster Deployment Guide</a></li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/docker/how-to-build-a-mysql-galera-cluster-on-a-docker-network-step-by-step-setup-guide/">How to Build a MySQL Galera Cluster on a Docker Network: Step-by-Step Setup Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/docker/how-to-build-a-mysql-galera-cluster-on-a-docker-network-step-by-step-setup-guide/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4670</post-id>	</item>
		<item>
		<title>How to Build an S3-Compatible Object Store with Cassandra on Kubernetes: Complete Guide</title>
		<link>https://awjunaid.com/docker/how-to-build-an-s3-compatible-object-store-with-cassandra-on-kubernetes-complete-guide/</link>
					<comments>https://awjunaid.com/docker/how-to-build-an-s3-compatible-object-store-with-cassandra-on-kubernetes-complete-guide/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sat, 02 Sep 2023 06:08:53 +0000</pubDate>
				<category><![CDATA[Docker]]></category>
		<category><![CDATA[docker]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=4667</guid>

					<description><![CDATA[<p>I&#8217;ll be upfront about something most tutorials on this topic gloss over: Cassandra isn&#8217;t natively an object store,&#8230;</p>
<p>The post <a href="https://awjunaid.com/docker/how-to-build-an-s3-compatible-object-store-with-cassandra-on-kubernetes-complete-guide/">How to Build an S3-Compatible Object Store with Cassandra on Kubernetes: Complete Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I&#8217;ll be upfront about something most tutorials on this topic gloss over: Cassandra isn&#8217;t natively an object store, and there&#8217;s no mainstream open-source project that uses Cassandra as its <em>primary</em> backend for full S3 semantics the way MinIO uses erasure-coded drives. What people actually mean — and what I&#8217;ve built in practice — is a metadata + chunked-blob storage layer on top of Cassandra, fronted by an S3-compatible API gateway. That&#8217;s a legitimate and genuinely useful pattern (it&#8217;s close to what early versions of things like OpenStack Swift-on-Cassandra experiments and several homegrown CDN backends have done), so that&#8217;s exactly what I&#8217;ll walk through: a real, working architecture, not a fictional &#8220;Cassandra has an S3 mode&#8221; shortcut.</p>



<h2 class="wp-block-heading">The Architecture</h2>



<pre class="wp-block-code"><code>                     +-------------------+
   S3 clients  ---&gt;  |  Gateway (API)    |
  (aws-cli, SDKs)     |  S3-compatible    |
                     +-------------------+
                        |            |
                 +-------------+  +----------------+
                 |  Cassandra  |  | Object chunks   |
                 | (metadata + |  | (stored as blobs|
                 |  small obj) |  |  in Cassandra   |
                 +-------------+  |  blob tables)   |
                                  +----------------+
                     All running as StatefulSets/Deployments in Kubernetes
</code></pre>



<p class="wp-block-paragraph">We&#8217;ll deploy:</p>



<ol class="wp-block-list">
<li>A <strong>Cassandra StatefulSet</strong> (3 nodes) for metadata and object-chunk storage.</li>



<li>A lightweight <strong>S3 gateway</strong> service that translates S3 REST calls into Cassandra reads/writes (implemented conceptually — I&#8217;ll show the CQL schema and a minimal Node.js gateway you can extend).</li>



<li><strong>Kubernetes networking</strong> (headless Service for Cassandra gossip, ClusterIP for the gateway, Ingress for external S3 access).</li>
</ol>



<p class="wp-block-paragraph">If you want a production-grade, fully S3-compliant system without building the gateway yourself, the honest recommendation is MinIO or Ceph RGW — I&#8217;ll note that in best practices below. But if your goal is specifically &#8220;S3-compatible API in front of Cassandra-backed storage&#8221; (common in multi-region, tunable-consistency use cases), here&#8217;s how it&#8217;s built.</p>



<h2 class="wp-block-heading">Step 1: Namespace and Storage Class</h2>



<pre class="wp-block-code"><code>kubectl create namespace object-store
</code></pre>



<p class="wp-block-paragraph">Confirm you have a <code>StorageClass</code> that supports dynamic provisioning (cloud environments usually do by default):</p>



<pre class="wp-block-code"><code>kubectl get storageclass
</code></pre>



<p class="wp-block-paragraph">Expected output (varies by provider):</p>



<pre class="wp-block-code"><code>NAME                 PROVISIONER             RECLAIMPOLICY
standard (default)   kubernetes.io/gce-pd     Delete
</code></pre>



<h2 class="wp-block-heading">Step 2: Headless Service for Cassandra Gossip</h2>



<p class="wp-block-paragraph">Cassandra nodes discover each other via gossip, which requires stable network identities — a headless Service (<code>clusterIP: None</code>) gives each pod a predictable DNS name.</p>



<pre class="wp-block-code"><code># cassandra-headless-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: cassandra
  namespace: object-store
  labels:
    app: cassandra
spec:
  clusterIP: None
  ports:
    - port: 7000
      name: intra-node
    - port: 7001
      name: tls-intra-node
    - port: 7199
      name: jmx
    - port: 9042
      name: cql
  selector:
    app: cassandra
</code></pre>



<pre class="wp-block-code"><code>kubectl apply -f cassandra-headless-svc.yaml
</code></pre>



<h2 class="wp-block-heading">Step 3: Cassandra StatefulSet</h2>



<pre class="wp-block-code"><code># cassandra-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: cassandra
  namespace: object-store
spec:
  serviceName: cassandra
  replicas: 3
  selector:
    matchLabels:
      app: cassandra
  template:
    metadata:
      labels:
        app: cassandra
    spec:
      terminationGracePeriodSeconds: 1800
      containers:
        - name: cassandra
          image: cassandra:5.0
          ports:
            - containerPort: 7000
              name: intra-node
            - containerPort: 7199
              name: jmx
            - containerPort: 9042
              name: cql
          resources:
            requests:
              cpu: "1"
              memory: 2Gi
            limits:
              cpu: "2"
              memory: 4Gi
          env:
            - name: CASSANDRA_SEEDS
              value: "cassandra-0.cassandra.object-store.svc.cluster.local"
            - name: CASSANDRA_CLUSTER_NAME
              value: "ObjectStoreCluster"
            - name: CASSANDRA_DC
              value: "dc1"
            - name: CASSANDRA_RACK
              value: "rack1"
            - name: CASSANDRA_ENDPOINT_SNITCH
              value: "GossipingPropertyFileSnitch"
            - name: POD_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
          readinessProbe:
            exec:
              command: &#91;"/bin/bash", "-c", "nodetool status | grep -E \"^UN\\s+${POD_IP}\""]
            initialDelaySeconds: 60
            periodSeconds: 15
          volumeMounts:
            - name: cassandra-data
              mountPath: /var/lib/cassandra
  volumeClaimTemplates:
    - metadata:
        name: cassandra-data
      spec:
        accessModes: &#91;"ReadWriteOnce"]
        resources:
          requests:
            storage: 20Gi
</code></pre>



<pre class="wp-block-code"><code>kubectl apply -f cassandra-statefulset.yaml
kubectl get pods -n object-store -w
</code></pre>



<p class="wp-block-paragraph">Expected output as nodes come up one by one (StatefulSets deploy sequentially):</p>



<pre class="wp-block-code"><code>NAME          READY   STATUS    RESTARTS   AGE
cassandra-0   1/1     Running   0          2m
cassandra-1   1/1     Running   0          90s
cassandra-2   1/1     Running   0          40s
</code></pre>



<p class="wp-block-paragraph">Verify cluster formation:</p>



<pre class="wp-block-code"><code>kubectl exec -it cassandra-0 -n object-store -- nodetool status
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>Datacenter: dc1
===============
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
--  Address      Load      Tokens  Owns   Host ID   Rack
UN  10.244.1.5   115 KiB   16      33.4%  ...       rack1
UN  10.244.2.7   102 KiB   16      33.3%  ...       rack1
UN  10.244.3.9   98 KiB    16      33.3%  ...       rack1
</code></pre>



<p class="wp-block-paragraph"><code>UN</code> means Up/Normal — all three nodes are healthy and joined.</p>



<h2 class="wp-block-heading">Step 4: Schema for Object Storage</h2>



<p class="wp-block-paragraph">Connect via <code>cqlsh</code>:</p>



<pre class="wp-block-code"><code>kubectl exec -it cassandra-0 -n object-store -- cqlsh
</code></pre>



<pre class="wp-block-code"><code>CREATE KEYSPACE object_store
WITH replication = {'class': 'NetworkTopologyStrategy', 'dc1': 3};

USE object_store;

CREATE TABLE buckets (
    bucket_name text PRIMARY KEY,
    created_at timestamp,
    owner text
);

CREATE TABLE objects (
    bucket_name text,
    object_key text,
    version_id timeuuid,
    size bigint,
    etag text,
    content_type text,
    created_at timestamp,
    chunk_count int,
    PRIMARY KEY ((bucket_name), object_key, version_id)
) WITH CLUSTERING ORDER BY (object_key ASC, version_id DESC);

CREATE TABLE object_chunks (
    bucket_name text,
    object_key text,
    version_id timeuuid,
    chunk_index int,
    data blob,
    PRIMARY KEY ((bucket_name, object_key, version_id), chunk_index)
);
</code></pre>



<p class="wp-block-paragraph">Design notes: objects are split into chunks (e.g., 1MB each) and stored as <code>blob</code> rows rather than one giant row, because Cassandra performs poorly with very large single cells/partitions. The <code>objects</code> table holds metadata; <code>object_chunks</code> holds the payload, partitioned so retrieval streams chunk-by-chunk.</p>



<h2 class="wp-block-heading">Step 5: Minimal S3-Compatible Gateway (Deployment)</h2>



<p class="wp-block-paragraph">A trimmed example gateway (Node.js + Express + <code>cassandra-driver</code>) exposing <code>PUT</code>/<code>GET</code> for objects, mapped to basic S3 REST semantics:</p>



<pre class="wp-block-code"><code>// gateway/index.js
const express = require("express");
const cassandra = require("cassandra-driver");
const { v1: uuidv1 } = require("uuid");

const client = new cassandra.Client({
  contactPoints: &#91;"cassandra-0.cassandra.object-store.svc.cluster.local"],
  localDataCenter: "dc1",
  keyspace: "object_store",
});

const app = express();
const CHUNK_SIZE = 1024 * 1024;

app.put("/:bucket/:key", express.raw({ type: "*/*", limit: "5gb" }), async (req, res) =&gt; {
  const { bucket, key } = req.params;
  const versionId = uuidv1();
  const data = req.body;
  const chunks = Math.ceil(data.length / CHUNK_SIZE);

  for (let i = 0; i &lt; chunks; i++) {
    const chunk = data.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
    await client.execute(
      "INSERT INTO object_chunks (bucket_name, object_key, version_id, chunk_index, data) VALUES (?, ?, ?, ?, ?)",
      &#91;bucket, key, versionId, i, chunk],
      { prepare: true }
    );
  }

  await client.execute(
    "INSERT INTO objects (bucket_name, object_key, version_id, size, chunk_count, created_at) VALUES (?, ?, ?, ?, ?, toTimestamp(now()))",
    &#91;bucket, key, versionId, data.length, chunks],
    { prepare: true }
  );

  res.set("ETag", versionId.toString()).status(200).end();
});

app.get("/:bucket/:key", async (req, res) =&gt; {
  const { bucket, key } = req.params;
  const meta = await client.execute(
    "SELECT * FROM objects WHERE bucket_name = ? AND object_key = ? LIMIT 1",
    &#91;bucket, key],
    { prepare: true }
  );
  if (meta.rowLength === 0) return res.status(404).end();

  const row = meta.first();
  for (let i = 0; i &lt; row.chunk_count; i++) {
    const chunkRes = await client.execute(
      "SELECT data FROM object_chunks WHERE bucket_name = ? AND object_key = ? AND version_id = ? AND chunk_index = ?",
      &#91;bucket, key, row.version_id, i],
      { prepare: true }
    );
    res.write(chunkRes.first().data);
  }
  res.end();
});

app.listen(8080, () =&gt; console.log("S3 gateway listening on 8080"));
</code></pre>



<p class="wp-block-paragraph"><code>Dockerfile</code>:</p>



<pre class="wp-block-code"><code>FROM node:20-slim
WORKDIR /app
COPY package.json .
RUN npm install --production
COPY index.js .
EXPOSE 8080
CMD &#91;"node", "index.js"]
</code></pre>



<p class="wp-block-paragraph">Build and push:</p>



<pre class="wp-block-code"><code>docker build -t your-registry/s3-cassandra-gateway:1.0 ./gateway
docker push your-registry/s3-cassandra-gateway:1.0
</code></pre>



<p class="wp-block-paragraph">Deployment manifest:</p>



<pre class="wp-block-code"><code># gateway-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: s3-gateway
  namespace: object-store
spec:
  replicas: 2
  selector:
    matchLabels:
      app: s3-gateway
  template:
    metadata:
      labels:
        app: s3-gateway
    spec:
      containers:
        - name: s3-gateway
          image: your-registry/s3-cassandra-gateway:1.0
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: "250m"
              memory: 256Mi
            limits:
              cpu: "500m"
              memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
  name: s3-gateway
  namespace: object-store
spec:
  selector:
    app: s3-gateway
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP
</code></pre>



<pre class="wp-block-code"><code>kubectl apply -f gateway-deployment.yaml
</code></pre>



<p class="wp-block-paragraph">Expose externally with an Ingress:</p>



<pre class="wp-block-code"><code>apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: s3-gateway-ingress
  namespace: object-store
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "5g"
spec:
  ingressClassName: nginx
  rules:
    - host: s3.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: s3-gateway
                port:
                  number: 80
</code></pre>



<h2 class="wp-block-heading">Step 6: Test With a Standard S3 Client</h2>



<pre class="wp-block-code"><code>kubectl port-forward svc/s3-gateway -n object-store 8080:80

curl -X PUT --data-binary @localfile.txt http://localhost:8080/my-bucket/localfile.txt
curl http://localhost:8080/my-bucket/localfile.txt -o downloaded.txt
diff localfile.txt downloaded.txt
</code></pre>



<p class="wp-block-paragraph">Expected: <code>diff</code> returns nothing, confirming round-trip integrity.</p>



<h2 class="wp-block-heading">Internals: Why This Works (and Where It Strains)</h2>



<p class="wp-block-paragraph">Cassandra&#8217;s <code>NetworkTopologyStrategy</code> with <code>RF=3</code> gives you the same durability model S3 relies on internally — every chunk is written to 3 nodes across the ring before being acknowledged (tunable via consistency level, e.g. <code>LOCAL_QUORUM</code>). Partitioning chunks by <code>(bucket, key, version_id)</code> spreads large objects across the cluster instead of hammering one node with a giant blob.</p>



<p class="wp-block-paragraph">Where this genuinely strains compared to purpose-built object stores like MinIO or Ceph: Cassandra wasn&#8217;t designed for large binary blobs, and very large objects (multi-GB) will pressure compaction and heap usage. In practice, teams doing this successfully cap object size, tune <code>compaction throughput</code>, and often route only <em>metadata</em> and <em>small objects</em> through Cassandra while large blobs go to a separate blob store — a hybrid model.</p>



<h2 class="wp-block-heading">Security</h2>



<ul class="wp-block-list">
<li>Enable Cassandra internode and client-to-node TLS via <code>cassandra.yaml</code> (<code>server_encryption_options</code>, <code>client_encryption_options</code>) — don&#8217;t run gossip traffic unencrypted across namespaces.</li>



<li>Use Kubernetes <code>NetworkPolicy</code> to restrict which pods can reach Cassandra&#8217;s CQL port (9042):</li>
</ul>



<pre class="wp-block-code"><code>apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: cassandra-allow-gateway-only
  namespace: object-store
spec:
  podSelector:
    matchLabels:
      app: cassandra
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: s3-gateway
      ports:
        - port: 9042
</code></pre>



<ul class="wp-block-list">
<li>Store the gateway&#8217;s Cassandra credentials in a Kubernetes <code>Secret</code>, not plaintext env vars in the manifest.</li>



<li>Add authentication (SigV4 or a simpler bearer token scheme) to the gateway before exposing it publicly — the example above has none, intentionally, to keep the code focused on the storage mechanics.</li>
</ul>



<h2 class="wp-block-heading">Monitoring and Troubleshooting</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Symptom</th><th>Cause</th><th>Fix</th></tr></thead><tbody><tr><td><code>nodetool status</code> shows <code>DN</code> (Down/Normal)</td><td>Pod crashed or network partition</td><td><code>kubectl describe pod cassandra-N</code>, check node resource pressure</td></tr><tr><td>Gateway 504s on large uploads</td><td>Ingress body-size limit too low</td><td>Raise <code>proxy-body-size</code> annotation</td></tr><tr><td>High latency on reads</td><td>Chunk reads happening serially</td><td>Parallelize chunk fetches in the gateway with <code>Promise.all</code></td></tr><tr><td>Compaction pressure / high disk I/O</td><td>Storing very large blobs directly</td><td>Cap object size or route large files to dedicated blob storage</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Deploy <code>cassandra-exporter</code> or JMX-to-Prometheus exporters as a sidecar for cluster-level metrics (pending compactions, read/write latency, heap usage), and scrape gateway request metrics via a <code>/metrics</code> endpoint for Prometheus + Grafana dashboards.</p>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph">Cassandra doesn&#8217;t give you S3 out of the box, but its distributed, replicated, partition-tolerant design makes it a solid metadata and chunk-storage backend when you build an S3-compatible API layer on top — especially in Kubernetes, where StatefulSets and headless Services handle the hard parts of stable node identity and gossip discovery for you. For most production object-storage needs, evaluate MinIO or Ceph RGW first; reach for this Cassandra-backed pattern when you specifically need Cassandra&#8217;s tunable consistency and multi-datacenter replication model married to an S3-shaped API.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://cassandra.apache.org/doc/latest/">Apache Cassandra Documentation</a></li>



<li><a href="https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/">Kubernetes StatefulSets</a></li>



<li><a href="https://kubernetes.io/docs/concepts/services-networking/service/#headless-services">Kubernetes Headless Services</a></li>



<li><a href="https://kubernetes.io/docs/concepts/services-networking/network-policies/">Kubernetes NetworkPolicy</a></li>



<li><a href="https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html">Amazon S3 API Reference</a></li>



<li><a href="https://min.io/docs/minio/kubernetes/upstream/">MinIO Documentation</a> (for a purpose-built alternative)</li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/docker/how-to-build-an-s3-compatible-object-store-with-cassandra-on-kubernetes-complete-guide/">How to Build an S3-Compatible Object Store with Cassandra on Kubernetes: Complete Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/docker/how-to-build-an-s3-compatible-object-store-with-cassandra-on-kubernetes-complete-guide/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4667</post-id>	</item>
		<item>
		<title>How to Use Logspout to Collect Container Logs: Centralized Docker Logging Setup</title>
		<link>https://awjunaid.com/docker/how-to-use-logspout-to-collect-container-logs-centralized-docker-logging-setup/</link>
					<comments>https://awjunaid.com/docker/how-to-use-logspout-to-collect-container-logs-centralized-docker-logging-setup/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sat, 02 Sep 2023 06:02:49 +0000</pubDate>
				<category><![CDATA[Docker]]></category>
		<category><![CDATA[docker]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=4664</guid>

					<description><![CDATA[<p>The first time I had more than five containers running on a single host, docker logs container-name stopped&#8230;</p>
<p>The post <a href="https://awjunaid.com/docker/how-to-use-logspout-to-collect-container-logs-centralized-docker-logging-setup/">How to Use Logspout to Collect Container Logs: Centralized Docker Logging Setup</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">The first time I had more than five containers running on a single host, <code>docker logs container-name</code> stopped being a workflow and became a chore. Tailing logs one container at a time, across multiple hosts, during an incident is exactly the kind of thing that turns a 10-minute debugging session into an hour. Logspout solved that for me — it&#8217;s a lightweight log router that attaches to the Docker socket, tails stdout/stderr from every running container, and ships it wherever you want: syslog, a HTTP endpoint, Logstash, Elasticsearch, or a custom adapter. This guide covers setting it up from scratch, understanding how it works internally, and routing logs to a real aggregation backend.</p>



<h2 class="wp-block-heading">What Logspout Actually Does</h2>



<p class="wp-block-paragraph">Logspout is not a log storage system — it&#8217;s a <strong>router</strong>. It:</p>



<ol class="wp-block-list">
<li>Connects to the Docker daemon&#8217;s Unix socket (<code>/var/run/docker.sock</code>).</li>



<li>Discovers all running containers and attaches to their log streams (the same stream <code>docker logs</code> reads from).</li>



<li>Watches for new containers starting/stopping and attaches/detaches dynamically — no restart needed.</li>



<li>Forwards each log line, tagged with container metadata, to one or more configured destinations (&#8220;routes&#8221;).</li>
</ol>



<p class="wp-block-paragraph">Because it reads directly from the Docker log driver, it works with any container without requiring you to modify application code to log to a specific place.</p>



<h2 class="wp-block-heading">Prerequisites</h2>



<ul class="wp-block-list">
<li>Docker Engine installed and running.</li>



<li>Root or a user in the <code>docker</code> group (Logspout needs access to <code>/var/run/docker.sock</code>).</li>
</ul>



<p class="wp-block-paragraph">Check your Docker version:</p>



<pre class="wp-block-code"><code>docker version --format '{{.Server.Version}}'
</code></pre>



<h2 class="wp-block-heading">Step 1: Run Logspout With a Basic Route</h2>



<p class="wp-block-paragraph">The simplest setup forwards all container logs to a syslog endpoint. Let&#8217;s start with an actual syslog server for testing — <code>papertrail</code>-style or a local <code>syslog-ng</code> container works, but for a first test we&#8217;ll just print to a local UDP listener.</p>



<pre class="wp-block-code"><code>docker run -d --name logspout \
  --volume=/var/run/docker.sock:/var/run/docker.sock \
  --publish=127.0.0.1:8000:80 \
  gliderlabs/logspout
</code></pre>



<p class="wp-block-paragraph">This starts Logspout with its HTTP API exposed on port 8000 (useful for live-tailing without a destination configured yet).</p>



<p class="wp-block-paragraph">Verify it&#8217;s running:</p>



<pre class="wp-block-code"><code>docker ps --filter name=logspout
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>CONTAINER ID   IMAGE                  STATUS         PORTS                      NAMES
a1b2c3d4e5f6   gliderlabs/logspout    Up 5 seconds   0.0.0.0:8000-&gt;80/tcp      logspout
</code></pre>



<h2 class="wp-block-heading">Step 2: Live-Tail All Container Logs Over HTTP</h2>



<p class="wp-block-paragraph">Logspout exposes a <code>/logs</code> streaming endpoint out of the box:</p>



<pre class="wp-block-code"><code>curl http://127.0.0.1:8000/logs
</code></pre>



<p class="wp-block-paragraph">Start another container to generate some log output:</p>



<pre class="wp-block-code"><code>docker run -d --name test-app alpine sh -c "while true; do echo 'hello from test-app'; sleep 2; done"
</code></pre>



<p class="wp-block-paragraph">Back in your <code>curl</code> terminal, expected output:</p>



<pre class="wp-block-code"><code>test-app|hello from test-app
test-app|hello from test-app
</code></pre>



<p class="wp-block-paragraph">This confirms Logspout picked up the new container automatically — no configuration change or restart required, which is the core value proposition versus manually configuring log shipping per container.</p>



<h2 class="wp-block-heading">Step 3: Route Logs to Syslog (Real Aggregation)</h2>



<p class="wp-block-paragraph">Most production setups forward to a centralized syslog receiver, Logstash, or a managed log service. The route is passed as a command argument using the <code>syslog://</code> scheme:</p>



<pre class="wp-block-code"><code>docker run -d --name logspout \
  --volume=/var/run/docker.sock:/var/run/docker.sock \
  gliderlabs/logspout \
  syslog://logs.example.com:514
</code></pre>



<p class="wp-block-paragraph">For local testing, stand up a syslog receiver container:</p>



<pre class="wp-block-code"><code>docker run -d --name syslog-server -p 514:514/udp balabit/syslog-ng:latest
</code></pre>



<p class="wp-block-paragraph">Then point Logspout at it via the Docker network (assuming both are on the same user-defined network, <code>logging-net</code>):</p>



<pre class="wp-block-code"><code>docker network create logging-net

docker run -d --name syslog-server \
  --network logging-net \
  -p 514:514/udp \
  balabit/syslog-ng:latest

docker run -d --name logspout \
  --network logging-net \
  --volume=/var/run/docker.sock:/var/run/docker.sock \
  gliderlabs/logspout \
  syslog://syslog-server:514
</code></pre>



<h2 class="wp-block-heading">Step 4: Filter Which Containers/Logs Get Shipped</h2>



<p class="wp-block-paragraph">By default Logspout ships stdout+stderr from every container, including itself. You almost always want to exclude some containers (e.g., the log shipper itself, or noisy health-check sidecars). Use environment variables or route-level filters:</p>



<pre class="wp-block-code"><code>docker run -d --name logspout \
  --network logging-net \
  --volume=/var/run/docker.sock:/var/run/docker.sock \
  -e EXCLUDE_LABEL=logspout.exclude \
  gliderlabs/logspout \
  syslog://syslog-server:514
</code></pre>



<p class="wp-block-paragraph">Then label containers you want excluded:</p>



<pre class="wp-block-code"><code>docker run -d --name noisy-healthcheck \
  --label logspout.exclude=true \
  alpine sh -c "while true; do echo ping; sleep 1; done"
</code></pre>



<p class="wp-block-paragraph">You can also filter by source stream:</p>



<pre class="wp-block-code"><code>gliderlabs/logspout syslog://syslog-server:514?filter.sources=stdout
</code></pre>



<p class="wp-block-paragraph">This ships only stdout, excluding stderr — useful if you route error logs somewhere different.</p>



<h2 class="wp-block-heading">Step 5: Route to Multiple Destinations at Once</h2>



<p class="wp-block-paragraph">Logspout supports multiple simultaneous routes, comma-separated:</p>



<pre class="wp-block-code"><code>docker run -d --name logspout \
  --network logging-net \
  --volume=/var/run/docker.sock:/var/run/docker.sock \
  gliderlabs/logspout \
  syslog://syslog-server:514,http://logstash.example.com:8080
</code></pre>



<p class="wp-block-paragraph">This is useful during a migration — ship to your old syslog target and a new ELK pipeline simultaneously while you validate the new one.</p>



<h2 class="wp-block-heading">Step 6: Docker Compose Setup for a Real Stack</h2>



<p class="wp-block-paragraph">A realistic logging stack pairs Logspout with Logstash/Elasticsearch/Kibana (the classic ELK approach) or a lighter Loki-based stack. Here&#8217;s Logspout feeding Logstash:</p>



<pre class="wp-block-code"><code># docker-compose.yml
version: "3.8"

networks:
  logging-net:
    driver: bridge

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.14.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - ES_JAVA_OPTS=-Xms512m -Xmx512m
    networks:
      - logging-net
    ports:
      - "9200:9200"

  logstash:
    image: docker.elastic.co/logstash/logstash:8.14.0
    volumes:
      - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
    networks:
      - logging-net
    depends_on:
      - elasticsearch
    ports:
      - "5000:5000"

  kibana:
    image: docker.elastic.co/kibana/kibana:8.14.0
    networks:
      - logging-net
    ports:
      - "5601:5601"
    depends_on:
      - elasticsearch

  logspout:
    image: gliderlabs/logspout
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - logging-net
    command: logstash://logstash:5000
    depends_on:
      - logstash

  app:
    image: nginx:alpine
    networks:
      - logging-net
</code></pre>



<p class="wp-block-paragraph"><code>logstash.conf</code>:</p>



<pre class="wp-block-code"><code>input {
  tcp {
    port =&gt; 5000
    codec =&gt; json_lines
  }
}

output {
  elasticsearch {
    hosts =&gt; &#91;"http://elasticsearch:9200"]
    index =&gt; "docker-logs-%{+YYYY.MM.dd}"
  }
}
</code></pre>



<pre class="wp-block-code"><code>docker compose up -d
</code></pre>



<p class="wp-block-paragraph">After a minute, check that logs landed in Elasticsearch:</p>



<pre class="wp-block-code"><code>curl -s http://localhost:9200/_cat/indices?v
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>health status index                  uuid    pri rep docs.count
yellow open   docker-logs-2026.07.29 xxxxxx  1   1   142
</code></pre>



<p class="wp-block-paragraph">Open Kibana at <code>http://localhost:5601</code> and create a data view over <code>docker-logs-*</code> to search and visualize.</p>



<h2 class="wp-block-heading">How Logspout Works Internally</h2>



<p class="wp-block-paragraph">Under the hood, Logspout uses the Docker Engine API&#8217;s container <code>attach</code>/<code>logs</code> endpoint with <code>follow=true</code>, which streams multiplexed stdout/stderr frames over the socket connection. Docker&#8217;s log driver framing includes a stream-type byte (stdout vs stderr) and a length-prefixed payload — Logspout demultiplexes this, attaches container metadata (name, ID, image, labels), and hands each line off to whichever output adapter matches your configured route scheme (<code>syslog://</code>, <code>http://</code>, <code>logstash://</code>, or a custom Go adapter you register at build time).</p>



<p class="wp-block-paragraph">Because it uses the <code>logs</code> API rather than reading log files directly, it works regardless of which logging driver the container itself is configured with (<code>json-file</code>, <code>local</code>, etc.), as long as that driver supports the read-back API — note that drivers like <code>none</code> or some remote-only drivers won&#8217;t have anything for Logspout to read.</p>



<h2 class="wp-block-heading">Security Considerations</h2>



<ul class="wp-block-list">
<li><strong>Docker socket access is root-equivalent.</strong> Mounting <code>/var/run/docker.sock</code> into Logspout gives it the same power as root on the host. Only run trusted, pinned images (use a specific tag/digest, not <code>latest</code>, in production), and don&#8217;t expose Logspout&#8217;s HTTP API (<code>8000</code>) publicly.</li>



<li>Encrypt log transport in production — plain <code>syslog://</code> over UDP is unauthenticated and unencrypted; use <code>syslog+tls://</code> where supported, or route through a TLS-terminating aggregator.</li>



<li>Scrub sensitive data (tokens, PII) at the application log-emission level — Logspout ships whatever containers print; it does not redact.</li>
</ul>



<h2 class="wp-block-heading">Troubleshooting</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Symptom</th><th>Cause</th><th>Fix</th></tr></thead><tbody><tr><td>No logs appearing at destination</td><td>Route URL/scheme typo, or destination unreachable</td><td>Check <code>docker logs logspout</code> for connection errors</td></tr><tr><td>Logspout can&#8217;t start / permission denied</td><td>Docker socket not mounted or wrong permissions</td><td>Confirm <code>-v /var/run/docker.sock:/var/run/docker.sock</code> and that the user has socket access</td></tr><tr><td>Only some containers&#8217; logs show up</td><td><code>EXCLUDE_LABEL</code> or <code>filter.sources</code> misconfigured</td><td>Remove filters temporarily to confirm baseline behavior</td></tr><tr><td>Duplicate log lines at destination</td><td>Multiple Logspout instances or restart loops</td><td>Ensure only one Logspout container per host; check <code>docker ps -a</code> for crash-looping instances</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Check Logspout&#8217;s own logs for adapter errors:</p>



<pre class="wp-block-code"><code>docker logs logspout --tail 50
</code></pre>



<h2 class="wp-block-heading">Real-World Deployment Notes</h2>



<p class="wp-block-paragraph">I&#8217;ve run Logspout as a single instance per host in a multi-host fleet, each one shipping to a shared aggregation cluster rather than trying to centralize the shipper itself — it&#8217;s stateless, so there&#8217;s no coordination needed between instances on different hosts. One thing worth planning for early: log volume grows faster than people expect once every container is shipping unfiltered stdout/stderr, so set up index/retention policies on your Elasticsearch (or equivalent) cluster from day one rather than after a disk-full incident. If you&#8217;re on Kubernetes rather than plain Docker hosts, Logspout isn&#8217;t the typical choice — Fluent Bit or Vector as a DaemonSet is more common there, since they integrate more natively with the Kubernetes API for pod metadata enrichment. Logspout shines specifically in plain Docker/Compose/Swarm environments where you don&#8217;t already have a Kubernetes-native logging pipeline.</p>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph">Logspout earns its place in a Docker logging stack by doing one thing well: dynamically discovering containers and routing their log streams without requiring per-container configuration. It&#8217;s not a replacement for a full observability stack — you still need something to store, index, and query logs (ELK, Loki, a SaaS platform) — but as the collection layer, it removes the operational burden of manually wiring log shipping into every container you run.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://github.com/gliderlabs/logspout">Logspout GitHub Repository</a></li>



<li><a href="https://docs.docker.com/reference/api/engine/">Docker Engine API — Container Logs</a></li>



<li><a href="https://docs.docker.com/engine/logging/configure/">Docker Logging Drivers</a></li>



<li><a href="https://www.elastic.co/guide/en/logstash/current/index.html">Elastic Logstash Documentation</a></li>
</ul>
<p>The post <a href="https://awjunaid.com/docker/how-to-use-logspout-to-collect-container-logs-centralized-docker-logging-setup/">How to Use Logspout to Collect Container Logs: Centralized Docker Logging Setup</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/docker/how-to-use-logspout-to-collect-container-logs-centralized-docker-logging-setup/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4664</post-id>	</item>
		<item>
		<title>How to Get the Logs of a Container with Docker Logs: Complete Logging and Troubleshooting Guide</title>
		<link>https://awjunaid.com/docker/how-to-get-the-logs-of-a-container-with-docker-logs-complete-logging-and-troubleshooting-guide/</link>
					<comments>https://awjunaid.com/docker/how-to-get-the-logs-of-a-container-with-docker-logs-complete-logging-and-troubleshooting-guide/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sat, 02 Sep 2023 05:59:47 +0000</pubDate>
				<category><![CDATA[Docker]]></category>
		<category><![CDATA[docker]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=4661</guid>

					<description><![CDATA[<p>docker logs is the first command I reach for when anything goes wrong with a container, and it&#8217;s&#8230;</p>
<p>The post <a href="https://awjunaid.com/docker/how-to-get-the-logs-of-a-container-with-docker-logs-complete-logging-and-troubleshooting-guide/">How to Get the Logs of a Container with Docker Logs: Complete Logging and Troubleshooting Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><code>docker logs</code> is the first command I reach for when anything goes wrong with a container, and it&#8217;s also the one I see used incorrectly most often — people either don&#8217;t know about the flags that make it actually useful (<code>--since</code>, <code>--tail</code>, <code>-f</code>), or they don&#8217;t understand why logs sometimes disappear entirely after a container restarts. This guide covers the command properly: from the absolute basics to the internals of how Docker captures and stores log output, plus the configuration decisions that determine whether <code>docker logs</code> works at all.</p>



<h2 class="wp-block-heading">The Basics</h2>



<pre class="wp-block-code"><code>docker logs &lt;container_name_or_id&gt;
</code></pre>



<p class="wp-block-paragraph">Example — start a container that emits output on a loop:</p>



<pre class="wp-block-code"><code>docker run -d --name demo-app alpine sh -c "i=0; while true; do echo \"log line \$i\"; i=\$((i+1)); sleep 1; done"
</code></pre>



<p class="wp-block-paragraph">Fetch its logs:</p>



<pre class="wp-block-code"><code>docker logs demo-app
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>log line 0
log line 1
log line 2
...
</code></pre>



<h2 class="wp-block-heading">Following Logs in Real Time</h2>



<p class="wp-block-paragraph">Add <code>-f</code> (or <code>--follow</code>) to stream new log lines as they&#8217;re written, similar to <code>tail -f</code>:</p>



<pre class="wp-block-code"><code>docker logs -f demo-app
</code></pre>



<p class="wp-block-paragraph">This keeps your terminal attached until you <code>Ctrl+C</code>. It does <strong>not</strong> stop the container — it just detaches your view.</p>



<h2 class="wp-block-heading">Limiting Output With <code>--tail</code></h2>



<p class="wp-block-paragraph">For a container that&#8217;s been running for hours, dumping the entire log history is rarely useful. Show just the last N lines:</p>



<pre class="wp-block-code"><code>docker logs --tail 20 demo-app
</code></pre>



<p class="wp-block-paragraph">Expected output: the most recent 20 lines only, in order.</p>



<p class="wp-block-paragraph">Combine with <code>-f</code> to follow starting from the last 20 lines instead of the beginning:</p>



<pre class="wp-block-code"><code>docker logs -f --tail 20 demo-app
</code></pre>



<h2 class="wp-block-heading">Filtering by Time</h2>



<p class="wp-block-paragraph">Two flags control time-based filtering:</p>



<ul class="wp-block-list">
<li><code>--since</code> — show logs after a given time</li>



<li><code>--until</code> — show logs before a given time</li>
</ul>



<p class="wp-block-paragraph">Both accept RFC3339 timestamps, Unix timestamps, or relative durations like <code>10m</code>, <code>1h</code>.</p>



<pre class="wp-block-code"><code>docker logs --since 5m demo-app
</code></pre>



<p class="wp-block-paragraph">Expected output: only lines logged in the last 5 minutes.</p>



<pre class="wp-block-code"><code>docker logs --since "2026-07-29T10:00:00" --until "2026-07-29T10:15:00" demo-app
</code></pre>



<p class="wp-block-paragraph">Expected output: lines logged only within that 15-minute window — extremely useful when correlating logs to a specific incident window from a monitoring alert.</p>



<h2 class="wp-block-heading">Including Timestamps</h2>



<p class="wp-block-paragraph">By default, <code>docker logs</code> doesn&#8217;t print timestamps. Add <code>-t</code> (or <code>--timestamps</code>):</p>



<pre class="wp-block-code"><code>docker logs -t --tail 5 demo-app
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>2026-07-29T10:22:01.123456789Z log line 118
2026-07-29T10:22:02.123456789Z log line 119
2026-07-29T10:22:03.123456789Z log line 120
2026-07-29T10:22:04.123456789Z log line 121
2026-07-29T10:22:05.123456789Z log line 122
</code></pre>



<p class="wp-block-paragraph">This is stored internally regardless — Docker always timestamps log entries at capture time — <code>-t</code> just controls whether it&#8217;s printed.</p>



<h2 class="wp-block-heading">Separating stdout and stderr</h2>



<p class="wp-block-paragraph">By default <code>docker logs</code> interleaves both streams. To isolate one:</p>



<pre class="wp-block-code"><code>docker logs demo-app 1&gt;stdout.log 2&gt;stderr.log
</code></pre>



<p class="wp-block-paragraph">This works because Docker&#8217;s CLI writes stdout and stderr from the container to the CLI&#8217;s own stdout/stderr respectively, so normal shell redirection separates them into files.</p>



<h2 class="wp-block-heading">Getting Logs From a Docker Compose Service</h2>



<pre class="wp-block-code"><code>docker compose logs
docker compose logs -f web
docker compose logs --tail 50 --since 10m worker
</code></pre>



<p class="wp-block-paragraph">Compose&#8217;s <code>logs</code> command is effectively a wrapper that aggregates <code>docker logs</code> across all containers in the project, prefixing each line with the service name:</p>



<pre class="wp-block-code"><code>web_1     | 200 GET /health
worker_1  | processing job 4821
</code></pre>



<h2 class="wp-block-heading">Why Logs Sometimes Disappear: Logging Drivers</h2>



<p class="wp-block-paragraph">This is the part that trips people up most. <code>docker logs</code> doesn&#8217;t read from the container process directly — it reads from whatever <strong>logging driver</strong> is configured, which defaults to <code>json-file</code>. That driver writes each container&#8217;s stdout/stderr to a JSON file on the host at:</p>



<pre class="wp-block-code"><code>/var/lib/docker/containers/&lt;container-id&gt;/&lt;container-id&gt;-json.log
</code></pre>



<p class="wp-block-paragraph">Check the current driver:</p>



<pre class="wp-block-code"><code>docker info --format '{{.LoggingDriver}}'
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>json-file
</code></pre>



<p class="wp-block-paragraph">Inspect the raw file directly (useful when <code>docker logs</code> itself is misbehaving):</p>



<pre class="wp-block-code"><code>sudo cat /var/lib/docker/containers/$(docker inspect --format '{{.Id}}' demo-app)/*-json.log
</code></pre>



<p class="wp-block-paragraph">Expected output — raw JSON, one object per line:</p>



<pre class="wp-block-code"><code>{"log":"log line 130\n","stream":"stdout","time":"2026-07-29T10:25:10.123456789Z"}
</code></pre>



<p class="wp-block-paragraph"><strong>Critically</strong>: if you switch the logging driver to something like <code>syslog</code>, <code>journald</code>, <code>fluentd</code>, <code>awslogs</code>, <code>gelf</code>, or <code>none</code>, <code>docker logs</code> may return <strong>nothing</strong> or an error, because those drivers don&#8217;t support Docker&#8217;s log-reading API the same way (some, like <code>journald</code>, do support it; others like <code>awslogs</code> do not). This is by far the most common cause of &#8220;why is <code>docker logs</code> empty&#8221; support questions.</p>



<p class="wp-block-paragraph">Check per-container driver override:</p>



<pre class="wp-block-code"><code>docker inspect demo-app --format '{{.HostConfig.LogConfig.Type}}'
</code></pre>



<p class="wp-block-paragraph">If this returns <code>none</code>, that container was explicitly configured to discard logs — set at run time:</p>



<pre class="wp-block-code"><code>docker run -d --log-driver none alpine echo "this won't be captured by docker logs"
</code></pre>



<h2 class="wp-block-heading">Configuring Log Rotation (a Common Production Gotcha)</h2>



<p class="wp-block-paragraph">By default, <code>json-file</code> logs grow <strong>unbounded</strong> unless you configure rotation — a chatty container can fill your disk. Set size limits and rotation count at run time:</p>



<pre class="wp-block-code"><code>docker run -d --name demo-app \
  --log-driver json-file \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  alpine sh -c "while true; do echo tick; sleep 1; done"
</code></pre>



<p class="wp-block-paragraph">Verify:</p>



<pre class="wp-block-code"><code>docker inspect demo-app --format '{{json .HostConfig.LogConfig}}'
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>{"Type":"json-file","Config":{"max-file":"3","max-size":"10m"}}
</code></pre>



<p class="wp-block-paragraph">This caps total log storage per container at 30MB (10MB × 3 files), discarding the oldest once rotated. For a daemon-wide default (applies to all new containers), edit <code>/etc/docker/daemon.json</code>:</p>



<pre class="wp-block-code"><code>{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
</code></pre>



<pre class="wp-block-code"><code>sudo systemctl restart docker
</code></pre>



<h2 class="wp-block-heading">Logs After Container Removal</h2>



<p class="wp-block-paragraph">Once a container is removed (<code>docker rm</code>), its log file is deleted along with it — <code>docker logs</code> on a removed container returns an error:</p>



<pre class="wp-block-code"><code>docker rm -f demo-app
docker logs demo-app
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>Error response from daemon: No such container: demo-app
</code></pre>



<p class="wp-block-paragraph">This is why production systems ship logs to an external aggregator (see the Logspout guide) rather than relying on <code>docker logs</code> as long-term storage — it&#8217;s a live debugging tool, not a retention system.</p>



<h2 class="wp-block-heading">Combining Flags for Real Debugging Scenarios</h2>



<p class="wp-block-paragraph">Investigating a crash that happened around 14:32, want context before and after, with timestamps:</p>



<pre class="wp-block-code"><code>docker logs -t --since "2026-07-29T14:30:00" --until "2026-07-29T14:35:00" demo-app
</code></pre>



<p class="wp-block-paragraph">Watching a freshly deployed container for errors in real time, ignoring history:</p>



<pre class="wp-block-code"><code>docker logs -f --since 0s demo-app
</code></pre>



<p class="wp-block-paragraph">Grepping logs for a specific error pattern:</p>



<pre class="wp-block-code"><code>docker logs demo-app 2&gt;&amp;1 | grep -i "error\|exception\|panic"
</code></pre>



<h2 class="wp-block-heading">Security Considerations</h2>



<ul class="wp-block-list">
<li>Log files under <code>/var/lib/docker/containers/</code> are readable by root (and the <code>docker</code> group, which is effectively root-equivalent) — treat any secrets accidentally printed to stdout as compromised the moment they&#8217;re logged.</li>



<li>Avoid logging credentials, tokens, or PII from application code; <code>docker logs</code> output is often shipped verbatim to third-party aggregation services.</li>



<li>Restrict <code>docker</code> group membership on shared hosts — anyone in it can read every container&#8217;s logs, not just their own.</li>
</ul>



<h2 class="wp-block-heading">Troubleshooting Reference</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Symptom</th><th>Cause</th><th>Fix</th></tr></thead><tbody><tr><td><code>docker logs</code> returns nothing</td><td>Logging driver doesn&#8217;t support log API (e.g. <code>awslogs</code>, <code>gcplogs</code>) or driver set to <code>none</code></td><td>Check <code>docker inspect --format '{{.HostConfig.LogConfig.Type}}'</code>; view logs in the target system instead</td></tr><tr><td>Logs stop appearing mid-stream</td><td>Log file rotated/deleted, or disk full</td><td>Check <code>df -h</code>, configure <code>max-size</code>/<code>max-file</code></td></tr><tr><td><code>docker logs -f</code> hangs forever with no new output</td><td>Container isn&#8217;t producing output, or app buffers stdout (not flushing)</td><td>Check app&#8217;s stdout buffering; for Python use <code>python -u</code>, for Node ensure no custom buffered writer</td></tr><tr><td>Old logs missing after container recreation</td><td>New container = new log file; old one deleted with old container</td><td>Ship logs externally before container replacement (CI/CD rolling deploys)</td></tr><tr><td><code>permission denied</code> reading log file directly</td><td>Not running as root/docker group</td><td>Use <code>sudo</code>, or stick to the <code>docker logs</code> CLI which handles permissions internally</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Real-World Deployment Notes</h2>



<p class="wp-block-paragraph">On any host running more than a handful of containers, I set <code>max-size</code>/<code>max-file</code> defaults in <code>daemon.json</code> on day one — it&#8217;s the single most common &#8220;why is my disk full&#8221; incident I&#8217;ve seen with Docker in production, and it&#8217;s entirely preventable with two lines of config. For teams already running a log aggregator (ELK, Loki, a SaaS platform), <code>docker logs</code> becomes primarily a local, immediate-feedback tool used during active debugging or in CI pipelines where spinning up a full logging stack isn&#8217;t worth it — <code>docker compose logs -f</code> during a test run is often all you need. Where I&#8217;ve seen <code>docker logs</code> genuinely fail teams is when they treat it as their only logging strategy in production: it&#8217;s tied to container lifecycle and host disk space, neither of which are guarantees you want your incident postmortems depending on.</p>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph"><code>docker logs</code> looks like a trivial command until you need it under pressure — then the flags for time filtering, tailing, and stream separation become the difference between finding the root cause in 30 seconds and scrolling through thousands of irrelevant lines. Understanding that it&#8217;s backed by a configurable logging driver (not some magic container-internal buffer) explains almost every &#8220;why don&#8217;t my logs show up&#8221; issue you&#8217;ll hit, and configuring rotation up front saves you from a full disk during an incident, which is the worst possible time to discover you never set <code>max-size</code>.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://docs.docker.com/reference/cli/docker/container/logs/">Docker CLI Reference — docker logs</a></li>



<li><a href="https://docs.docker.com/engine/logging/configure/">Docker Logging Drivers Overview</a></li>



<li><a href="https://docs.docker.com/reference/cli/docker/compose/logs/">Docker Compose CLI — logs</a></li>



<li><a href="https://docs.docker.com/reference/cli/dockerd/#daemon-configuration-file">Docker daemon.json Reference</a></li>
</ul>
<p>The post <a href="https://awjunaid.com/docker/how-to-get-the-logs-of-a-container-with-docker-logs-complete-logging-and-troubleshooting-guide/">How to Get the Logs of a Container with Docker Logs: Complete Logging and Troubleshooting Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/docker/how-to-get-the-logs-of-a-container-with-docker-logs-complete-logging-and-troubleshooting-guide/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4661</post-id>	</item>
		<item>
		<title>How to Listen to Docker Events on Your Docker Hosts: Real-Time Event Monitoring Guide</title>
		<link>https://awjunaid.com/docker/how-to-listen-to-docker-events-on-your-docker-hosts-real-time-event-monitoring-guide/</link>
					<comments>https://awjunaid.com/docker/how-to-listen-to-docker-events-on-your-docker-hosts-real-time-event-monitoring-guide/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sat, 02 Sep 2023 05:57:57 +0000</pubDate>
				<category><![CDATA[Docker]]></category>
		<category><![CDATA[docker]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=4658</guid>

					<description><![CDATA[<p>Most people find out about docker events after something&#8217;s already gone wrong — a container kept restarting overnight,&#8230;</p>
<p>The post <a href="https://awjunaid.com/docker/how-to-listen-to-docker-events-on-your-docker-hosts-real-time-event-monitoring-guide/">How to Listen to Docker Events on Your Docker Hosts: Real-Time Event Monitoring Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Most people find out about <code>docker events</code> after something&#8217;s already gone wrong — a container kept restarting overnight, or an image got pulled and nobody knows when or by whom. I started using it proactively instead: piping it into a small alerting script so I get notified the moment a container dies, rather than finding out from a user complaint an hour later. This guide covers <code>docker events</code> from basic usage through filtering, JSON output for scripting, and wiring it into real monitoring pipelines.</p>



<h2 class="wp-block-heading">What <code>docker events</code> Actually Is</h2>



<p class="wp-block-paragraph">The Docker daemon maintains an internal event stream for nearly everything that happens to containers, images, volumes, networks, and the daemon itself. <code>docker events</code> is a client that subscribes to that stream in real time over the Docker Engine API (<code>GET /events</code>) — it&#8217;s push-based, not polling, so events appear the instant they happen with no delay.</p>



<p class="wp-block-paragraph">Event categories include:</p>



<ul class="wp-block-list">
<li><strong>Container</strong>: create, start, stop, die, kill, restart, pause, unpause, oom, exec_create, health_status</li>



<li><strong>Image</strong>: pull, push, delete, tag, untag</li>



<li><strong>Volume</strong>: create, mount, unmount, destroy</li>



<li><strong>Network</strong>: create, connect, disconnect, destroy</li>



<li><strong>Daemon</strong>: reload</li>
</ul>



<h2 class="wp-block-heading">Basic Usage</h2>



<pre class="wp-block-code"><code>docker events
</code></pre>



<p class="wp-block-paragraph">This blocks and streams events live. Open a second terminal and generate some activity:</p>



<pre class="wp-block-code"><code>docker run -d --name events-demo alpine sleep 300
</code></pre>



<p class="wp-block-paragraph">Back in the first terminal, expected output:</p>



<pre class="wp-block-code"><code>2026-07-29T10:40:12.123456789Z container create a1b2c3d4... (image=alpine, name=events-demo)
2026-07-29T10:40:12.234567890Z network connect 8f7e6d5c... (container=a1b2c3d4..., name=bridge)
2026-07-29T10:40:12.345678901Z container start a1b2c3d4... (image=alpine, name=events-demo)
</code></pre>



<p class="wp-block-paragraph">Stop it and watch the corresponding events:</p>



<pre class="wp-block-code"><code>docker stop events-demo
</code></pre>



<pre class="wp-block-code"><code>2026-07-29T10:41:05.111222333Z container kill a1b2c3d4... (signal=15)
2026-07-29T10:41:05.222333444Z container die a1b2c3d4... (exitCode=143)
2026-07-29T10:41:05.333444555Z container stop a1b2c3d4...
</code></pre>



<p class="wp-block-paragraph">Note the <code>exitCode</code> in the <code>die</code> event — this is one of the most operationally useful fields, letting you distinguish a clean shutdown (<code>0</code>) from a crash (non-zero) without inspecting the container separately.</p>



<h2 class="wp-block-heading">Filtering Events</h2>



<p class="wp-block-paragraph">Use <code>--filter</code> to narrow the stream — essential once you have more than a couple of containers running, or the output becomes unreadable noise.</p>



<p class="wp-block-paragraph"><strong>By event type:</strong></p>



<pre class="wp-block-code"><code>docker events --filter event=die
</code></pre>



<p class="wp-block-paragraph"><strong>By container:</strong></p>



<pre class="wp-block-code"><code>docker events --filter container=events-demo
</code></pre>



<p class="wp-block-paragraph"><strong>By image:</strong></p>



<pre class="wp-block-code"><code>docker events --filter image=alpine
</code></pre>



<p class="wp-block-paragraph"><strong>By label:</strong></p>



<pre class="wp-block-code"><code>docker events --filter label=environment=production
</code></pre>



<p class="wp-block-paragraph"><strong>Combine multiple filters</strong> (AND logic between different filter keys, OR logic within the same key):</p>



<pre class="wp-block-code"><code>docker events --filter event=die --filter event=oom --filter label=environment=production
</code></pre>



<p class="wp-block-paragraph">Expected behavior: this streams only <code>die</code> or <code>oom</code> events, restricted to containers labeled <code>environment=production</code>.</p>



<h2 class="wp-block-heading">Time-Bounded Queries (Not Just Live Streaming)</h2>



<p class="wp-block-paragraph"><code>docker events</code> isn&#8217;t only for live tailing — you can query a historical window using <code>--since</code> and <code>--until</code>, exactly like <code>docker logs</code>:</p>



<pre class="wp-block-code"><code>docker events --since "2026-07-29T09:00:00" --until "2026-07-29T10:00:00"
</code></pre>



<p class="wp-block-paragraph">Expected output: every daemon event that occurred in that one-hour window, useful for reconstructing what happened during an incident after the fact — assuming the daemon was running continuously (this reads live from the daemon&#8217;s in-memory buffer, not a persisted log, so events before a daemon restart are gone; more on that below).</p>



<p class="wp-block-paragraph">Relative durations work too:</p>



<pre class="wp-block-code"><code>docker events --since 30m
</code></pre>



<h2 class="wp-block-heading">JSON Output for Scripting</h2>



<p class="wp-block-paragraph">Raw text output isn&#8217;t great for programmatic consumption. Use <code>--format</code>:</p>



<pre class="wp-block-code"><code>docker events --format '{{json .}}'
</code></pre>



<p class="wp-block-paragraph">Expected output — one JSON object per line:</p>



<pre class="wp-block-code"><code>{"status":"die","id":"a1b2c3d4...","from":"alpine","Type":"container","Action":"die","Actor":{"ID":"a1b2c3d4...","Attributes":{"exitCode":"137","image":"alpine","name":"events-demo"}},"time":1753784465,"timeNano":1753784465123456789}
</code></pre>



<p class="wp-block-paragraph">Pipe into <code>jq</code> to build a real alerting one-liner:</p>



<pre class="wp-block-code"><code>docker events --filter event=die --format '{{json .}}' | jq -r '"\(.Actor.Attributes.name) exited with code \(.Actor.Attributes.exitCode) at \(.time)"'
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>events-demo exited with code 137 at 1753784465
</code></pre>



<p class="wp-block-paragraph">Exit code 137 specifically means the container was killed via SIGKILL (128 + 9) — commonly from an OOM kill or a forced <code>docker kill</code>. Combine with the <code>oom</code> event filter to distinguish memory kills from manual ones.</p>



<h2 class="wp-block-heading">Building a Real Alert Script</h2>



<p class="wp-block-paragraph">Here&#8217;s a practical example — a bash script that watches for any container dying with a non-zero exit code and posts to a webhook (e.g., Slack):</p>



<pre class="wp-block-code"><code>#!/usr/bin/env bash
# watch-crashes.sh
WEBHOOK_URL="https://hooks.slack.com/services/XXX/YYY/ZZZ"

docker events --filter event=die --format '{{json .}}' | while read -r event; do
  exit_code=$(echo "$event" | jq -r '.Actor.Attributes.exitCode')
  name=$(echo "$event" | jq -r '.Actor.Attributes.name')

  if &#91; "$exit_code" != "0" ]; then
    message="⚠️ Container *${name}* exited with code ${exit_code}"
    curl -s -X POST -H 'Content-type: application/json' \
      --data "{\"text\":\"${message}\"}" \
      "$WEBHOOK_URL"
  fi
done
</code></pre>



<pre class="wp-block-code"><code>chmod +x watch-crashes.sh
nohup ./watch-crashes.sh &amp;
</code></pre>



<p class="wp-block-paragraph">Run it as a systemd service for durability:</p>



<pre class="wp-block-code"><code># /etc/systemd/system/docker-crash-watch.service
&#91;Unit]
Description=Watch Docker container crashes and alert
After=docker.service
Requires=docker.service

&#91;Service]
ExecStart=/usr/local/bin/watch-crashes.sh
Restart=always
RestartSec=5

&#91;Install]
WantedBy=multi-user.target
</code></pre>



<pre class="wp-block-code"><code>sudo systemctl daemon-reload
sudo systemctl enable --now docker-crash-watch.service
</code></pre>



<h2 class="wp-block-heading">Health Check Events</h2>



<p class="wp-block-paragraph">If your containers define a <code>HEALTHCHECK</code>, Docker emits <code>health_status</code> events, which are extremely useful for catching &#8220;the process is running but the app is broken&#8221; scenarios that <code>die</code> events won&#8217;t cover:</p>



<pre class="wp-block-code"><code>docker run -d --name health-demo \
  --health-cmd="curl -f http://localhost/ || exit 1" \
  --health-interval=5s \
  nginx:alpine

docker events --filter event=health_status --filter container=health-demo
</code></pre>



<p class="wp-block-paragraph">Expected output over time:</p>



<pre class="wp-block-code"><code>2026-07-29T10:50:05.000000000Z container health_status: healthy a1b2c3d4... (name=health-demo)
</code></pre>



<p class="wp-block-paragraph">If you then break the container&#8217;s health check target, you&#8217;d see a transition to <code>health_status: unhealthy</code> — a strong signal to alert on and one that plain <code>die</code> monitoring would completely miss.</p>



<h2 class="wp-block-heading">Docker Compose and Events</h2>



<p class="wp-block-paragraph"><code>docker events</code> works daemon-wide regardless of whether containers were started via Compose, <code>docker run</code>, or Swarm — Compose doesn&#8217;t have its own separate event bus. Filter by Compose project label to scope to one stack:</p>



<pre class="wp-block-code"><code>docker events --filter label=com.docker.compose.project=myapp
</code></pre>



<h2 class="wp-block-heading">Internals: Where This Data Comes From</h2>



<p class="wp-block-paragraph">The Docker daemon (<code>dockerd</code>) maintains an in-memory ring buffer of recent events (default capacity: 1000 events) that the Engine API&#8217;s <code>/events</code> endpoint streams from over a long-lived HTTP connection. This has two important consequences:</p>



<ol class="wp-block-list">
<li><strong>Events are not persisted across daemon restarts.</strong> If <code>dockerd</code> restarts, the buffer is cleared — <code>--since</code> queries can&#8217;t reach further back than the daemon&#8217;s current uptime plus whatever&#8217;s still in the buffer.</li>



<li><strong>High-volume event bursts can evict old events from the buffer</strong> before a slow consumer reads them, though this is rare in normal operation — it matters mainly on hosts running very high container churn (e.g., CI runners spinning up hundreds of short-lived containers per minute).</li>
</ol>



<p class="wp-block-paragraph">For durable, long-term event history, you need to actively consume the stream into external storage — there&#8217;s no built-in persistence layer, which is exactly why the alert-script pattern above (or shipping into a time-series/log system) matters for anything beyond live debugging.</p>



<h2 class="wp-block-heading">Security Considerations</h2>



<ul class="wp-block-list">
<li>The events API requires access to the Docker socket, which is root-equivalent — treat any script consuming <code>docker events</code> with the same care as anything else touching <code>/var/run/docker.sock</code>.</li>



<li>Don&#8217;t expose the Docker daemon&#8217;s remote API (<code>-H tcp://0.0.0.0:2375</code>) without TLS — an unauthenticated remote events endpoint leaks your entire container topology (names, images, labels) to anyone who can reach it.</li>



<li>If shipping events to a webhook (like the Slack example), use a secrets manager or environment injection for the webhook URL rather than hardcoding it in a script under version control.</li>
</ul>



<h2 class="wp-block-heading">Troubleshooting</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Symptom</th><th>Cause</th><th>Fix</th></tr></thead><tbody><tr><td><code>docker events</code> shows nothing</td><td>No activity since command started, or filters too narrow</td><td>Trigger a test action (<code>docker run</code>, <code>docker stop</code>) in another terminal</td></tr><tr><td>Missing events from before a certain time</td><td>Daemon restarted, clearing the in-memory buffer</td><td>Ship events externally going forward; historical gap is unrecoverable</td></tr><tr><td>Script consuming events stops receiving new ones</td><td>Long-lived connection dropped (daemon restart, network blip)</td><td>Wrap the consumer in a restart loop (systemd <code>Restart=always</code>, as shown above)</td></tr><tr><td><code>permission denied</code> on socket</td><td>User not in <code>docker</code> group</td><td>Add user to group or run with <code>sudo</code></td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Real-World Deployment Notes</h2>



<p class="wp-block-paragraph">The alert-script pattern in this guide is genuinely how I&#8217;ve bootstrapped monitoring on small Docker fleets before a full observability stack was justified — it&#8217;s a few lines of bash and gets you meaningful signal on day one. As the fleet grows, I&#8217;ve typically graduated this into a small always-on consumer service (rather than a bash loop) that normalizes events into a message queue (Kafka, RabbitMQ) so multiple downstream systems — alerting, audit logging, autoscaling triggers — can consume the same event stream independently instead of each polling the Docker socket separately. Also worth knowing: on Kubernetes, the equivalent isn&#8217;t <code>docker events</code> at all but the Kubernetes Events API (<code>kubectl get events</code> or watching via the API server) — the underlying container runtime events still exist, but orchestration-level tooling on K8s abstracts you away from talking to the container engine directly.</p>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph"><code>docker events</code> turns Docker from something you have to actively check on into something that tells you when it needs attention. The combination of <code>--filter</code> for signal-to-noise control, <code>--format '{{json .}}'</code> for scriptability, and the fact that it&#8217;s a genuine live push stream (not polling) makes it a small but high-leverage piece of any container monitoring setup — especially for catching OOM kills and unhealthy states that simpler &#8220;is the container running&#8221; checks miss entirely.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://docs.docker.com/reference/cli/docker/system/events/">Docker CLI Reference — docker events</a></li>



<li><a href="https://docs.docker.com/reference/api/engine/version/v1.47/#tag/System/operation/SystemEvents">Docker Engine API — Monitor Events</a></li>



<li><a href="https://docs.docker.com/reference/dockerfile/#healthcheck">Docker Health Checks</a></li>



<li><a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html">systemd.service Documentation</a></li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/docker/how-to-listen-to-docker-events-on-your-docker-hosts-real-time-event-monitoring-guide/">How to Listen to Docker Events on Your Docker Hosts: Real-Time Event Monitoring Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/docker/how-to-listen-to-docker-events-on-your-docker-hosts-real-time-event-monitoring-guide/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4658</post-id>	</item>
		<item>
		<title>How to Obtain Usage Statistics of a Running Container: Docker Stats and Resource Monitoring</title>
		<link>https://awjunaid.com/docker/how-to-obtain-usage-statistics-of-a-running-container-docker-stats-and-resource-monitoring/</link>
					<comments>https://awjunaid.com/docker/how-to-obtain-usage-statistics-of-a-running-container-docker-stats-and-resource-monitoring/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sat, 02 Sep 2023 05:54:54 +0000</pubDate>
				<category><![CDATA[Docker]]></category>
		<category><![CDATA[docker]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=4655</guid>

					<description><![CDATA[<p>Somewhere between &#8220;the container is running&#8221; and &#8220;the container is healthy&#8221; there&#8217;s a whole category of problems that&#8230;</p>
<p>The post <a href="https://awjunaid.com/docker/how-to-obtain-usage-statistics-of-a-running-container-docker-stats-and-resource-monitoring/">How to Obtain Usage Statistics of a Running Container: Docker Stats and Resource Monitoring</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Somewhere between &#8220;the container is running&#8221; and &#8220;the container is healthy&#8221; there&#8217;s a whole category of problems that only <code>docker stats</code> catches — a memory leak that&#8217;s slowly climbing toward the limit, a container pegging CPU because of a runaway loop, or a network-chatty process eating bandwidth nobody accounted for. I check <code>docker stats</code> reflexively any time something feels sluggish, before I even open logs. This guide covers the command itself, how to make it scriptable, and what&#8217;s actually happening under the hood via cgroups — because understanding that is what lets you interpret the numbers correctly instead of guessing.</p>



<h2 class="wp-block-heading">Basic Usage</h2>



<pre class="wp-block-code"><code>docker stats
</code></pre>



<p class="wp-block-paragraph">This launches a live, auto-refreshing table for <strong>all running containers</strong>:</p>



<pre class="wp-block-code"><code>CONTAINER ID   NAME         CPU %     MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O         PIDS
a1b2c3d4e5f6   web          2.34%     45.2MiB / 512MiB       8.83%    1.2kB / 850B      0B / 4.1kB        4
f6e5d4c3b2a1   worker       78.91%    390MiB / 512MiB        76.17%   3.4kB / 1.2kB     12MB / 0B         9
</code></pre>



<p class="wp-block-paragraph">Press <code>Ctrl+C</code> to exit — this doesn&#8217;t stop any containers, only detaches the view.</p>



<h2 class="wp-block-heading">Scoping to Specific Containers</h2>



<pre class="wp-block-code"><code>docker stats web worker
</code></pre>



<p class="wp-block-paragraph">Or by pattern using shell expansion with <code>docker ps</code>:</p>



<pre class="wp-block-code"><code>docker stats $(docker ps --filter "name=web" --format "{{.Names}}")
</code></pre>



<h2 class="wp-block-heading">Getting a One-Time Snapshot (Not Live-Streaming)</h2>



<p class="wp-block-paragraph">By default <code>docker stats</code> streams continuously, which is awkward in scripts. Use <code>--no-stream</code> for a single sample:</p>



<pre class="wp-block-code"><code>docker stats --no-stream
</code></pre>



<p class="wp-block-paragraph">Expected output: the same table format, printed once, then the command exits immediately — ideal for cron jobs, health-check scripts, or logging snapshots at intervals.</p>



<h2 class="wp-block-heading">Machine-Readable Output With <code>--format</code></h2>



<p class="wp-block-paragraph">For scripting or feeding into a monitoring pipeline, use Go template formatting:</p>



<pre class="wp-block-code"><code>docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>NAME      CPU %     MEM USAGE / LIMIT     MEM %
web       2.34%     45.2MiB / 512MiB      8.83%
worker    78.91%    390MiB / 512MiB       76.17%
</code></pre>



<p class="wp-block-paragraph">For JSON (much easier to parse programmatically):</p>



<pre class="wp-block-code"><code>docker stats --no-stream --format '{{json .}}'
</code></pre>



<p class="wp-block-paragraph">Expected output — one JSON object per container:</p>



<pre class="wp-block-code"><code>{"BlockIO":"0B / 4.1kB","CPUPerc":"2.34%","Container":"a1b2c3d4e5f6","ID":"a1b2c3d4e5f6","MemPerc":"8.83%","MemUsage":"45.2MiB / 512MiB","Name":"web","NetIO":"1.2kB / 850B","PIDs":"4"}
</code></pre>



<p class="wp-block-paragraph">Pipe through <code>jq</code> to alert on thresholds:</p>



<pre class="wp-block-code"><code>docker stats --no-stream --format '{{json .}}' | jq -r 'select(.MemPerc | rtrimstr("%") | tonumber &gt; 75) | "\(.Name) is at \(.MemPerc) memory usage"'
</code></pre>



<p class="wp-block-paragraph">Expected output when a container crosses 75% memory:</p>



<pre class="wp-block-code"><code>worker is at 76.17% memory usage
</code></pre>



<h2 class="wp-block-heading">Scripted Polling for Monitoring Pipelines</h2>



<p class="wp-block-paragraph">A simple script that logs stats every 10 seconds to a CSV for later analysis:</p>



<pre class="wp-block-code"><code>#!/usr/bin/env bash
# poll-stats.sh
OUTFILE="container-stats.csv"
echo "timestamp,name,cpu_perc,mem_usage,mem_perc" &gt; "$OUTFILE"

while true; do
  ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
  docker stats --no-stream --format '{{.Name}},{{.CPUPerc}},{{.MemUsage}},{{.MemPerc}}' \
    | while IFS=, read -r name cpu mem_usage mem_perc; do
        echo "${ts},${name},${cpu},${mem_usage},${mem_perc}" &gt;&gt; "$OUTFILE"
      done
  sleep 10
done
</code></pre>



<pre class="wp-block-code"><code>chmod +x poll-stats.sh
nohup ./poll-stats.sh &amp;
</code></pre>



<p class="wp-block-paragraph">This is a lightweight starting point — for anything beyond ad-hoc analysis, feed the same data into Prometheus instead (see below), which handles retention, querying, and alerting far better than a flat CSV file.</p>



<h2 class="wp-block-heading">Inspecting a Single Container&#8217;s Raw cgroup Stats via the API</h2>



<p class="wp-block-paragraph"><code>docker stats</code> is actually a formatted view over the Docker Engine API&#8217;s <code>/containers/{id}/stats</code> endpoint, which returns raw cgroup accounting data. You can query it directly:</p>



<pre class="wp-block-code"><code>curl --unix-socket /var/run/docker.sock \
  "http:/containers/web/stats?stream=false" | jq .
</code></pre>



<p class="wp-block-paragraph">Expected output (trimmed): a large JSON object including <code>cpu_stats</code>, <code>precpu_stats</code>, <code>memory_stats</code>, <code>networks</code>, and <code>blkio_stats</code> — this is the raw data <code>docker stats</code> computes its percentages from. Useful when you need finer-grained fields than the CLI table exposes, like per-CPU usage or detailed memory breakdown (<code>cache</code>, <code>rss</code>, <code>swap</code>).</p>



<pre class="wp-block-code"><code>{
  "memory_stats": {
    "usage": 47431680,
    "limit": 536870912,
    "stats": {
      "cache": 2048000,
      "rss": 45383680
    }
  }
}
</code></pre>



<h2 class="wp-block-heading">How Docker Calculates These Numbers (cgroups Under the Hood)</h2>



<p class="wp-block-paragraph">Docker containers get their resource accounting from Linux <strong>control groups (cgroups)</strong> — the same kernel mechanism that enforces resource limits. Every container is assigned a cgroup, and the kernel tracks CPU time, memory usage, and I/O directly against it. Docker doesn&#8217;t measure anything itself; it reads these kernel-maintained counters and does the math.</p>



<p class="wp-block-paragraph"><strong>CPU %</strong> is calculated as:</p>



<pre class="wp-block-code"><code>cpu_delta = current_cpu_usage - previous_cpu_usage
system_delta = current_system_cpu_usage - previous_system_cpu_usage
CPU % = (cpu_delta / system_delta) * number_of_cpus * 100
</code></pre>



<p class="wp-block-paragraph">This is why CPU percentages can exceed 100% for multi-threaded containers on multi-core hosts — a container using 2 full cores shows as ~200%.</p>



<p class="wp-block-paragraph"><strong>MEM USAGE</strong> is the cgroup&#8217;s tracked memory, which by default <em>includes page cache</em> on cgroup v1 — meaning a container reading a lot of files from disk can show inflated memory usage that isn&#8217;t actually &#8220;at risk&#8221; memory the OOM killer would reclaim under pressure. On cgroup v2 (default on modern Linux distros and Docker 20.10+), this accounting is more accurate and generally excludes reclaimable cache from the pressure calculation, though <code>docker stats</code> display still includes cache in the raw usage number.</p>



<p class="wp-block-paragraph">Check which cgroup version your host uses:</p>



<pre class="wp-block-code"><code>docker info --format '{{.CgroupVersion}}'
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>2
</code></pre>



<h2 class="wp-block-heading">Setting Resource Limits (What &#8220;LIMIT&#8221; in the Output Means)</h2>



<p class="wp-block-paragraph">The <code>LIMIT</code> column reflects whatever you constrained the container to at run time — if unset, it defaults to the host&#8217;s total memory. Set explicit limits:</p>



<pre class="wp-block-code"><code>docker run -d --name limited-app \
  --memory=256m \
  --memory-swap=256m \
  --cpus="0.5" \
  nginx:alpine
</code></pre>



<pre class="wp-block-code"><code>docker stats limited-app --no-stream
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>NAME           CPU %     MEM USAGE / LIMIT     MEM %
limited-app    0.05%     3.2MiB / 256MiB        1.25%
</code></pre>



<p class="wp-block-paragraph"><code>--cpus="0.5"</code> caps the container to half a CPU core — under sustained load, <code>docker stats</code> CPU% for this container will plateau near 50% even though more capacity exists on the host, because the cgroup CPU quota is throttling it.</p>



<h2 class="wp-block-heading">Docker Compose Resource Limits and Stats</h2>



<pre class="wp-block-code"><code># docker-compose.yml
services:
  worker:
    image: myapp/worker:latest
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          memory: 256M
</code></pre>



<p class="wp-block-paragraph">Note: <code>deploy.resources</code> is honored by <code>docker compose up</code> for local development in recent Compose versions, and always honored in Swarm mode. Check applied limits:</p>



<pre class="wp-block-code"><code>docker inspect worker --format '{{.HostConfig.Memory}}'
</code></pre>



<h2 class="wp-block-heading">Feeding Stats Into Prometheus for Real Monitoring</h2>



<p class="wp-block-paragraph">For anything beyond ad-hoc checks, run <strong>cAdvisor</strong>, which exposes per-container resource metrics in Prometheus format — this is the standard approach in production rather than polling <code>docker stats</code> yourself:</p>



<pre class="wp-block-code"><code>docker run -d \
  --name cadvisor \
  --volume=/:/rootfs:ro \
  --volume=/var/run:/var/run:ro \
  --volume=/sys:/sys:ro \
  --volume=/var/lib/docker/:/var/lib/docker:ro \
  --volume=/dev/disk/:/dev/disk:ro \
  --publish=8080:8080 \
  --privileged \
  --device=/dev/kmsg \
  gcr.io/cadvisor/cadvisor:v0.49.1
</code></pre>



<p class="wp-block-paragraph">Verify metrics are exposed:</p>



<pre class="wp-block-code"><code>curl -s http://localhost:8080/metrics | grep container_memory_usage_bytes | head -3
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>container_memory_usage_bytes{container_label_...,name="web"} 4.7431680e+07
</code></pre>



<p class="wp-block-paragraph">Point a Prometheus scrape config at it:</p>



<pre class="wp-block-code"><code>scrape_configs:
  - job_name: 'cadvisor'
    static_configs:
      - targets: &#91;'localhost:8080']
</code></pre>



<p class="wp-block-paragraph">From there, Grafana dashboards and alerting rules (e.g., <code>container_memory_usage_bytes / container_spec_memory_limit_bytes &gt; 0.9</code>) give you the sustained, historical view that a live <code>docker stats</code> terminal session can&#8217;t.</p>



<h2 class="wp-block-heading">Security Considerations</h2>



<ul class="wp-block-list">
<li>cAdvisor requires broad host mounts and <code>--privileged</code> to read cgroup/proc data — restrict it to trusted internal networks and don&#8217;t expose port 8080 publicly without authentication in front of it (e.g., a reverse proxy with auth).</li>



<li>Access to <code>docker stats</code> requires Docker socket access, same root-equivalent caveat as every other command in this series — don&#8217;t grant <code>docker</code> group membership casually on shared/multi-tenant hosts.</li>



<li>Resource limits (<code>--memory</code>, <code>--cpus</code>) are a security boundary as much as a performance one — an unconstrained container can starve co-located workloads (noisy-neighbor problem) or exhaust host memory, triggering the OOM killer against unrelated processes.</li>
</ul>



<h2 class="wp-block-heading">Troubleshooting</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Symptom</th><th>Cause</th><th>Fix</th></tr></thead><tbody><tr><td><code>docker stats</code> shows <code>--</code> for all fields</td><td>Container just started, first sample not yet available</td><td>Wait one refresh cycle (~1s)</td></tr><tr><td>MEM % seems too high for what the app should use</td><td>cgroup v1 including page cache in usage</td><td>Check <code>docker info</code> cgroup version; consider upgrading host to cgroup v2</td></tr><tr><td>CPU % stuck near a fixed ceiling</td><td><code>--cpus</code> limit throttling the container</td><td>Check <code>docker inspect --format '{{.HostConfig.NanoCpus}}'</code></td></tr><tr><td><code>docker stats</code> hangs / very slow to update</td><td>Large number of containers on the host</td><td>Scope to specific containers instead of the full fleet</td></tr><tr><td>cAdvisor container fails to start</td><td>Missing <code>--privileged</code> or required host mounts</td><td>Re-check the exact volume/device flags in the run command above</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Real-World Deployment Notes</h2>



<p class="wp-block-paragraph"><code>docker stats</code> is the tool I reach for first because it needs zero setup — no exporter, no scrape config, just a running Docker daemon. That makes it ideal for the &#8220;something feels off, let me check right now&#8221; moment. But I&#8217;ve learned not to trust a single snapshot too much: a container can look fine at the instant you check and still have been thrashing memory five minutes earlier. That gap is exactly why cAdvisor-plus-Prometheus earns its place once a service matters enough to have an on-call rotation — it turns &#8220;what does this look like right now&#8221; into &#8220;what did this look like during the incident window,&#8221; which is almost always the more useful question during a postmortem.</p>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph"><code>docker stats</code> gives you a real-time window into what a container is actually doing on the host — CPU, memory, network, and block I/O — sourced directly from the kernel&#8217;s cgroup accounting rather than anything Docker computes independently. It&#8217;s the right tool for live debugging and quick sanity checks; for anything you need historical trends or alerting on, graduate to cAdvisor plus Prometheus and Grafana, which read the same underlying cgroup data but retain and query it properly over time.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://docs.docker.com/reference/cli/docker/container/stats/">Docker CLI Reference — docker stats</a></li>



<li><a href="https://docs.docker.com/reference/api/engine/version/v1.47/#tag/Container/operation/ContainerStats">Docker Engine API — Container Stats</a></li>



<li><a href="https://docs.docker.com/engine/containers/resource_constraints/">Docker Resource Constraints</a></li>



<li><a href="https://github.com/google/cadvisor">cAdvisor GitHub Repository</a></li>



<li><a href="https://prometheus.io/docs/introduction/overview/">Prometheus Documentation</a></li>



<li><a href="https://docs.kernel.org/admin-guide/cgroup-v2.html">Kernel Documentation — Control Groups v2</a></li>
</ul>
<p>The post <a href="https://awjunaid.com/docker/how-to-obtain-usage-statistics-of-a-running-container-docker-stats-and-resource-monitoring/">How to Obtain Usage Statistics of a Running Container: Docker Stats and Resource Monitoring</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/docker/how-to-obtain-usage-statistics-of-a-running-container-docker-stats-and-resource-monitoring/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4655</post-id>	</item>
		<item>
		<title>How to Get Detailed Information About a Container with Docker Inspect: Complete Guide</title>
		<link>https://awjunaid.com/docker/how-to-get-detailed-information-about-a-container-with-docker-inspect-complete-guide/</link>
					<comments>https://awjunaid.com/docker/how-to-get-detailed-information-about-a-container-with-docker-inspect-complete-guide/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sat, 02 Sep 2023 05:51:30 +0000</pubDate>
				<category><![CDATA[Docker]]></category>
		<category><![CDATA[docker]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=4652</guid>

					<description><![CDATA[<p>If you have spent any real time running containers in production, you already know that docker ps only&#8230;</p>
<p>The post <a href="https://awjunaid.com/docker/how-to-get-detailed-information-about-a-container-with-docker-inspect-complete-guide/">How to Get Detailed Information About a Container with Docker Inspect: Complete Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">If you have spent any real time running containers in production, you already know that <code>docker ps</code> only tells you half the story. It shows you that a container is running, its name, its ports, and how long it has been up — but it doesn&#8217;t tell you <em>why</em> a container is misbehaving, what network it&#8217;s actually attached to, what volumes are mounted where, or what environment variables it started with. That&#8217;s where <code>docker inspect</code> comes in.</p>



<p class="wp-block-paragraph">In this guide I&#8217;m going to walk through everything I use <code>docker inspect</code> for day to day — from the absolute basics to the advanced formatting tricks that let you pull exact fields out of the JSON output for use in scripts, monitoring tools, and CI/CD pipelines.</p>



<h2 class="wp-block-heading">What Is Docker Inspect?</h2>



<p class="wp-block-paragraph"><code>docker inspect</code> is a low-level Docker CLI command that returns detailed configuration and runtime information about Docker objects — containers, images, volumes, networks, and even Swarm services — in JSON format. Think of it as the &#8220;X-ray&#8221; command for anything Docker manages on your host.</p>



<p class="wp-block-paragraph">Under the hood, when you run <code>docker inspect</code>, the Docker CLI is really just calling the Docker Engine API&#8217;s <code>/containers/{id}/json</code> endpoint (or the equivalent endpoint for images, volumes, or networks) and pretty-printing the response. Everything you see in <code>docker inspect</code> output is metadata that the Docker daemon (<code>dockerd</code>) already tracks internally — inspect just exposes it to you.</p>



<h2 class="wp-block-heading">Basic Syntax</h2>



<pre class="wp-block-code"><code>docker inspect &#91;OPTIONS] NAME|ID &#91;NAME|ID...]
</code></pre>



<p class="wp-block-paragraph">You can pass one or more container names or IDs, and Docker will return a JSON array with one object per target.</p>



<p class="wp-block-paragraph">Let&#8217;s start with a real container so the examples aren&#8217;t abstract.</p>



<pre class="wp-block-code"><code>docker run -d --name web-test -p 8080:80 nginx:latest
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
a480a496ba95: Pull complete
...
Status: Downloaded newer image for nginx:latest
3f1a9c2e8b7d4e6f9a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f
</code></pre>



<p class="wp-block-paragraph">Now inspect it:</p>



<pre class="wp-block-code"><code>docker inspect web-test
</code></pre>



<p class="wp-block-paragraph">This dumps a large JSON document. The top-level keys you&#8217;ll see most often are:</p>



<ul class="wp-block-list">
<li><strong>Id</strong> — the full 64-character container ID</li>



<li><strong>Created</strong> — timestamp of creation</li>



<li><strong>Path</strong> and <strong>Args</strong> — the entrypoint and arguments</li>



<li><strong>State</strong> — running status, PID, exit code, health status</li>



<li><strong>Image</strong> — the image ID the container was created from</li>



<li><strong>NetworkSettings</strong> — IP address, gateway, ports, and network config</li>



<li><strong>Mounts</strong> — volumes and bind mounts attached to the container</li>



<li><strong>Config</strong> — environment variables, labels, working directory, exposed ports</li>



<li><strong>HostConfig</strong> — resource limits, restart policy, privileged mode, capabilities</li>
</ul>



<h2 class="wp-block-heading">Inspecting Specific Sections</h2>



<p class="wp-block-paragraph">Dumping the entire JSON blob is rarely useful on its own. The real power of <code>docker inspect</code> comes from the <code>--format</code> (or <code>-f</code>) flag, which uses Go&#8217;s <code>text/template</code> syntax to extract exactly the field you want.</p>



<h3 class="wp-block-heading">Get the container&#8217;s IP address</h3>



<pre class="wp-block-code"><code>docker inspect --format='{{.NetworkSettings.IPAddress}}' web-test
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>172.17.0.2
</code></pre>



<h3 class="wp-block-heading">Get the container&#8217;s status</h3>



<pre class="wp-block-code"><code>docker inspect --format='{{.State.Status}}' web-test
</code></pre>



<p class="wp-block-paragraph">Output:</p>



<pre class="wp-block-code"><code>running
</code></pre>



<h3 class="wp-block-heading">Get environment variables</h3>



<pre class="wp-block-code"><code>docker inspect --format='{{.Config.Env}}' web-test
</code></pre>



<p class="wp-block-paragraph">Output:</p>



<pre class="wp-block-code"><code>&#91;PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin NGINX_VERSION=1.27.0 ...]
</code></pre>



<h3 class="wp-block-heading">Get mounted volumes</h3>



<pre class="wp-block-code"><code>docker inspect --format='{{json .Mounts}}' web-test
</code></pre>



<p class="wp-block-paragraph">Wrapping a field in <code>json</code> forces Go&#8217;s template engine to render it as properly formatted JSON rather than Go&#8217;s default struct representation, which is especially handy for arrays and nested objects.</p>



<h3 class="wp-block-heading">Get restart policy</h3>



<pre class="wp-block-code"><code>docker inspect --format='{{.HostConfig.RestartPolicy.Name}}' web-test
</code></pre>



<h3 class="wp-block-heading">Get exposed ports and their host bindings</h3>



<pre class="wp-block-code"><code>docker inspect --format='{{json .NetworkSettings.Ports}}' web-test
</code></pre>



<p class="wp-block-paragraph">Output:</p>



<pre class="wp-block-code"><code>{"80/tcp":&#91;{"HostIp":"0.0.0.0","HostPort":"8080"}]}
</code></pre>



<h2 class="wp-block-heading">Filtering with <code>--format</code> and Ranges</h2>



<p class="wp-block-paragraph">Go templates support loops, which is useful when you have multiple containers attached to multiple networks, or several mounts.</p>



<pre class="wp-block-code"><code>docker inspect --format='{{range .Mounts}}{{.Source}} -&gt; {{.Destination}}{{"\n"}}{{end}}' web-test
</code></pre>



<p class="wp-block-paragraph">This iterates over every mount and prints the source and destination path on its own line — extremely useful when a container has five or six volumes and you just want a clean list instead of a JSON blob.</p>



<h2 class="wp-block-heading">Inspecting Images, Volumes, and Networks</h2>



<p class="wp-block-paragraph"><code>docker inspect</code> isn&#8217;t limited to containers. It&#8217;s a general-purpose object inspector.</p>



<pre class="wp-block-code"><code>docker inspect nginx:latest
docker inspect my-volume
docker inspect bridge
</code></pre>



<p class="wp-block-paragraph">For images, you&#8217;ll get layers, exposed ports, entrypoint, and the architecture the image was built for. For volumes, you get the mount point on the host filesystem and driver options. For networks, you get subnet, gateway, and which containers are currently attached.</p>



<h3 class="wp-block-heading">Example: Checking which containers are on a network</h3>



<pre class="wp-block-code"><code>docker inspect --format='{{range $k, $v := .Containers}}{{$v.Name}} {{end}}' bridge
</code></pre>



<h2 class="wp-block-heading">Using Docker Inspect for Health Checks</h2>



<p class="wp-block-paragraph">If your image defines a <code>HEALTHCHECK</code>, <code>docker inspect</code> is the primary way to query it programmatically.</p>



<pre class="wp-block-code"><code>docker inspect --format='{{json .State.Health}}' web-test
</code></pre>



<p class="wp-block-paragraph">Expected output (if a healthcheck is configured):</p>



<pre class="wp-block-code"><code>{"Status":"healthy","FailingStreak":0,"Log":&#91;{"Start":"2026-07-29T10:00:00Z","End":"2026-07-29T10:00:01Z","ExitCode":0,"Output":""}]}
</code></pre>



<p class="wp-block-paragraph">This is exactly the kind of field an orchestration script (or a custom Nagios/Prometheus exporter) would poll before deciding whether to route traffic to a container.</p>



<h2 class="wp-block-heading">Combining Inspect with jq</h2>



<p class="wp-block-paragraph">While Go templates are powerful, most engineers I know prefer to just dump raw JSON and pipe it through <code>jq</code> for anything beyond a single field.</p>



<pre class="wp-block-code"><code>docker inspect web-test | jq '.&#91;0].State'
</code></pre>



<pre class="wp-block-code"><code>docker inspect web-test | jq '.&#91;0].NetworkSettings.Networks | keys'
</code></pre>



<p class="wp-block-paragraph">This is often faster to write than remembering Go template syntax, and it composes well with other JSON-aware tooling in a CI pipeline.</p>



<h2 class="wp-block-heading">Real-World Use Cases</h2>



<p class="wp-block-paragraph"><strong>1. Debugging a container that keeps restarting</strong></p>



<pre class="wp-block-code"><code>docker inspect --format='{{.State.ExitCode}} {{.State.Error}}' flaky-app
</code></pre>



<p class="wp-block-paragraph"><strong>2. Verifying resource limits before a load test</strong></p>



<pre class="wp-block-code"><code>docker inspect --format='CPU: {{.HostConfig.NanoCpus}} Memory: {{.HostConfig.Memory}}' web-test
</code></pre>



<p class="wp-block-paragraph"><strong>3. Auditing security settings</strong></p>



<pre class="wp-block-code"><code>docker inspect --format='Privileged: {{.HostConfig.Privileged}} Capabilities: {{.HostConfig.CapAdd}}' web-test
</code></pre>



<p class="wp-block-paragraph">Running containers as privileged, or with unnecessary added capabilities, is one of the most common container security misconfigurations. Wiring <code>docker inspect</code> into an automated audit script (or using it inside a CI gate before deployment) catches this early.</p>



<p class="wp-block-paragraph"><strong>4. Extracting IPs for a monitoring inventory</strong></p>



<pre class="wp-block-code"><code>for c in $(docker ps -q); do
  echo "$(docker inspect --format='{{.Name}}' $c) $(docker inspect --format='{{.NetworkSettings.IPAddress}}' $c)"
done
</code></pre>



<h2 class="wp-block-heading">Docker Inspect and Container Internals</h2>



<p class="wp-block-paragraph">To really understand what <code>docker inspect</code> is showing you, it helps to understand what a container actually <em>is</em>. A Docker container is a set of Linux namespaces (PID, network, mount, UTS, IPC, user) combined with cgroups for resource limiting, layered together with a union filesystem (usually overlay2). <code>docker inspect</code>&#8216;s <code>GraphDriver</code> field shows you exactly which filesystem driver is in use and where the merged, upper, and lower directories live on disk:</p>



<pre class="wp-block-code"><code>docker inspect --format='{{json .GraphDriver}}' web-test
</code></pre>



<p class="wp-block-paragraph">Output:</p>



<pre class="wp-block-code"><code>{"Data":{"LowerDir":"/var/lib/docker/overlay2/.../diff","MergedDir":"/var/lib/docker/overlay2/.../merged","UpperDir":"/var/lib/docker/overlay2/.../diff","WorkDir":"/var/lib/docker/overlay2/.../work"},"Name":"overlay2"}
</code></pre>



<p class="wp-block-paragraph">This is the actual filesystem view the container process sees, assembled from immutable image layers plus a writable layer on top.</p>



<h2 class="wp-block-heading">Troubleshooting Tips</h2>



<ul class="wp-block-list">
<li><strong>Container exits immediately after start</strong> — check <code>State.ExitCode</code> and <code>State.Error</code> first.</li>



<li><strong>Networking issues between containers</strong> — compare <code>NetworkSettings.Networks</code> across the two containers to confirm they&#8217;re actually on the same Docker network, not just the same host.</li>



<li><strong>Volume data not persisting</strong> — check <code>Mounts[].Source</code> to confirm the path Docker is actually writing to, since a typo in a Compose file can silently create a new anonymous volume instead of reusing the one you expect.</li>



<li><strong>&#8220;No such object&#8221; error</strong> — this usually means you&#8217;re inspecting a container by a name that has already been removed; <code>docker ps -a</code> will confirm whether it still exists in a stopped state.</li>
</ul>



<h2 class="wp-block-heading">Best Practices</h2>



<ol class="wp-block-list">
<li>Always prefer <code>--format</code> or <code>jq</code> over eyeballing raw JSON in scripts — it&#8217;s faster and less error-prone.</li>



<li>Use <code>docker inspect</code> in your CI/CD pipeline as a post-deployment sanity check (verify image digest, environment variables, and port mappings match what you expect).</li>



<li>Don&#8217;t rely on <code>docker inspect</code> output format staying identical across major Docker Engine versions — some fields have been renamed or restructured over the years (for example, network settings pre- and post-<code>docker network</code> introduction in Docker 1.9).</li>



<li>Combine with <code>docker events</code> for real-time monitoring rather than polling <code>inspect</code> in a tight loop.</li>
</ol>



<h2 class="wp-block-heading">Docker Inspect Across Multiple Objects at Once</h2>



<p class="wp-block-paragraph">You&#8217;re not limited to inspecting one object per call. Pass several names or IDs together and Docker returns an array covering all of them:</p>



<pre class="wp-block-code"><code>docker inspect web-test nginx:latest bridge
</code></pre>



<p class="wp-block-paragraph">This is handy in scripts that need to correlate a container, the image it was built from, and the network it&#8217;s attached to in a single pass, rather than issuing three separate commands.</p>



<p class="wp-block-paragraph">You can also mix <code>docker ps -q</code> with <code>xargs</code> to inspect every running container in one shot:</p>



<pre class="wp-block-code"><code>docker inspect $(docker ps -q) | jq '&#91;.&#91;] | {name: .Name, image: .Config.Image, status: .State.Status}]'
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>&#91;
  {"name": "/web-test", "image": "nginx:latest", "status": "running"}
]
</code></pre>



<h2 class="wp-block-heading">Type-Specific Inspection with <code>--type</code></h2>



<p class="wp-block-paragraph">If you have a name collision between a container and a network (rare, but possible), you can disambiguate with <code>--type</code>:</p>



<pre class="wp-block-code"><code>docker inspect --type=container web-test
docker inspect --type=network web-test
</code></pre>



<p class="wp-block-paragraph">Valid values include <code>container</code>, <code>image</code>, <code>volume</code>, <code>network</code>, <code>plugin</code>, <code>node</code>, <code>service</code>, <code>task</code>, and <code>config</code> (the last few apply only in Swarm mode).</p>



<h2 class="wp-block-heading">Docker Inspect in Swarm Mode</h2>



<p class="wp-block-paragraph">If you&#8217;re running Docker Swarm rather than standalone containers, <code>docker inspect</code> extends naturally to services and tasks:</p>



<pre class="wp-block-code"><code>docker service inspect --pretty my-service
docker inspect $(docker ps -q --filter "label=com.docker.swarm.service.name=my-service")
</code></pre>



<p class="wp-block-paragraph">The <code>--pretty</code> flag on <code>docker service inspect</code> renders a human-readable summary instead of raw JSON — useful when you just need a quick sanity check of replica count, update policy, and published ports without piping through <code>jq</code>.</p>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>



<p class="wp-block-paragraph"><strong>Does <code>docker inspect</code> work on stopped containers?</strong> Yes. As long as the container hasn&#8217;t been removed (<code>docker rm</code>), <code>docker inspect</code> returns its full configuration, including its last known state and exit code — this is often the first command to run when debugging a container that crashed.</p>



<p class="wp-block-paragraph"><strong>Why does <code>NetworkSettings.IPAddress</code> show an empty string?</strong> This field is only populated for containers on the default bridge network. If your container is attached to a user-defined network (which is the norm with Compose), look under <code>NetworkSettings.Networks.&lt;network-name&gt;.IPAddress</code> instead.</p>



<p class="wp-block-paragraph"><strong>Can I use <code>docker inspect</code> to see what command was actually run inside the container?</strong> Yes — <code>Config.Cmd</code> and <code>Config.Entrypoint</code> show exactly what was configured, and <code>Path</code>/<code>Args</code> at the top level show what was actually executed as PID 1.</p>



<p class="wp-block-paragraph"><strong>Is there a performance cost to running <code>docker inspect</code> frequently?</strong> It&#8217;s a lightweight read against data the daemon already holds in memory, so occasional or even per-minute polling in a monitoring script is fine. Avoid tight loops (sub-second polling) against many containers simultaneously, since each call is still a full API round-trip.</p>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph"><code>docker inspect</code> is one of those commands that looks simple on the surface but becomes indispensable once you start using its <code>--format</code> flag or piping into <code>jq</code>. It&#8217;s the fastest way to answer &#8220;what is actually going on inside this container&#8221; — IP addresses, mounts, environment variables, health status, resource limits, and security context all live in that one JSON document. Once you&#8217;re comfortable extracting specific fields, you can wire <code>docker inspect</code> into monitoring scripts, CI/CD gates, and security audits with very little extra tooling.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li>Docker CLI reference: https://docs.docker.com/reference/cli/docker/inspect/</li>



<li>Docker Engine API reference: https://docs.docker.com/reference/api/engine/</li>



<li>Docker storage drivers documentation: https://docs.docker.com/storage/storagedriver/</li>



<li>Go <code>text/template</code> package documentation: https://pkg.go.dev/text/template</li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/docker/how-to-get-detailed-information-about-a-container-with-docker-inspect-complete-guide/">How to Get Detailed Information About a Container with Docker Inspect: Complete Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/docker/how-to-get-detailed-information-about-a-container-with-docker-inspect-complete-guide/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4652</post-id>	</item>
		<item>
		<title>How to Run a Cloud Provider CLI in a Docker Container: AWS, Azure, and GCP Setup Guide</title>
		<link>https://awjunaid.com/docker/how-to-run-a-cloud-provider-cli-in-a-docker-container-aws-azure-and-gcp-setup-guide/</link>
					<comments>https://awjunaid.com/docker/how-to-run-a-cloud-provider-cli-in-a-docker-container-aws-azure-and-gcp-setup-guide/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sat, 02 Sep 2023 05:46:32 +0000</pubDate>
				<category><![CDATA[Docker]]></category>
		<category><![CDATA[docker]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=4649</guid>

					<description><![CDATA[<p>I got tired, a long time ago, of installing the AWS CLI on one laptop, the Azure CLI&#8230;</p>
<p>The post <a href="https://awjunaid.com/docker/how-to-run-a-cloud-provider-cli-in-a-docker-container-aws-azure-and-gcp-setup-guide/">How to Run a Cloud Provider CLI in a Docker Container: AWS, Azure, and GCP Setup Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I got tired, a long time ago, of installing the AWS CLI on one laptop, the Azure CLI on another, and then discovering version mismatches between what&#8217;s on my machine and what&#8217;s on a teammate&#8217;s machine when we&#8217;re debugging the same deployment. The fix I landed on, and the one most DevOps teams eventually land on too, is to stop installing cloud CLIs locally altogether and run them from containers instead.</p>



<p class="wp-block-paragraph">This guide covers exactly how to do that for AWS, Azure, and GCP — including authentication, credential persistence, and building your own multi-cloud CLI image.</p>



<h2 class="wp-block-heading">Why Run Cloud CLIs in Containers?</h2>



<p class="wp-block-paragraph">Before the how, the why:</p>



<ul class="wp-block-list">
<li><strong>No local pollution.</strong> Python version conflicts, Node dependency clashes, and CLI tool version drift never touch your host machine.</li>



<li><strong>Reproducibility.</strong> Everyone on the team runs the exact same CLI version, pinned by image tag.</li>



<li><strong>Portability.</strong> The same container works identically on Linux, macOS, and Windows (via Docker Desktop).</li>



<li><strong>CI/CD friendliness.</strong> The same image you use locally can be dropped straight into a GitLab CI or GitHub Actions job.</li>



<li><strong>Easy version pinning.</strong> Need to test against an older CLI version for a legacy pipeline? Just pull a different tag.</li>
</ul>



<h2 class="wp-block-heading">AWS CLI in a Container</h2>



<p class="wp-block-paragraph">AWS publishes an official CLI image.</p>



<pre class="wp-block-code"><code>docker pull amazon/aws-cli:2.17.0
</code></pre>



<h3 class="wp-block-heading">Running a Single Command</h3>



<pre class="wp-block-code"><code>docker run --rm amazon/aws-cli:2.17.0 --version
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>aws-cli/2.17.0 Python/3.11.6 Linux/6.1.0 exe/x86_64.docker
</code></pre>



<h3 class="wp-block-heading">Passing Credentials</h3>



<p class="wp-block-paragraph">The cleanest way is to mount your existing <code>~/.aws</code> directory as read-only:</p>



<pre class="wp-block-code"><code>docker run --rm -it \
  -v ~/.aws:/root/.aws:ro \
  amazon/aws-cli:2.17.0 s3 ls
</code></pre>



<p class="wp-block-paragraph">Expected output (assuming you have buckets):</p>



<pre class="wp-block-code"><code>2026-01-15 09:32:11 my-app-logs
2026-03-02 14:11:47 terraform-state-prod
</code></pre>



<p class="wp-block-paragraph">Alternatively, pass credentials as environment variables — useful in CI where you don&#8217;t want a mounted config directory:</p>



<pre class="wp-block-code"><code>docker run --rm \
  -e AWS_ACCESS_KEY_ID=AKIA... \
  -e AWS_SECRET_ACCESS_KEY=... \
  -e AWS_DEFAULT_REGION=us-east-1 \
  amazon/aws-cli:2.17.0 ec2 describe-instances
</code></pre>



<h3 class="wp-block-heading">Creating a Shell Alias</h3>



<p class="wp-block-paragraph">To make the container feel like a native binary, add this to your <code>.bashrc</code> or <code>.zshrc</code>:</p>



<pre class="wp-block-code"><code>alias aws='docker run --rm -it -v ~/.aws:/root/.aws:ro -v "$PWD":/aws amazon/aws-cli:2.17.0'
</code></pre>



<p class="wp-block-paragraph">Now <code>aws s3 ls</code> works exactly like the local binary would, transparently running inside a container.</p>



<h2 class="wp-block-heading">Azure CLI in a Container</h2>



<p class="wp-block-paragraph">Microsoft publishes an official Azure CLI image.</p>



<pre class="wp-block-code"><code>docker pull mcr.microsoft.com/azure-cli:2.63.0
</code></pre>



<h3 class="wp-block-heading">Interactive Login</h3>



<pre class="wp-block-code"><code>docker run -it mcr.microsoft.com/azure-cli:2.63.0 az login
</code></pre>



<p class="wp-block-paragraph">This prints a device-code URL and code:</p>



<pre class="wp-block-code"><code>To sign in, use a web browser to open the page https://microsoft.com/devicelogin
and enter the code ABCD1234 to authenticate.
</code></pre>



<h3 class="wp-block-heading">Persisting Login Across Container Runs</h3>



<p class="wp-block-paragraph">By default, once the container exits, your login session is gone. Persist the <code>~/.azure</code> config directory with a named volume:</p>



<pre class="wp-block-code"><code>docker volume create azure-cli-config

docker run -it \
  -v azure-cli-config:/root/.azure \
  mcr.microsoft.com/azure-cli:2.63.0 az login
</code></pre>



<p class="wp-block-paragraph">Subsequent runs reuse that same volume and stay authenticated:</p>



<pre class="wp-block-code"><code>docker run --rm \
  -v azure-cli-config:/root/.azure \
  mcr.microsoft.com/azure-cli:2.63.0 az account show
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>{
  "environmentName": "AzureCloud",
  "id": "b1234567-89ab-cdef-0123-456789abcdef",
  "isDefault": true,
  "name": "Pay-As-You-Go",
  "user": {
    "name": "you@example.com",
    "type": "user"
  }
}
</code></pre>



<h3 class="wp-block-heading">Service Principal Login (Non-Interactive, CI-Friendly)</h3>



<pre class="wp-block-code"><code>docker run --rm mcr.microsoft.com/azure-cli:2.63.0 az login \
  --service-principal \
  -u $AZURE_CLIENT_ID \
  -p $AZURE_CLIENT_SECRET \
  --tenant $AZURE_TENANT_ID
</code></pre>



<h2 class="wp-block-heading">Google Cloud CLI (gcloud) in a Container</h2>



<p class="wp-block-paragraph">Google publishes official images under <code>gcr.io/google.com/cloudsdktool/google-cloud-cli</code>.</p>



<pre class="wp-block-code"><code>docker pull gcr.io/google.com/cloudsdktool/google-cloud-cli:latest
</code></pre>



<h3 class="wp-block-heading">Authenticating</h3>



<pre class="wp-block-code"><code>docker run -it \
  -v gcloud-config:/root/.config/gcloud \
  gcr.io/google.com/cloudsdktool/google-cloud-cli:latest \
  gcloud auth login
</code></pre>



<h3 class="wp-block-heading">Using a Service Account Key</h3>



<p class="wp-block-paragraph">For automation, mount a service account JSON key file and activate it:</p>



<pre class="wp-block-code"><code>docker run --rm \
  -v /path/to/key.json:/tmp/key.json:ro \
  gcr.io/google.com/cloudsdktool/google-cloud-cli:latest \
  gcloud auth activate-service-account --key-file=/tmp/key.json
</code></pre>



<h3 class="wp-block-heading">Listing Compute Instances</h3>



<pre class="wp-block-code"><code>docker run --rm \
  -v gcloud-config:/root/.config/gcloud \
  gcr.io/google.com/cloudsdktool/google-cloud-cli:latest \
  gcloud compute instances list
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>NAME        ZONE           MACHINE_TYPE   STATUS
web-server  us-central1-a  e2-medium      RUNNING
</code></pre>



<h2 class="wp-block-heading">Building a Single &#8220;Multi-Cloud&#8221; CLI Image</h2>



<p class="wp-block-paragraph">Rather than switching between three separate images, many teams build one image with all three CLIs installed, so a single container can talk to AWS, Azure, and GCP in the same script.</p>



<pre class="wp-block-code"><code>FROM debian:bookworm-slim

RUN apt-get update &amp;&amp; apt-get install -y \
    curl unzip python3 python3-pip apt-transport-https \
    lsb-release gnupg ca-certificates &amp;&amp; \
    rm -rf /var/lib/apt/lists/*

# AWS CLI v2
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" &amp;&amp; \
    unzip awscliv2.zip &amp;&amp; ./aws/install &amp;&amp; rm -rf awscliv2.zip aws

# Azure CLI
RUN curl -sL https://aka.ms/InstallAzureCLIDeb | bash

# Google Cloud CLI
RUN curl -sSL https://sdk.cloud.google.com | bash -s -- --disable-prompts &amp;&amp; \
    ln -s /root/google-cloud-sdk/bin/gcloud /usr/local/bin/gcloud

WORKDIR /workspace
ENTRYPOINT &#91;"/bin/bash"]
</code></pre>



<p class="wp-block-paragraph">Build and run it:</p>



<pre class="wp-block-code"><code>docker build -t multicloud-cli:1.0 .
docker run --rm -it multicloud-cli:1.0
</code></pre>



<p class="wp-block-paragraph">Inside the container, verify all three:</p>



<pre class="wp-block-code"><code>aws --version
az --version
gcloud --version
</code></pre>



<h2 class="wp-block-heading">Using Compose to Manage Credential Volumes</h2>



<p class="wp-block-paragraph">If you regularly switch between clouds, a <code>docker-compose.yml</code> makes this cleaner than remembering long <code>docker run</code> commands:</p>



<pre class="wp-block-code"><code>services:
  aws-cli:
    image: amazon/aws-cli:2.17.0
    volumes:
      - ~/.aws:/root/.aws:ro
    entrypoint: aws

  azure-cli:
    image: mcr.microsoft.com/azure-cli:2.63.0
    volumes:
      - azure-config:/root/.azure

  gcloud-cli:
    image: gcr.io/google.com/cloudsdktool/google-cloud-cli:latest
    volumes:
      - gcloud-config:/root/.config/gcloud

volumes:
  azure-config:
  gcloud-config:
</code></pre>



<p class="wp-block-paragraph">Run any of them with:</p>



<pre class="wp-block-code"><code>docker compose run --rm aws-cli s3 ls
docker compose run --rm azure-cli az group list
docker compose run --rm gcloud-cli gcloud projects list
</code></pre>



<h2 class="wp-block-heading">Understanding What&#8217;s Actually Happening Under the Hood</h2>



<p class="wp-block-paragraph">It&#8217;s worth being clear about what containerizing a CLI does and doesn&#8217;t change. The <code>aws</code>, <code>az</code>, and <code>gcloud</code> binaries are ordinary programs — running them inside a container doesn&#8217;t make them faster or give them special access to anything. What changes is <em>isolation</em>: the container gets its own filesystem layer, its own set of installed dependencies, and (unless you mount your credential directory in) no access to your host&#8217;s files at all by default. That last point is exactly why the <code>-v ~/.aws:/root/.aws:ro</code> pattern matters — without it, the CLI inside the container has no way to find your credentials, because containers don&#8217;t inherit anything from the host filesystem unless you explicitly mount it.</p>



<p class="wp-block-paragraph">This also explains why <code>--rm</code> shows up in almost every example above: without it, Docker leaves a stopped container behind after every single command, and running dozens of <code>aws s3 ls</code> calls a day would otherwise leave dozens of dead containers cluttering <code>docker ps -a</code>.</p>



<h2 class="wp-block-heading">Handling Output Files and Local Directories</h2>



<p class="wp-block-paragraph">A common need is running a CLI command that writes output to a local file — for example, downloading an S3 object or exporting a Terraform plan. Since the container has its own isolated filesystem, you need to mount your working directory in as well as your credentials:</p>



<pre class="wp-block-code"><code>docker run --rm \
  -v ~/.aws:/root/.aws:ro \
  -v "$PWD":/workspace \
  -w /workspace \
  amazon/aws-cli:2.17.0 s3 cp s3://my-bucket/report.csv ./report.csv
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>download: s3://my-bucket/report.csv to ./report.csv
</code></pre>



<p class="wp-block-paragraph">The <code>-w /workspace</code> flag sets the container&#8217;s working directory to match the mounted volume, so relative paths in the command behave the way you&#8217;d expect.</p>



<h2 class="wp-block-heading">Integrating into CI/CD Pipelines</h2>



<p class="wp-block-paragraph">The exact same images work as pipeline steps. A GitHub Actions example using the AWS CLI container directly (rather than the <code>aws-actions/configure-aws-credentials</code> action) looks like this:</p>



<pre class="wp-block-code"><code>jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Sync to S3
        run: |
          docker run --rm \
            -e AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} \
            -e AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} \
            -e AWS_DEFAULT_REGION=us-east-1 \
            -v "$PWD":/workspace -w /workspace \
            amazon/aws-cli:2.17.0 s3 sync ./dist s3://my-app-bucket
</code></pre>



<p class="wp-block-paragraph">The advantage over installing the CLI directly in the CI runner is version pinning — the pipeline behaves identically regardless of what&#8217;s baked into the runner image, and upgrading the CLI version is a one-line tag change.</p>



<h2 class="wp-block-heading">Performance Considerations</h2>



<p class="wp-block-paragraph">Containerized CLIs add a small amount of overhead versus a natively installed binary — mainly image pull time on first use (subsequent runs use the cached image) and a few hundred milliseconds of container startup per invocation. For interactive use this is imperceptible; for scripts issuing hundreds of CLI calls in a tight loop, consider starting a single long-lived container and issuing multiple commands inside it via <code>docker exec</code>, rather than paying the startup cost per call:</p>



<pre class="wp-block-code"><code>docker run -d --name aws-session --entrypoint sleep \
  -v ~/.aws:/root/.aws:ro amazon/aws-cli:2.17.0 infinity

docker exec aws-session aws s3 ls
docker exec aws-session aws ec2 describe-instances

docker stop aws-session &amp;&amp; docker rm aws-session
</code></pre>



<h2 class="wp-block-heading">Security Considerations</h2>



<ul class="wp-block-list">
<li><strong>Never bake credentials into an image.</strong> Layers are cached and can leak secrets even after deletion from later layers.</li>



<li><strong>Prefer named volumes or bind mounts</strong> for credential storage over environment variables when running interactively, since environment variables are visible via <code>docker inspect</code> and process listings on the host.</li>



<li><strong>Use short-lived tokens where possible</strong> — AWS STS assume-role sessions, Azure managed identities, and GCP workload identity federation all reduce the blast radius of a leaked credential.</li>



<li><strong>Mount credential directories read-only (<code>:ro</code>)</strong> whenever the CLI doesn&#8217;t need to write back to them.</li>



<li><strong>Rotate CI service principal / service account credentials</strong> regularly, and scope their IAM permissions to the minimum required for the pipeline.</li>
</ul>



<h2 class="wp-block-heading">Troubleshooting</h2>



<ul class="wp-block-list">
<li><strong>&#8220;Unable to locate credentials&#8221; (AWS)</strong> — double check the mount path matches the CLI&#8217;s expected config location (<code>/root/.aws</code> for the <code>root</code> user inside the container, not <code>/home/user/.aws</code> unless you changed the container&#8217;s user).</li>



<li><strong>Azure login opens a browser but the container has no browser</strong> — this is expected; use the device code flow (<code>az login</code> without <code>--service-principal</code>) shown above, or use a service principal in headless environments.</li>



<li><strong>gcloud commands fail with <code>PERMISSION_DENIED</code></strong> — verify the active account/project with <code>gcloud config list</code> before assuming the credentials themselves are bad.</li>



<li><strong>Volume permissions errors on Linux hosts</strong> — if the container runs as a non-root user internally, you may need to <code>chown</code> the mounted directory or pass <code>--user $(id -u):$(id -g)</code> to match host file ownership.</li>
</ul>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph">Running cloud CLIs inside Docker containers removes an entire category of &#8220;works on my machine&#8221; problems from multi-cloud DevOps workflows. Official images exist for AWS, Azure, and GCP, authentication can be persisted across runs using named volumes, and it&#8217;s straightforward to combine all three into a single multi-cloud utility image for scripts that need to touch more than one provider. The same pattern scales cleanly into CI/CD, where these exact images can be reused as pipeline steps.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li>AWS CLI Docker image: https://hub.docker.com/r/amazon/aws-cli</li>



<li>Azure CLI Docker image: https://learn.microsoft.com/en-us/cli/azure/run-azure-cli-docker</li>



<li>Google Cloud CLI Docker image: https://cloud.google.com/sdk/docs/downloads-docker</li>



<li>Docker Compose file reference: https://docs.docker.com/reference/compose-file/</li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/docker/how-to-run-a-cloud-provider-cli-in-a-docker-container-aws-azure-and-gcp-setup-guide/">How to Run a Cloud Provider CLI in a Docker Container: AWS, Azure, and GCP Setup Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/docker/how-to-run-a-cloud-provider-cli-in-a-docker-container-aws-azure-and-gcp-setup-guide/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4649</post-id>	</item>
		<item>
		<title>How to Start a Docker Host on Microsoft Azure: Complete Deployment and Configuration Guide</title>
		<link>https://awjunaid.com/docker/how-to-start-a-docker-host-on-microsoft-azure-complete-deployment-and-configuration-guide/</link>
					<comments>https://awjunaid.com/docker/how-to-start-a-docker-host-on-microsoft-azure-complete-deployment-and-configuration-guide/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sat, 02 Sep 2023 05:44:22 +0000</pubDate>
				<category><![CDATA[Docker]]></category>
		<category><![CDATA[docker]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=4646</guid>

					<description><![CDATA[<p>Running Docker locally is fine for development, but sooner or later you need a real remote host —&#8230;</p>
<p>The post <a href="https://awjunaid.com/docker/how-to-start-a-docker-host-on-microsoft-azure-complete-deployment-and-configuration-guide/">How to Start a Docker Host on Microsoft Azure: Complete Deployment and Configuration Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Running Docker locally is fine for development, but sooner or later you need a real remote host — something with a public IP, real compute, and enough resources to run more than a couple of containers at once. Azure is one of the three big options for that, and this guide walks through provisioning a Docker-ready VM on Azure from scratch, securing it properly, and connecting to its Docker daemon remotely.</p>



<h2 class="wp-block-heading">Prerequisites</h2>



<ul class="wp-block-list">
<li>An Azure account with an active subscription</li>



<li>Azure CLI installed locally, or run it via container (see the companion guide on running cloud CLIs in Docker)</li>



<li>SSH key pair for authentication (<code>ssh-keygen -t ed25519</code>)</li>
</ul>



<h2 class="wp-block-heading">Step 1: Log In and Set Your Subscription</h2>



<pre class="wp-block-code"><code>az login
az account list --output table
az account set --subscription "Pay-As-You-Go"
</code></pre>



<h2 class="wp-block-heading">Step 2: Create a Resource Group</h2>



<p class="wp-block-paragraph">Resource groups are Azure&#8217;s logical container for related resources — keeping everything under one group makes cleanup trivial later.</p>



<pre class="wp-block-code"><code>az group create --name docker-rg --location eastus
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>{
  "id": "/subscriptions/xxxx/resourceGroups/docker-rg",
  "location": "eastus",
  "name": "docker-rg",
  "properties": {
    "provisioningState": "Succeeded"
  }
}
</code></pre>



<h2 class="wp-block-heading">Step 3: Create a Virtual Machine</h2>



<p class="wp-block-paragraph">Azure has a marketplace image for Docker-ready hosts, but I generally prefer starting from plain Ubuntu and installing Docker myself via cloud-init — it keeps the setup transparent and reproducible.</p>



<pre class="wp-block-code"><code>az vm create \
  --resource-group docker-rg \
  --name docker-host-01 \
  --image Ubuntu2404 \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --custom-data cloud-init-docker.yml
</code></pre>



<p class="wp-block-paragraph">Where <code>cloud-init-docker.yml</code> looks like this:</p>



<pre class="wp-block-code"><code>#cloud-config
package_update: true
package_upgrade: true
runcmd:
  - curl -fsSL https://get.docker.com -o get-docker.sh
  - sh get-docker.sh
  - usermod -aG docker azureuser
  - systemctl enable docker
  - systemctl start docker
</code></pre>



<p class="wp-block-paragraph">Expected output after a minute or two:</p>



<pre class="wp-block-code"><code>{
  "fqdns": "",
  "location": "eastus",
  "publicIpAddress": "20.185.44.112",
  "resourceGroup": "docker-rg"
}
</code></pre>



<h2 class="wp-block-heading">Step 4: Open the Required Network Ports</h2>



<p class="wp-block-paragraph">By default Azure VMs allow SSH (22) but nothing else. Open port 2376 if you plan to expose the Docker daemon remotely (with TLS — never without), and any application ports you need.</p>



<pre class="wp-block-code"><code>az vm open-port --resource-group docker-rg --name docker-host-01 --port 2376 --priority 900
az vm open-port --resource-group docker-rg --name docker-host-01 --port 80 --priority 910
</code></pre>



<h2 class="wp-block-heading">Step 5: SSH In and Verify Docker</h2>



<pre class="wp-block-code"><code>ssh azureuser@20.185.44.112
docker --version
docker run hello-world
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>Docker version 27.3.1, build ce12230
...
Hello from Docker!
This message shows that your installation appears to be working correctly.
</code></pre>



<h2 class="wp-block-heading">Step 6: Securing Remote Docker API Access (Optional but Important)</h2>



<p class="wp-block-paragraph">If you want to run <code>docker</code> commands against this host from your laptop (rather than SSHing in every time), you need to expose the Docker daemon over TCP — and that must be done with TLS certificates, never in plaintext, since an open Docker socket is equivalent to root access on the host.</p>



<p class="wp-block-paragraph">On the VM, generate a CA and server certificates:</p>



<pre class="wp-block-code"><code>mkdir -p ~/docker-certs &amp;&amp; cd ~/docker-certs
openssl genrsa -aes256 -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
openssl genrsa -out server-key.pem 4096
openssl req -subj "/CN=$(hostname)" -sha256 -new -key server-key.pem -out server.csr
echo subjectAltName = IP:20.185.44.112,IP:127.0.0.1 &gt; extfile.cnf
openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \
  -CAcreateserial -out server-cert.pem -extfile extfile.cnf
</code></pre>



<p class="wp-block-paragraph">Then configure the Docker daemon to use TLS by editing <code>/etc/docker/daemon.json</code>:</p>



<pre class="wp-block-code"><code>{
  "hosts": &#91;"unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"],
  "tls": true,
  "tlsverify": true,
  "tlscacert": "/etc/docker/ca.pem",
  "tlscert": "/etc/docker/server-cert.pem",
  "tlskey": "/etc/docker/server-key.pem"
}
</code></pre>



<p class="wp-block-paragraph">Restart Docker:</p>



<pre class="wp-block-code"><code>sudo systemctl restart docker
</code></pre>



<p class="wp-block-paragraph">On your laptop, generate a client cert signed by the same CA and connect:</p>



<pre class="wp-block-code"><code>export DOCKER_HOST=tcp://20.185.44.112:2376
export DOCKER_TLS_VERIFY=1
export DOCKER_CERT_PATH=~/docker-certs/client

docker version
</code></pre>



<h2 class="wp-block-heading">Step 7: Attach a Managed Disk for Persistent Storage</h2>



<p class="wp-block-paragraph">If your containers need persistent storage beyond the OS disk, attach and mount an Azure managed disk.</p>



<pre class="wp-block-code"><code>az disk create \
  --resource-group docker-rg \
  --name docker-data-disk \
  --size-gb 64 \
  --sku Premium_LRS

az vm disk attach \
  --resource-group docker-rg \
  --vm-name docker-host-01 \
  --name docker-data-disk
</code></pre>



<p class="wp-block-paragraph">On the VM:</p>



<pre class="wp-block-code"><code>sudo lsblk
sudo mkfs.ext4 /dev/sdc
sudo mkdir /mnt/docker-data
sudo mount /dev/sdc /mnt/docker-data
</code></pre>



<p class="wp-block-paragraph">Then point Docker&#8217;s data root at this disk by adding <code>"data-root": "/mnt/docker-data/docker"</code> to <code>/etc/docker/daemon.json</code> and restarting Docker.</p>



<h2 class="wp-block-heading">Networking Fundamentals on This Host</h2>



<p class="wp-block-paragraph">Once Docker is running on the Azure VM, container networking behaves exactly as it does anywhere else — the <code>docker0</code> bridge handles container-to-container traffic on the host, and published ports (<code>-p 80:80</code>) are what Azure&#8217;s Network Security Group rules need to allow through externally. It&#8217;s a common point of confusion: opening a port in the NSG doesn&#8217;t automatically mean a container is listening on it — you still need <code>-p</code> (or a Compose <code>ports:</code> entry) to actually bind the container&#8217;s port to the host.</p>



<h2 class="wp-block-heading">Running a Real Workload</h2>



<pre class="wp-block-code"><code>docker run -d --name nginx-demo -p 80:80 --restart unless-stopped nginx:latest
</code></pre>



<p class="wp-block-paragraph">Visit <code>http://20.185.44.112</code> in a browser — you should see the default Nginx welcome page.</p>



<h2 class="wp-block-heading">Monitoring the Host</h2>



<pre class="wp-block-code"><code>docker stats --no-stream
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>CONTAINER ID   NAME          CPU %   MEM USAGE / LIMIT   MEM %   NET I/O
a1b2c3d4e5f6   nginx-demo    0.00%   2.4MiB / 1.91GiB    0.12%   1.2kB / 0B
</code></pre>



<p class="wp-block-paragraph">For anything beyond ad-hoc checks, install <code>cadvisor</code> or point Azure Monitor&#8217;s VM insights at the host for CPU, memory, and disk metrics over time.</p>



<h2 class="wp-block-heading">Deploying with Docker Compose on the Azure VM</h2>



<p class="wp-block-paragraph">Most real workloads involve more than a single container, and Compose is usually the fastest way to define them. Install the Compose plugin (already included if you used the <code>get-docker.sh</code> convenience script above) and define a stack:</p>



<pre class="wp-block-code"><code># docker-compose.yml
services:
  web:
    image: nginx:latest
    ports:
      - "80:80"
    restart: unless-stopped
    depends_on:
      - api

  api:
    image: myregistry.example.com/api:1.0.0
    environment:
      - DATABASE_URL=postgresql://appuser:apppass@db:5432/appdb
    restart: unless-stopped

  db:
    image: postgres:16
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=appdb
      - POSTGRES_USER=appuser
      - POSTGRES_PASSWORD=apppass
    restart: unless-stopped

volumes:
  db-data:
</code></pre>



<pre class="wp-block-code"><code>docker compose up -d
</code></pre>



<p class="wp-block-paragraph">Expected output:</p>



<pre class="wp-block-code"><code>&#91;+] Running 4/4
 ✔ Network azureuser_default   Created
 ✔ Container azureuser-db-1    Started
 ✔ Container azureuser-api-1   Started
 ✔ Container azureuser-web-1   Started
</code></pre>



<h2 class="wp-block-heading">Using an Azure Load Balancer for High Availability</h2>



<p class="wp-block-paragraph">A single VM is a single point of failure. For anything beyond a dev/test environment, put an Azure Load Balancer or Application Gateway in front of two or more identically configured Docker hosts, using a Virtual Machine Scale Set (VMSS) so Azure can add or remove instances automatically based on load:</p>



<pre class="wp-block-code"><code>az vmss create \
  --resource-group docker-rg \
  --name docker-vmss \
  --image Ubuntu2404 \
  --custom-data cloud-init-docker.yml \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --instance-count 2 \
  --lb-sku Standard \
  --public-ip-per-vm false
</code></pre>



<p class="wp-block-paragraph">Each instance in the scale set boots with the same cloud-init script, so Docker and your container startup logic come up identically on every node.</p>



<h2 class="wp-block-heading">Understanding Azure&#8217;s Networking Layers</h2>



<p class="wp-block-paragraph">It helps to keep Azure&#8217;s network layers distinct in your head, since they each control a different thing:</p>



<ul class="wp-block-list">
<li><strong>Virtual Network (VNet) and Subnets</strong> — define the private IP address space the VM lives in.</li>



<li><strong>Network Security Group (NSG)</strong> — the stateful firewall controlling inbound/outbound traffic to the VM&#8217;s network interface; this is what <code>az vm open-port</code> modifies.</li>



<li><strong>Docker&#8217;s own bridge network</strong> — entirely internal to the VM, invisible to Azure; controls how containers talk to each other and to the host.</li>



<li><strong>Published ports (<code>-p</code>)</strong> — the bridge between the two; a container port isn&#8217;t reachable from the VNet or the internet until it&#8217;s published to the host, and the host port isn&#8217;t reachable externally until the NSG allows it.</li>
</ul>



<p class="wp-block-paragraph">Missing any one of these three layers is the most common reason a &#8220;working&#8221; container is unreachable from outside the VM.</p>



<h2 class="wp-block-heading">Best Practices</h2>



<ol class="wp-block-list">
<li><strong>Never expose the Docker API without TLS.</strong> An unauthenticated Docker socket over the network is a full remote-root vulnerability.</li>



<li><strong>Use Azure Network Security Groups as a second layer</strong> on top of TLS — restrict port 2376 to known IP ranges rather than <code>0.0.0.0/0</code>.</li>



<li><strong>Enable automatic OS patching</strong> via Azure&#8217;s VM guest patching settings, or handle it yourself with unattended-upgrades.</li>



<li><strong>Use a managed disk for <code>/var/lib/docker</code></strong> if you expect meaningful image/container storage growth — resizing the OS disk later is more disruptive.</li>



<li><strong>Tag resources</strong> (<code>--tags env=prod team=platform</code>) from day one; cleanup and cost attribution get painful otherwise.</li>



<li><strong>Consider Azure Container Instances or AKS</strong> instead of a raw VM if you don&#8217;t specifically need daemon-level control — a VM is the right choice when you need custom daemon config, privileged containers, or GPU passthrough.</li>
</ol>



<h2 class="wp-block-heading">Troubleshooting</h2>



<ul class="wp-block-list">
<li><strong>VM creation fails with quota errors</strong> — check <code>az vm list-usage --location eastus --output table</code> for your subscription&#8217;s vCPU quota.</li>



<li><strong>Can&#8217;t SSH in</strong> — confirm the NSG allows port 22 from your IP, and that you&#8217;re using the correct username (<code>azureuser</code> by default in the example above).</li>



<li><strong><code>docker run hello-world</code> hangs</strong> — check <code>systemctl status docker</code> on the VM; cloud-init logs at <code>/var/log/cloud-init-output.log</code> will show if the install script failed.</li>



<li><strong>Remote TLS connection refused from laptop</strong> — confirm port 2376 is open in the NSG and that <code>daemon.json</code> was loaded correctly (<code>sudo systemctl status docker</code> will show a parse error if the JSON is malformed).</li>
</ul>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph">Standing up a Docker host on Azure is a matter of provisioning a VM (via cloud-init for a repeatable setup), opening the right ports, and — if remote access is needed — securing the Docker API with TLS certificates rather than leaving it open. From there, container networking, storage, and monitoring on Azure behave exactly like they would on any Linux Docker host; the cloud-specific work is really just the provisioning and network security layer around it.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li>Azure CLI documentation: https://learn.microsoft.com/en-us/cli/azure/</li>



<li>Docker Engine installation guide: https://docs.docker.com/engine/install/ubuntu/</li>



<li>Docker Engine API security (TLS): https://docs.docker.com/engine/security/protect-access/</li>



<li>Azure Virtual Machines documentation: https://learn.microsoft.com/en-us/azure/virtual-machines/</li>
</ul>
<p>The post <a href="https://awjunaid.com/docker/how-to-start-a-docker-host-on-microsoft-azure-complete-deployment-and-configuration-guide/">How to Start a Docker Host on Microsoft Azure: Complete Deployment and Configuration Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/docker/how-to-start-a-docker-host-on-microsoft-azure-complete-deployment-and-configuration-guide/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4646</post-id>	</item>
	</channel>
</rss>
