<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>docker · wleeaf field notes</title><link>https://articles.wleeaf.dev/tags/docker/</link><description>Hands-on guides and build logs from the wleeaf studio: self-hosting, Linux, networking, Docker, the command line and reverse engineering.</description><language>en-us</language><lastBuildDate>Sun, 29 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://articles.wleeaf.dev/tags/docker/feed.xml" rel="self" type="application/rss+xml"/><item><title>Debugging Running Docker Containers Without Restarting Them</title><link>https://articles.wleeaf.dev/posts/docker-debugging-running-containers/</link><pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/docker-debugging-running-containers/</guid><description>exec, logs, inspect, and nsenter. Four tools for figuring out what is happening inside a container without touching it.</description><category>docker</category><category>debugging</category><category>linux</category><content:encoded><![CDATA[<p>Something is wrong with your container. It is running but not behaving. You could rebuild and redeploy, but that might hide the problem. Here is how to investigate a running container without restarting it, using <code>docker exec</code>, <code>docker logs</code>, <code>docker inspect</code>, <code>docker cp</code>, <code>nsenter</code> and <code>docker diff</code>.</p>
<h2 id="docker-exec">docker exec<a class="anchor" href="#docker-exec" aria-label="Link to “docker exec”">#</a></h2>
<p>The obvious one. Drop into a shell:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker <span class="nb">exec</span> -it mycontainer sh</span></span></code></pre></div>
</div>
<p>Use <code>sh</code> not <code>bash</code> because Alpine images do not have bash. Once inside, you can poke around:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Check what processes are running</span>
</span></span><span class="line"><span class="cl">ps aux
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Look at environment variables</span>
</span></span><span class="line"><span class="cl">env <span class="p">|</span> sort
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Test network connectivity</span>
</span></span><span class="line"><span class="cl">wget -qO- http://other-service:3000/health
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check disk usage</span>
</span></span><span class="line"><span class="cl">df -h</span></span></code></pre></div>
</div>
<p>But what if the container has no shell? Minimal images built <code>FROM scratch</code> or distroless images strip everything out.</p>
<h2 id="docker-logs">docker logs<a class="anchor" href="#docker-logs" aria-label="Link to “docker logs”">#</a></h2>
<p>Before exec-ing into anything, check the logs:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Last 100 lines</span>
</span></span><span class="line"><span class="cl">docker logs --tail <span class="m">100</span> mycontainer
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Follow in real time</span>
</span></span><span class="line"><span class="cl">docker logs -f mycontainer
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Logs since a specific time</span>
</span></span><span class="line"><span class="cl">docker logs --since 2026-03-29T10:00:00 mycontainer
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># With timestamps</span>
</span></span><span class="line"><span class="cl">docker logs -t mycontainer</span></span></code></pre></div>
</div>
<p>The <code>--since</code> flag is underrated. When a container has been running for weeks, you do not want to scroll through megabytes of old logs.</p>
<h2 id="docker-inspect">docker inspect<a class="anchor" href="#docker-inspect" aria-label="Link to “docker inspect”">#</a></h2>
<p>This tells you everything about the container&rsquo;s configuration:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Full JSON dump</span>
</span></span><span class="line"><span class="cl">docker inspect mycontainer
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Just the IP address</span>
</span></span><span class="line"><span class="cl">docker inspect -f <span class="s1">&#39;{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}&#39;</span> mycontainer
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Environment variables</span>
</span></span><span class="line"><span class="cl">docker inspect -f <span class="s1">&#39;{{json .Config.Env}}&#39;</span> mycontainer <span class="p">|</span> jq .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Mount points</span>
</span></span><span class="line"><span class="cl">docker inspect -f <span class="s1">&#39;{{json .Mounts}}&#39;</span> mycontainer <span class="p">|</span> jq .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Port mappings</span>
</span></span><span class="line"><span class="cl">docker inspect -f <span class="s1">&#39;{{json .NetworkSettings.Ports}}&#39;</span> mycontainer <span class="p">|</span> jq .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Health check status</span>
</span></span><span class="line"><span class="cl">docker inspect -f <span class="s1">&#39;{{json .State.Health}}&#39;</span> mycontainer <span class="p">|</span> jq .</span></span></code></pre></div>
</div>
<h2 id="docker-cp">docker cp<a class="anchor" href="#docker-cp" aria-label="Link to “docker cp”">#</a></h2>
<p>Copy files in or out of a running container:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Copy a config file out for inspection</span>
</span></span><span class="line"><span class="cl">docker cp mycontainer:/etc/nginx/nginx.conf ./nginx.conf
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Copy a fix in</span>
</span></span><span class="line"><span class="cl">docker cp ./fixed-config.json mycontainer:/app/config.json</span></span></code></pre></div>
</div>
<p>This works even if the container has no shell or text editor.</p>
<h2 id="nsenter-the-nuclear-option">nsenter: The Nuclear Option<a class="anchor" href="#nsenter-the-nuclear-option" aria-label="Link to “nsenter: The Nuclear Option”">#</a></h2>
<p>When <code>docker exec</code> is not enough or the container has no shell, <code>nsenter</code> lets you enter the container&rsquo;s namespaces from the host:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Get the container&#39;s PID</span>
</span></span><span class="line"><span class="cl"><span class="nv">PID</span><span class="o">=</span><span class="k">$(</span>docker inspect -f <span class="s1">&#39;{{.State.Pid}}&#39;</span> mycontainer<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Enter its network namespace</span>
</span></span><span class="line"><span class="cl">sudo nsenter -t <span class="nv">$PID</span> -n ss -tlnp
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Enter all namespaces (like exec but from the host)</span>
</span></span><span class="line"><span class="cl">sudo nsenter -t <span class="nv">$PID</span> -m -u -i -n -p -- /bin/sh</span></span></code></pre></div>
</div>
<p>This is useful for network debugging. You can run <code>tcpdump</code>, <code>ss</code>, or <code>ip</code> inside the container&rsquo;s network namespace even if those tools are not installed in the container.</p>
<h2 id="docker-diff">docker diff<a class="anchor" href="#docker-diff" aria-label="Link to “docker diff”">#</a></h2>
<p>See what files changed since the container started:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker diff mycontainer</span></span></code></pre></div>
</div>
<p>Output looks like:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>C /app
A /app/data/cache.db
C /tmp
A /tmp/something-unexpected</code></pre>
</div>
<p><code>A</code> means added, <code>C</code> means changed, <code>D</code> means deleted. This is great for finding containers that are writing to unexpected places.</p>
<h2 id="debugging-network-issues">Debugging Network Issues<a class="anchor" href="#debugging-network-issues" aria-label="Link to “Debugging Network Issues”">#</a></h2>
<p>When containers cannot talk to each other:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Check which networks a container is on</span>
</span></span><span class="line"><span class="cl">docker inspect mycontainer -f <span class="s1">&#39;{{json .NetworkSettings.Networks}}&#39;</span> <span class="p">|</span> jq <span class="s1">&#39;keys&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Ping from one container to another</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> web ping -c <span class="m">3</span> api
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check DNS resolution</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> web nslookup api
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># List all containers on a network</span>
</span></span><span class="line"><span class="cl">docker network inspect mynetwork -f <span class="s1">&#39;{{range .Containers}}{{.Name}} {{.IPv4Address}}{{end}}&#39;</span></span></span></code></pre></div>
</div>
<h2 id="one-liner-health-checks">One-Liner Health Checks<a class="anchor" href="#one-liner-health-checks" aria-label="Link to “One-Liner Health Checks”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Which containers are unhealthy?</span>
</span></span><span class="line"><span class="cl">docker ps --filter <span class="nv">health</span><span class="o">=</span>unhealthy --format <span class="s1">&#39;{{.Names}}: {{.Status}}&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Which containers restarted recently?</span>
</span></span><span class="line"><span class="cl">docker ps --format <span class="s1">&#39;{{.Names}}: {{.Status}}&#39;</span> <span class="p">|</span> grep -i restarting
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Resource usage snapshot</span>
</span></span><span class="line"><span class="cl">docker stats --no-stream --format <span class="s1">&#39;table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}&#39;</span></span></span></code></pre></div>
</div>
<p>The instinct to restart a broken container is strong. But nine times out of ten, the information you need to fix the actual problem is sitting right there in the logs, the inspect output, or the filesystem diff. Restarting just resets the evidence.</p>
]]></content:encoded></item><item><title>Running 15 Services on One VPS With Docker Compose</title><link>https://articles.wleeaf.dev/posts/docker-compose-single-server/</link><pubDate>Sun, 22 Mar 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/docker-compose-single-server/</guid><description>How a single compose file, two networks, and some naming discipline keeps everything manageable.</description><category>docker</category><category>infrastructure</category><category>self-hosting</category><content:encoded><![CDATA[<p>At some point I looked at my server and counted fifteen containers. A reverse proxy, a git server, a CI runner, a game server, a mail server, a chat platform, a webmail client, a database, a VPN, an auth portal, an admin panel, a music bot, a TURN server, and a couple of static site builders. All on one four-core VPS with eight gigs of RAM.</p>
<p>It sounds like a mess, but it is actually pretty clean. The trick is structure: one compose file, two networks and some naming discipline.</p>
<h2 id="one-compose-file-two-networks">One Compose File, Two Networks<a class="anchor" href="#one-compose-file-two-networks" aria-label="Link to “One Compose File, Two Networks”">#</a></h2>
<p>Every service lives in a single docker-compose.yml. I tried splitting it into multiple compose files early on and it was a nightmare. Services that need to talk to each other end up on different networks, you forget which file defines what, and debugging becomes archaeology.</p>
<p>One file means one place to look. The trade-off is that the file is about 200 lines long, but that is way better than hunting through five separate files.</p>
<p>The networking is simple: two Docker bridge networks. The first one connects Caddy to the internet. The second one connects Caddy to every backend service. Services that need to be reached by Caddy join the backend network. Services that need direct port access to the internet get explicit port mappings.</p>
<p>The WireGuard container is the exception. It runs with host networking because it needs raw access to the network stack for the VPN tunnel. Everything else uses the bridge networks.</p>
<h2 id="container-naming">Container Naming<a class="anchor" href="#container-naming" aria-label="Link to “Container Naming”">#</a></h2>
<p>I name every container explicitly with <code>container_name</code>. Docker Compose generates names like <code>wleeaf-caddy-1</code> by default, which is ugly in logs and hard to type in commands. Explicit names mean I can do <code>docker logs caddy</code> instead of <code>docker logs wleeaf-caddy-1</code>.</p>
<p>This also matters for DNS resolution inside Docker networks. When you give a container a name, other containers on the same network can reach it by that name. So my Caddy config just says <code>reverse_proxy gitea:3000</code> and Docker handles the resolution.</p>
<h2 id="resource-reality">Resource Reality<a class="anchor" href="#resource-reality" aria-label="Link to “Resource Reality”">#</a></h2>
<p>People overestimate how much RAM you need. Here is what my containers actually use:</p>
<p>The reverse proxy sits at 15 megabytes. The git server uses about 110. The mail server is the hungriest at 160. The Matrix homeserver varies between 100 and 500 depending on federation activity. The database backing it uses 70. Everything else is under 50 each.</p>
<p>Total memory usage hovers around 2.5 gigabytes. That leaves five gigs free for spikes, builds, and the occasional agent session eating a gig for a few minutes.</p>
<p>CPU is basically idle. The load average rarely goes above 0.1 unless someone is building a Docker image or running a CI job. Four cores is overkill for this workload.</p>
<h2 id="secrets-management">Secrets Management<a class="anchor" href="#secrets-management" aria-label="Link to “Secrets Management”">#</a></h2>
<p>I keep secrets in a .env file next to the compose file. Docker Compose reads it automatically. Database passwords, API tokens, and hashed passwords all live there. The compose file references them with the dollar-sign-curly-brace syntax.</p>
<p>The .env file is not in any git repo. I back it up manually. Some people use Docker secrets or Vault for this, but for a single-server personal setup, a .env file is fine.</p>
<h2 id="restart-policies">Restart Policies<a class="anchor" href="#restart-policies" aria-label="Link to “Restart Policies”">#</a></h2>
<p>Every service has <code>restart: unless-stopped</code>. This means containers come back after a reboot or a crash, but stay down if I explicitly stop them. I used to use <code>always</code> but that fights you when you are debugging, because the container keeps restarting while you are trying to read the logs.</p>
<p>The one exception is the CI runner. It has <code>restart: unless-stopped</code> too, but I sometimes need to stop it to free resources during heavy builds. With <code>unless-stopped</code>, it stays down until I bring it back up.</p>
<h2 id="volume-strategy">Volume Strategy<a class="anchor" href="#volume-strategy" aria-label="Link to “Volume Strategy”">#</a></h2>
<p>I mount named directories from the host filesystem, not Docker volumes. Every service gets a directory under the main project folder. Caddy gets <code>./caddy</code>, Gitea gets <code>./gitea</code>, the mail server gets <code>./stalwart</code>, and so on.</p>
<p>This makes backups trivial. I can tar the entire project directory and I have everything: configs, data, and the compose file itself. Docker volumes are fine, but they hide the data in a location you have to look up every time.</p>
<h2 id="the-upgrade-dance">The Upgrade Dance<a class="anchor" href="#the-upgrade-dance" aria-label="Link to “The Upgrade Dance”">#</a></h2>
<p>Upgrading a service usually means changing the image tag and running <code>docker compose up -d</code>. For services using <code>latest</code>, I pull first with <code>docker compose pull</code> and then bring them up.</p>
<p>I do not use Watchtower or any automatic update tool. I want to know exactly when things change. An automatic update at 3 AM that breaks the mail server is not a problem I want to have.</p>
<p>The whole setup has been running for months with essentially zero unplanned downtime. The only time services go down is when I am actively working on them. For a personal infrastructure project, that is more than good enough.</p>
]]></content:encoded></item></channel></rss>