<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: David Tio</title>
    <description>The latest articles on DEV Community by David Tio (@davidtio).</description>
    <link>https://dev.to/davidtio</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3804844%2F9b6ee07a-d847-4296-af23-d07335a2a638.jpg</url>
      <title>DEV Community: David Tio</title>
      <link>https://dev.to/davidtio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/davidtio"/>
    <language>en</language>
    <item>
      <title>Docker Compose Explained: One File, One Container (2026)</title>
      <dc:creator>David Tio</dc:creator>
      <pubDate>Mon, 13 Apr 2026 02:18:49 +0000</pubDate>
      <link>https://dev.to/davidtio/docker-compose-explained-one-file-one-container-2026-38m6</link>
      <guid>https://dev.to/davidtio/docker-compose-explained-one-file-one-container-2026-38m6</guid>
      <description>&lt;h2&gt;
  
  
  🐳 Docker Compose Explained: One File, One Container (2026)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Quick one-liner:&lt;/strong&gt; Replace &lt;code&gt;docker run&lt;/code&gt; commands with a &lt;code&gt;docker-compose.yml&lt;/code&gt; file. One command to start or tear down any container, reproducibly, every time.&lt;/p&gt;




&lt;h3&gt;
  
  
  🤔 Why This Matters
&lt;/h3&gt;

&lt;p&gt;In the &lt;a href="https://blog.dtio.app/2026/04/docker-networking-explained.html" rel="noopener noreferrer"&gt;last post&lt;/a&gt;, you connected containers by building a custom bridge network and running CloudBeaver + PostgreSQL by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker network create dtstack
&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; dtpg &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--network&lt;/span&gt; dtstack &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;testdb &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; pgdata:/var/lib/postgresql/data &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--tmpfs&lt;/span&gt; /var/run/postgresql &lt;span class="se"&gt;\&lt;/span&gt;
    postgres:17
&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; cloudbeaver &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--network&lt;/span&gt; dtstack &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-p&lt;/span&gt; 8978:8978 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; cbdata:/opt/cloudbeaver/workspace &lt;span class="se"&gt;\&lt;/span&gt;
    dbeaver/cloudbeaver:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three commands. That's not the problem.&lt;/p&gt;

&lt;p&gt;The problem is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The second command is a 150-character wall of flags&lt;/li&gt;
&lt;li&gt;One typo in &lt;code&gt;--tmpfs&lt;/code&gt; and PostgreSQL silently starts but won't accept connections&lt;/li&gt;
&lt;li&gt;Forget &lt;code&gt;--network dtstack&lt;/code&gt; and the containers won't find each other&lt;/li&gt;
&lt;li&gt;Tear it down and rebuild? Type it all again&lt;/li&gt;
&lt;li&gt;What about when you have 5 containers? 10?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a better way.&lt;/p&gt;

&lt;p&gt;Docker Compose lets you define this entire stack in a single YAML file:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;One command. Same result. Every time.&lt;/p&gt;

&lt;p&gt;Here's how it works. Instead of typing flags every time, you write a &lt;code&gt;docker-compose.yml&lt;/code&gt; file that captures everything. You list the image, ports, volumes, environment variables, and networks. Then you run &lt;code&gt;docker compose up -d&lt;/code&gt; and Docker does the rest. Start it, stop it, tear it down. All with one command.&lt;/p&gt;

&lt;p&gt;We'll start by composing each of our containers individually. One compose file for PostgreSQL. One for CloudBeaver. You'll get comfortable with the &lt;code&gt;up&lt;/code&gt;/&lt;code&gt;ps&lt;/code&gt;/&lt;code&gt;logs&lt;/code&gt;/&lt;code&gt;down&lt;/code&gt; workflow.&lt;/p&gt;

&lt;p&gt;By the end of this post, you'll never have to stare at another never-ending line of &lt;code&gt;docker run&lt;/code&gt; flags again.&lt;/p&gt;




&lt;h3&gt;
  
  
  ✅ Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ep 1-6 completed.&lt;/strong&gt; Docker is installed and running, you know volumes, networking, and port mapping. Rootless mode recommended.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Compose plugin.&lt;/strong&gt; Already installed as part of Blog-01/02. Just run &lt;code&gt;docker compose version&lt;/code&gt; to verify.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Compose v2:&lt;/strong&gt; The old &lt;code&gt;docker-compose&lt;/code&gt; (with hyphen) is deprecated. Modern Docker ships &lt;code&gt;docker compose&lt;/code&gt; (space) as a plugin. If &lt;code&gt;docker compose version&lt;/code&gt; doesn't work, go back and re-run the installation steps in Blog-01 or Blog-02. The plugin was included there.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  📦 Your First docker-compose.yml
&lt;/h3&gt;

&lt;p&gt;Create a directory for your PostgreSQL service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; dtstack-pg &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;dtstack-pg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dtpg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dtpg&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;testdb&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;tmpfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/postgresql&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Four things to notice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;services:&lt;/code&gt; is the top-level key.&lt;/strong&gt; Each entry under &lt;code&gt;services:&lt;/code&gt; is one container. We have one, and it's called &lt;code&gt;dtpg&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;container_name&lt;/code&gt; gives it a clean name.&lt;/strong&gt; Instead of Compose's auto-generated &lt;code&gt;dtstack-pg-dtpg-1&lt;/code&gt;, we get &lt;code&gt;dtpg&lt;/code&gt;. Same as &lt;code&gt;--name&lt;/code&gt; in &lt;code&gt;docker run&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No &lt;code&gt;--network&lt;/code&gt; flag.&lt;/strong&gt; The network is implicit. We're not connecting to anything else yet. One container, one service.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Volumes are declared at the bottom.&lt;/strong&gt; Named volumes are defined in the &lt;code&gt;volumes:&lt;/code&gt; block and referenced by the service. Docker creates them on first use.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  🚀 Start the Service
&lt;/h3&gt;



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

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[+] Running 3/3
 ✔ Network dtstack-pg_default  Created
 ✔ Volume dtstack-pg_pgdata    Created
 ✔ Container dtpg              Started
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command creates a container, a network, and a volume. Everything you need.&lt;/p&gt;

&lt;p&gt;Verify it's up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose ps
NAME   IMAGE         COMMAND                  SERVICE   CREATED         STATUS         PORTS
dtpg   postgres:17   &lt;span class="s2"&gt;"docker-entrypoint.s…"&lt;/span&gt;   dtpg      54 seconds ago  Up 54 seconds  5432/tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🔍 Inspect the Service
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;View logs:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;dtpg | PostgreSQL init process complete;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ready &lt;span class="k"&gt;for &lt;/span&gt;start up.
&lt;span class="go"&gt;dtpg | database system is ready to accept connections
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Follow logs in real-time (like &lt;code&gt;docker logs -f&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;Ctrl-C&lt;/code&gt; to stop following.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connect and verify:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;dtpg psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT version();"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;                                                      &lt;span class="k"&gt;version&lt;/span&gt;
&lt;span class="c1"&gt;--------------------------------------------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;PostgreSQL&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Debian&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pgdg13&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;x86_64&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pc&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;linux&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;gnu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compiled&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;gcc&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Debian&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;bit&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL is running. We used &lt;code&gt;dtpg&lt;/code&gt; to target the container, and Compose knows exactly which one to hit.&lt;/p&gt;

&lt;p&gt;Let's bring it down before we make changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[+] Running 2/2
 ✔ Container dtpg              Removed
 ✔ Network dtstack-pg_default  Removed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The volume survives. Your data is safe.&lt;/p&gt;




&lt;h3&gt;
  
  
  📁 Using Environment Files
&lt;/h3&gt;

&lt;p&gt;Hardcoding passwords in YAML is bad practice. Move secrets to a &lt;code&gt;.env&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
POSTGRES_PASSWORD=docker
POSTGRES_DB=testdb
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update &lt;code&gt;docker-compose.yml&lt;/code&gt; to reference them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dtpg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dtpg&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;tmpfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/postgresql&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;docker compose up -d&lt;/code&gt; reads the variables automatically. Same command, cleaner file.&lt;/p&gt;




&lt;h3&gt;
  
  
  🛑 Tear It Down
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[+] Running 2/2
 ✔ Container dtpg              Removed
 ✔ Network dtstack-pg_default   Removed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container and network are gone, but the volume survives. Your data is still right where you left it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker volume &lt;span class="nb"&gt;ls&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;dtstack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local  dtstack-pg_pgdata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To remove the volume too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose down &lt;span class="nt"&gt;--volumes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[+] Running 1/1
 ✔ Volume dtstack-pg_pgdata  Removed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;--volumes&lt;/code&gt; when you want a clean slate. Leave it off when you want data to survive across restarts.&lt;/p&gt;




&lt;h3&gt;
  
  
  📦 Second Compose File: CloudBeaver
&lt;/h3&gt;

&lt;p&gt;Now let's do the same for CloudBeaver. It gets its own directory and its own compose file.&lt;/p&gt;

&lt;p&gt;First, go back to your home directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create the CloudBeaver directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; dtstack-cb &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;dtstack-cb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cloudbeaver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloudbeaver&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbeaver/cloudbeaver:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8978:8978"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cbdata:/opt/cloudbeaver/workspace&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Start it:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[+] Running 3/3
 ✔ Network dtstack-cb_default  Created
 ✔ Volume dtstack-cb_cbdata    Created
 ✔ Container cloudbeaver       Started
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:8978&lt;/code&gt;. CloudBeaver loads. ✅&lt;/p&gt;

&lt;p&gt;But there's no PostgreSQL on this network. CloudBeaver and PG live in &lt;strong&gt;separate compose projects&lt;/strong&gt;. Different directories, different networks. They can't talk to each other yet.&lt;/p&gt;

&lt;p&gt;Déjà vu. We solved this exact problem in the last post with custom bridge networks. Same concept, but this time we're doing it through Compose. We'll get there next post.&lt;/p&gt;

&lt;p&gt;For now, let's clean up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose down &lt;span class="nt"&gt;--volumes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  📋 Docker Run vs Docker Compose
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;&lt;code&gt;docker run&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;docker compose&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Start&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker run -d --name x --network n ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker ps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose ps&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker logs x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose logs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exec&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker exec -it x sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose exec x sh&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker stop x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose down&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker network create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;docker compose&lt;/code&gt; commands are scoped to your project. &lt;code&gt;docker compose ps&lt;/code&gt; only shows your stack's containers. It won't list everything running on your machine.&lt;/p&gt;




&lt;h3&gt;
  
  
  🧪 Exercise: Build Your Nextcloud Stack with Compose
&lt;/h3&gt;

&lt;p&gt;Nextcloud is a self-hosted productivity platform. It functions just like Google Docs, but it runs on your own server. It needs four services: a database, a cache, a web server, and a PHP backend. You'll create four compose files, one per service, each in its own directory.&lt;/p&gt;

&lt;p&gt;First, go back to your home directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Part 1: MariaDB
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; nc-db &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;nc-db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
MYSQL_ROOT_PASSWORD=nextcloud
MYSQL_DATABASE=nextcloud
MYSQL_USER=nextcloud
MYSQL_PASSWORD=nextcloud
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nc-db&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mariadb:11&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3306:3306"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_ROOT_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_DATABASE}&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_USER}&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dbdata:/var/lib/mysql&lt;/span&gt;

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

&lt;/div&gt;





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

&lt;/div&gt;



&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;db mariadb &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-pnextcloud&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SHOW DATABASES;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;--------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Database&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;--------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;mysql&lt;/span&gt;              &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;nextcloud&lt;/span&gt;          &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;performance_schema&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;                &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;--------------------+&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose down &lt;span class="nt"&gt;--volumes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Part 2: Redis
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; nc-redis &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;nc-redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nc-redis&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:8.6&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6379:6379"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redisdata:/data&lt;/span&gt;

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

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;redis redis-cli PING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should get &lt;code&gt;PONG&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose down &lt;span class="nt"&gt;--volumes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Part 3: Nextcloud PHP-FPM
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; nc-php &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;nc-php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;php&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nc-php&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud:fpm&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9000:9000"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./html:/var/www/html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





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

&lt;/div&gt;



&lt;p&gt;Nextcloud's PHP-FPM image comes with Nextcloud pre-installed. On first start, it runs its setup scripts and copies the app files into the bind-mounted &lt;code&gt;html/&lt;/code&gt; directory. You can see it populate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;html/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see Nextcloud's file structure. Things like &lt;code&gt;index.php&lt;/code&gt;, &lt;code&gt;core/&lt;/code&gt;, &lt;code&gt;apps/&lt;/code&gt;, &lt;code&gt;config/&lt;/code&gt;. The container put everything there for you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Part 4: Nginx
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; nc-nginx &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;nc-nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nginx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nc-nginx&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./html:/usr/share/nginx/html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; html
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; html/index.html &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
&amp;lt;h2&amp;gt;Nextcloud is coming&amp;lt;/h2&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;nginx curl localhost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;&amp;lt;h2&amp;gt;Nextcloud is coming&amp;lt;/h2&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;👉 &lt;strong&gt;Coming up:&lt;/strong&gt; This isn't a full Nextcloud deployment yet, but you now have all the containers you need to get it running. Next post, we'll glue them all up and get it working. See you then.&lt;/p&gt;

&lt;p&gt;📚 &lt;strong&gt;Want More?&lt;/strong&gt; This guide covers the basics from &lt;strong&gt;Chapter 11: Using Docker Compose&lt;/strong&gt; in my book, &lt;em&gt;"Levelling Up with Docker"&lt;/em&gt;. That's 14 chapters of practical, hands-on Docker guides.&lt;/p&gt;

&lt;p&gt;&amp;gt; &lt;strong&gt;Note:&lt;/strong&gt; The book has more content than this blog series. Some topics are only available in the book.&lt;/p&gt;

&lt;p&gt;📚 &lt;strong&gt;Grab the book:&lt;/strong&gt; &lt;a href="https://www.amazon.com/dp/B0GGZ76PHW" rel="noopener noreferrer"&gt;"Levelling Up with Docker" on Amazon&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Found this helpful?&lt;/strong&gt; 🙌&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://linkedin.com/in/davidtio" rel="noopener noreferrer"&gt;Share with your network&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twitter:&lt;/strong&gt; &lt;a href="https://x.com/intent/tweet?text=Just%20learned%20Docker%20Compose!&amp;amp;url=https://blog.dtio.app/2026/04/docker-compose-explained-one-file-one-container.html" rel="noopener noreferrer"&gt;Tweet about it&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Questions?&lt;/strong&gt; Drop a comment below or reach out on LinkedIn&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>compose</category>
      <category>linux</category>
      <category>devops</category>
    </item>
    <item>
      <title>Building a Blog Platform with Docker #2: Tailwind CSS</title>
      <dc:creator>David Tio</dc:creator>
      <pubDate>Sun, 12 Apr 2026 00:16:36 +0000</pubDate>
      <link>https://dev.to/davidtio/building-a-blog-platform-with-docker-2-tailwind-css-3be9</link>
      <guid>https://dev.to/davidtio/building-a-blog-platform-with-docker-2-tailwind-css-3be9</guid>
      <description>&lt;h1&gt;
  
  
  Building a Blog Platform with Docker #2: Tailwind CSS
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Quick one-liner:&lt;/strong&gt; Upgrade your Flask app to Tailwind CSS via CDN — dark theme with teal branding, no build step required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Last time, you got a basic Flask app running with a separate CSS file. It worked — dark background, teal heading, centered layout.&lt;/p&gt;

&lt;p&gt;But let's be honest — it's nothing like a blog yet. One centered heading. One paragraph. Nothing a reader would take seriously.&lt;/p&gt;

&lt;p&gt;Today, we're making it look a lot more like a blog. We're adding Tailwind CSS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Tailwind?&lt;/strong&gt; You could write custom CSS. Or use Bootstrap. Or Bulma. But Tailwind is fast, modern, and easy to customise. Plus, it's what I'll use for the final blog.dtio.app, so you're learning the actual stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No build step.&lt;/strong&gt; I'm not making you install Node.js, npm, and a whole build pipeline just for CSS. We're using Tailwind via CDN. It's not production-optimal, but for learning? Perfect.&lt;/p&gt;

&lt;p&gt;By the end of this post, you'll have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tailwind CSS loaded via CDN&lt;/li&gt;
&lt;li&gt;Google Fonts (DM Sans + Instrument Serif)&lt;/li&gt;
&lt;li&gt;A teal navigation bar&lt;/li&gt;
&lt;li&gt;A centered hero section&lt;/li&gt;
&lt;li&gt;An editorial post list&lt;/li&gt;
&lt;li&gt;A teal footer&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Starting Point
&lt;/h2&gt;

&lt;p&gt;You should have this from last time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tiohub-blog/
├── app.py
├── static/
│   └── css/
│       └── style.css
└── templates/
    └── index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your &lt;code&gt;index.html&lt;/code&gt; currently links to &lt;code&gt;static/css/style.css&lt;/code&gt; via a &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tag in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you don't have this, go back and build Episode 1 first. This one builds on it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Add Tailwind CDN and Google Fonts
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;templates/index.html&lt;/code&gt;. You currently have something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Welcome to David Tio's Blog&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{{ url_for('static', filename='css/style.css') }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Welcome to David Tio's Blog&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Building a blog platform with Docker.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the Google Fonts and Tailwind CDN in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;David Tio's Blog&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&amp;amp;family=DM+Sans:wght@400;500;700&amp;amp;display=swap"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.tailwindcss.com"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Welcome to David Tio's Blog&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Building a blog platform with Docker.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Tailwind is now loaded. We'll add the config file in Step 2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CDN trade-off:&lt;/strong&gt; It's bigger than a bundled build. But for a personal blog? Nobody's going to notice. And you save hours of build setup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Configure Tailwind
&lt;/h2&gt;

&lt;p&gt;Tailwind's default colors and fonts are a good start, but we want our own brand colors and the fonts we loaded. This can be configured using JavaScript. Let's create a directory for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; static/js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;static/js/tailwind.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;tailwind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#14B8A6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// teal accent&lt;/span&gt;
                    &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#0F766E&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// primary teal&lt;/span&gt;
                    &lt;span class="mi"&gt;700&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#0D5F57&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// darker teal&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="na"&gt;fontFamily&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;sans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DM Sans&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system-ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sans-serif&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="na"&gt;serif&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Instrument Serif&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Georgia&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;serif&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Load it in &lt;code&gt;index.html&lt;/code&gt; right after the Tailwind CDN script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&amp;amp;family=DM+Sans:wght@400;500;700&amp;amp;display=swap"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.tailwindcss.com"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"{{ url_for('static', filename='js/tailwind.config.js') }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can use &lt;code&gt;bg-brand-700&lt;/code&gt;, &lt;code&gt;text-brand-500&lt;/code&gt;, &lt;code&gt;font-serif&lt;/code&gt;, etc. in your classes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Build the Navigation Bar
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;templates/index.html&lt;/code&gt;. Add this right after the opening &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;nav&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-brand-700 border-b border-brand-600"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-6xl mx-auto px-6 py-4 flex items-center justify-between"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{{ url_for('index') }}"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-serif text-xl text-white"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            David Tio's Blog
        &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center space-x-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-teal-100 hover:text-white font-medium text-sm transition duration-200"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Home&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-teal-100 hover:text-white font-medium text-sm transition duration-200"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Series&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-teal-100 hover:text-white font-medium text-sm transition duration-200"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;About&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Darker teal navbar (&lt;code&gt;brand-700&lt;/code&gt;) with a subtle border line underneath&lt;/li&gt;
&lt;li&gt;Blog name on the left in Instrument Serif, linked back to your Flask &lt;code&gt;index&lt;/code&gt; route via &lt;code&gt;url_for&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Three navigation links on the right with white hover transitions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max-w-6xl mx-auto&lt;/code&gt; centers the nav content at a comfortable width&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mobile note:&lt;/strong&gt; This navbar isn't responsive yet. We'll fix that later. For now, it works on desktop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Placeholder links:&lt;/strong&gt; &lt;code&gt;Series&lt;/code&gt; and &lt;code&gt;About&lt;/code&gt; point to &lt;code&gt;#&lt;/code&gt; for now. They're dead links until we build those pages in a future episode. &lt;code&gt;Home&lt;/code&gt; links to your Flask &lt;code&gt;index&lt;/code&gt; route, which is already live.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Build the Hero and Post List
&lt;/h2&gt;

&lt;p&gt;Now replace the entire &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; section with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;body&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-slate-950 text-gray-100 font-sans min-h-screen flex flex-col"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Navbar from Step 3 --&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Hero --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-3xl mx-auto px-6 pt-20 pb-12 text-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"inline-block text-brand-500 text-xs font-semibold tracking-widest uppercase mb-5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Docker · Linux · Open Source&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-serif text-5xl text-white leading-tight mb-5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Practical guides for engineers.&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-400 text-lg leading-relaxed"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Building with containers, Linux, and open source tools — one post at a time.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Posts --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;main&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-3xl mx-auto px-6 pb-20 flex-1 w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"border-t border-slate-800 mb-10"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-xs font-semibold text-gray-500 uppercase tracking-widest mb-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Latest Posts&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

            &lt;span class="nt"&gt;&amp;lt;article&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"border-l-2 border-slate-800 hover:border-brand-500 pl-6 py-5 transition-all duration-300 group cursor-pointer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-600 text-xs mb-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;29 Mar 2026 &lt;span class="ni"&gt;&amp;amp;middot;&lt;/span&gt; Blog Platform&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;h3&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-100 font-semibold text-lg mb-2 group-hover:text-brand-500 transition-colors duration-200"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Building a Blog Platform #1: Flask Setup&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-500 text-sm leading-relaxed"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Get a basic Flask app running with separate CSS — no Docker yet, just Python and a stylesheet.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;

            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"border-t border-slate-800 ml-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Footer from Step 5 --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's happening here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;bg-slate-950&lt;/code&gt;&lt;/strong&gt; gives the whole page a dark slate background&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;min-h-screen flex flex-col&lt;/code&gt;&lt;/strong&gt; makes the body stretch to full viewport height and stacks nav, hero, posts, and footer vertically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;max-w-3xl mx-auto&lt;/code&gt;&lt;/strong&gt; centers the content at a readable width — just like a real blog&lt;/li&gt;
&lt;li&gt;The hero has a small teal label ("Docker · Linux · Open Source") and a large serif heading&lt;/li&gt;
&lt;li&gt;The post card has a left border that turns teal when you hover over it — try it&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;url_for&lt;/code&gt; in the navbar links back to your Flask &lt;code&gt;index&lt;/code&gt; route, so nothing is hardcoded&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 5: Add a Footer
&lt;/h2&gt;

&lt;p&gt;Before the closing &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt; tag, add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;footer&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-brand-700 border-t border-brand-600"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-6xl mx-auto px-6 py-6 flex items-center justify-between"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-teal-100 text-sm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="ni"&gt;&amp;amp;copy;&lt;/span&gt; 2026 David Tio.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex space-x-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-teal-100 hover:text-white text-sm transition-colors duration-200"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;LinkedIn&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-teal-100 hover:text-white text-sm transition-colors duration-200"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Twitter&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-teal-100 hover:text-white text-sm transition-colors duration-200"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;GitHub&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/footer&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Matches the nav — teal background, copyright left, social links right.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Clean Up Your CSS
&lt;/h2&gt;

&lt;p&gt;Now that Tailwind is doing all the work, your old &lt;code&gt;style.css&lt;/code&gt; is redundant. Remove the &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tag from the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of &lt;code&gt;index.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Delete this line --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{{ url_for('static', filename='css/style.css') }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then delete the file itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;rm &lt;/span&gt;static/css/style.css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 7: Test It
&lt;/h2&gt;

&lt;p&gt;If you closed the terminal since Episode 1, reactivate the venv first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run your Flask app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;a href="http://localhost:8000" rel="noopener noreferrer"&gt;http://localhost:8000&lt;/a&gt;. You should see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Teal navbar with your name in Instrument Serif&lt;/li&gt;
&lt;li&gt;Dark background with a centered hero&lt;/li&gt;
&lt;li&gt;Editorial post list with teal hover effects&lt;/li&gt;
&lt;li&gt;Matching teal footer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;It should look like an actual blog now.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What You've Built
&lt;/h2&gt;

&lt;p&gt;You now have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tailwind CSS via CDN (no build step)&lt;/li&gt;
&lt;li&gt;Google Fonts: DM Sans body, Instrument Serif for headings&lt;/li&gt;
&lt;li&gt;Custom brand colors (teal-500 accent, teal-700 nav/footer)&lt;/li&gt;
&lt;li&gt;Dark slate background (&lt;code&gt;slate-950&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Teal nav and footer branding&lt;/li&gt;
&lt;li&gt;Centered hero with serif heading&lt;/li&gt;
&lt;li&gt;Editorial post list with teal left-border hover&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fav0tx40wvs3lx8nxlthj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fav0tx40wvs3lx8nxlthj.png" alt="tiohub-blog running with Tailwind CSS dark theme, teal nav and footer, editorial post list" width="800" height="547"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Coming Up
&lt;/h2&gt;

&lt;p&gt;Right now, your posts are hardcoded HTML. You want to write Markdown files and have them render automatically. Next time: Markdown support. You'll write &lt;code&gt;.md&lt;/code&gt; files with frontmatter, and Flask will parse them into HTML.&lt;/p&gt;

&lt;p&gt;No more HTML templates for every blog post. Just write Markdown and go.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Found this helpful?&lt;/strong&gt; Share it with your network or drop a comment below.&lt;/p&gt;

</description>
      <category>python</category>
      <category>flask</category>
      <category>tailwindcss</category>
      <category>css</category>
    </item>
    <item>
      <title>Docker Networking Explained: Connect Your Containers (2026)</title>
      <dc:creator>David Tio</dc:creator>
      <pubDate>Fri, 10 Apr 2026 23:56:09 +0000</pubDate>
      <link>https://dev.to/davidtio/docker-networking-explained-connect-your-containers-2026-2j83</link>
      <guid>https://dev.to/davidtio/docker-networking-explained-connect-your-containers-2026-2j83</guid>
      <description>&lt;h2&gt;
  
  
  🌐 Docker Networking Explained: Connect Your Containers (2026)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Quick one-liner:&lt;/strong&gt; Connect your containers to each other and the outside world. Learn bridge networks, port mapping, DNS, and container-to-container communication.&lt;/p&gt;




&lt;h3&gt;
  
  
  🤔 Why This Matters
&lt;/h3&gt;

&lt;p&gt;In the &lt;a href="https://blog.dtio.app/2026/04/docker-volumes-explained-keep-your-data.html" rel="noopener noreferrer"&gt;last post&lt;/a&gt;, you got Redis + RedisInsight working. Your data persisted across a version upgrade, your config was pre-loaded via bind mount, and your Redis data survived the container being deleted and recreated.&lt;/p&gt;

&lt;p&gt;But remember what happened when we tried to connect RedisInsight to Redis?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# echo PING | nc dtredis86 6379
nc: bad address 'dtredis86'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hostname didn't resolve. We had to fall back to &lt;code&gt;docker inspect&lt;/code&gt; to find the Redis IP (&lt;code&gt;172.17.0.2&lt;/code&gt;) and connect that way. That works for a quick test, but it's not how you want to run anything real:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🏗️ If you restart the container, the IP changes&lt;/li&gt;
&lt;li&gt;📋 You'd need to manually update configs every time&lt;/li&gt;
&lt;li&gt;🔒 No network isolation — anything can reach anything&lt;/li&gt;
&lt;li&gt;🌐 No way to access containers from your browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Today, we're fixing all of that. We'll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Port mapping — exposing container ports to your browser&lt;/li&gt;
&lt;li&gt;Custom bridge networks — containers that find each other by name&lt;/li&gt;
&lt;li&gt;DNS resolution — no more &lt;code&gt;docker inspect&lt;/code&gt; to find IPs&lt;/li&gt;
&lt;li&gt;Network isolation — keeping your services segmented&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end, you'll have RedisInsight talking to Redis by name, accessible from your browser, on a clean isolated network. The right way.&lt;/p&gt;




&lt;h3&gt;
  
  
  ✅ Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker installed&lt;/strong&gt; (rootless mode recommended)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ep 1-5 completed&lt;/strong&gt; — you know volumes, environment variables, and bind mounts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5 minutes&lt;/strong&gt; to wire up your first multi-container setup&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🏗️ The Default Bridge Network
&lt;/h3&gt;

&lt;p&gt;When you run a container without specifying a network, Docker puts it on the default &lt;code&gt;bridge&lt;/code&gt; network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker network &lt;span class="nb"&gt;ls
&lt;/span&gt;NETWORK ID     NAME      DRIVER    SCOPE
a1b2c3d4e5f6   bridge    bridge    &lt;span class="nb"&gt;local
&lt;/span&gt;d4e5f6a1b2c3   host      host      &lt;span class="nb"&gt;local
&lt;/span&gt;e5f6a1b2c3d4   none      null      &lt;span class="nb"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Containers on the default bridge can reach each other &lt;strong&gt;by IP address&lt;/strong&gt;, but &lt;strong&gt;not by name&lt;/strong&gt;. That's exactly what you saw with RedisInsight and Redis in the last post. Let's reproduce it with the same setup — the Redis + RedisInsight you ended Ep 5 with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; dtredis86 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; redisdata:/data &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; ./redis.conf:/etc/redis/redis.conf &lt;span class="se"&gt;\&lt;/span&gt;
    redis:8.6 redis-server /etc/redis/redis.conf

&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; redisinsight &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; ./data/ri:/data &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;RI_PRE_SETUP_DATABASES_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/data/config.json &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;RI_ACCEPT_TERMS_AND_CONDITIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    redis/redisinsight
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;-p&lt;/code&gt; flag — so you can't reach it from your browser, just like in Ep 5.&lt;/p&gt;

&lt;p&gt;Try resolving &lt;code&gt;dtredis86&lt;/code&gt; from the RedisInsight container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;redisinsight sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"echo PING | nc dtredis86 6379"&lt;/span&gt;
nc: bad address &lt;span class="s1"&gt;'dtredis86'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hostname resolution doesn't work on the default bridge. You'd need to inspect the container to find its IP, then connect that way — exactly what you saw in Ep 5 when we had to fall back to &lt;code&gt;172.17.0.2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is fragile. If you restart the containers, the IPs change. Let's fix this properly.&lt;/p&gt;




&lt;h3&gt;
  
  
  🌐 Port Mapping: Expose Containers to the Host
&lt;/h3&gt;

&lt;p&gt;In Ep 5, you ran RedisInsight without port mapping and couldn't reach it at &lt;code&gt;http://localhost:5540&lt;/code&gt;. The container was alive inside, but your host had no way in.&lt;/p&gt;

&lt;p&gt;Let's add port mapping to the same RedisInsight setup from Ep 5:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker stop redisinsight

&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; redisinsight &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:5540 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; ./data/ri:/data &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;RI_PRE_SETUP_DATABASES_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/data/config.json &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;RI_ACCEPT_TERMS_AND_CONDITIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    redis/redisinsight
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;./data/ri&lt;/code&gt; directory should already have the correct &lt;code&gt;chown&lt;/code&gt; from Ep 5. If not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 1001000:1001000 ./data/ri
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-p 8080:5540&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Map host port &lt;code&gt;8080&lt;/code&gt; → container port &lt;code&gt;5540&lt;/code&gt; (RedisInsight)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now open &lt;code&gt;http://localhost:8080&lt;/code&gt; — RedisInsight loads. 🎉&lt;/p&gt;

&lt;p&gt;The format is always &lt;strong&gt;host_port:container_port&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; dtredis2 &lt;span class="nt"&gt;-p&lt;/span&gt; 6380:6379 redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now here's a practical exercise: open RedisInsight at &lt;code&gt;http://localhost:8080&lt;/code&gt; and try adding &lt;code&gt;dtredis2&lt;/code&gt; as a new connection:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you try&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redis://dtredis2:6379&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ DNS fails&lt;/td&gt;
&lt;td&gt;Hostnames don't resolve on default bridge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redis://127.0.0.1:6379&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ Connection refused&lt;/td&gt;
&lt;td&gt;That's the RedisInsight container's own loopback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redis://127.0.0.1:6380&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ Connection refused&lt;/td&gt;
&lt;td&gt;Same reason — localhost inside a container&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The only way to reach &lt;code&gt;dtredis2&lt;/code&gt; from RedisInsight is via your host's IP address. Get it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt;
&amp;lt;your_host_ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in RedisInsight: &lt;code&gt;redis://&amp;lt;your_host_ip&amp;gt;:6380&lt;/code&gt; — it connects, but it's ugly. If your host IP changes, you're updating configs everywhere.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F27ga4l40hz146ti9v6cs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F27ga4l40hz146ti9v6cs.png" alt="RedisInsight connected to Redis via host IP address" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It works — but we're using a raw IP address that could change at any time.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Verify the port mapping:&lt;/strong&gt;&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker port dtredis2
6379/tcp -&amp;gt; 0.0.0.0:6380
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;p&gt;There has to be a better way. 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rootless note:&lt;/strong&gt; In rootless mode, you can't bind to ports below 1024. So &lt;code&gt;-p 80:5540&lt;/code&gt; won't work. Use &lt;code&gt;-p 8080:5540&lt;/code&gt; or higher. If you need port 80, put a rootful reverse proxy (nginx, caddy, traefik) in front.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Clean up before moving on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker stop dtredis86 redisinsight
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🌉 Custom Bridge Networks: The Right Way
&lt;/h3&gt;

&lt;p&gt;Create a custom bridge network. Containers on the same custom network can reach each other &lt;strong&gt;by name&lt;/strong&gt; — Docker provides built-in DNS resolution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker network create dtapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run Redis and RedisInsight on this network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; dtredis &lt;span class="nt"&gt;--network&lt;/span&gt; dtapp redis
&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; ri &lt;span class="nt"&gt;--network&lt;/span&gt; dtapp &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:5540 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; ./data/ri:/data &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;RI_PRE_SETUP_DATABASES_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/data/config.json &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;RI_ACCEPT_TERMS_AND_CONDITIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    redis/redisinsight
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, verify the connection from the command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;ri sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"echo PING | nc dtredis 6379"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It resolves. ✅ No IP inspection, no manual config. The name works automatically.&lt;/p&gt;

&lt;p&gt;Now open &lt;code&gt;http://localhost:8080&lt;/code&gt; — RedisInsight loads, and you can add &lt;code&gt;dtredis:6379&lt;/code&gt; as a new connection. No IP addresses needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqro4e35yilwaaxwo3ser.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqro4e35yilwaaxwo3ser.png" alt="RedisInsight connected to Redis via hostname on custom bridge network" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connect an existing container to a network:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; dtredis2 redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This container is on the default bridge. Connect it to &lt;code&gt;dtapp&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker network connect dtapp dtredis2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;dtredis2&lt;/code&gt; is on both networks. Verify &lt;code&gt;ri&lt;/code&gt; can reach it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;ri sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"echo PING | nc dtredis2 6379"&lt;/span&gt;
PING
+PONG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redis responds by hostname. ✅&lt;/p&gt;




&lt;h3&gt;
  
  
  🧪 Exercise: CloudBeaver + PostgreSQL on a Custom Network
&lt;/h3&gt;

&lt;p&gt;Let's wire up a real multi-container stack — PostgreSQL for the database, CloudBeaver as the web UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 1: Create the Network and Run PostgreSQL
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker network create dtstack
&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; dtpg &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--network&lt;/span&gt; dtstack &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;testdb &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; pgdata:/var/lib/postgresql/data &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--tmpfs&lt;/span&gt; /var/run/postgresql &lt;span class="se"&gt;\&lt;/span&gt;
    postgres:17
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rootless note:&lt;/strong&gt; PostgreSQL tries to &lt;code&gt;chmod&lt;/code&gt; &lt;code&gt;/var/run/postgresql&lt;/code&gt; for its PID file at startup. Rootless containers can't do that. The &lt;code&gt;--tmpfs&lt;/code&gt; flag gives it a writable temp directory on that path without root.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Part 2: Run CloudBeaver on the Same Network
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; cloudbeaver &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--network&lt;/span&gt; dtstack &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-p&lt;/span&gt; 8978:8978 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; cbdata:/opt/cloudbeaver/workspace &lt;span class="se"&gt;\&lt;/span&gt;
    dbeaver/cloudbeaver:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait ~30 seconds for it to start, then open &lt;code&gt;http://localhost:8978&lt;/code&gt; in your browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It loads.&lt;/strong&gt; 🎉 Now add a connection in the CloudBeaver UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server:&lt;/strong&gt; &lt;code&gt;dtpg&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port:&lt;/strong&gt; &lt;code&gt;5432&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Username:&lt;/strong&gt; &lt;code&gt;postgres&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password:&lt;/strong&gt; &lt;code&gt;docker&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; &lt;code&gt;testdb&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqepk9hhf0j70buhskl8j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqepk9hhf0j70buhskl8j.png" alt="CloudBeaver successfully connected to PostgreSQL via hostname dtpg:5432" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 4: Check the Network
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker network inspect dtstack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see both containers listed under &lt;code&gt;Containers&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"Containers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"abc123..."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dtpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"IPv4Address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"172.18.0.2/16"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"def456..."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudbeaver"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"IPv4Address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"172.18.0.3/16"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two containers. One network. DNS works between them. Port mapping gives you browser access. This is how multi-container apps should run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 5: Clean Up
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker stop dtpg cloudbeaver
&lt;span class="nv"&gt;$ &lt;/span&gt;docker volume &lt;span class="nb"&gt;rm &lt;/span&gt;pgdata cbdata
&lt;span class="nv"&gt;$ &lt;/span&gt;docker network &lt;span class="nb"&gt;rm &lt;/span&gt;dtstack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Problem With What We Just Did
&lt;/h3&gt;

&lt;p&gt;Look at what it took to wire up two containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker network create dtstack
&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; dtpg &lt;span class="nt"&gt;--network&lt;/span&gt; dtstack &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;testdb &lt;span class="nt"&gt;-v&lt;/span&gt; pgdata:/var/lib/postgresql/data &lt;span class="nt"&gt;--tmpfs&lt;/span&gt; /var/run/postgresql postgres:17
&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; cloudbeaver &lt;span class="nt"&gt;--network&lt;/span&gt; dtstack &lt;span class="nt"&gt;-p&lt;/span&gt; 8978:8978 &lt;span class="nt"&gt;-v&lt;/span&gt; cbdata:/opt/cloudbeaver/workspace dbeaver/cloudbeaver:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three commands. That's not the problem.&lt;/p&gt;

&lt;p&gt;The problem is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The second command is a 150-character wall of flags, environment variables, and volume mounts&lt;/li&gt;
&lt;li&gt;One typo in &lt;code&gt;--tmpfs&lt;/code&gt; and PostgreSQL silently starts but fails to accept connections&lt;/li&gt;
&lt;li&gt;Forget &lt;code&gt;--network dtstack&lt;/code&gt; and the containers won't find each other&lt;/li&gt;
&lt;li&gt;Tear it down and rebuild? Type it all again&lt;/li&gt;
&lt;li&gt;What about when you have 5 containers? 10? Shared volumes? Secret files? Restart policies?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a better way.&lt;/p&gt;




&lt;h3&gt;
  
  
  📋 Network Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Network Type&lt;/th&gt;
&lt;th&gt;DNS Resolution&lt;/th&gt;
&lt;th&gt;Isolation&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;default bridge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ No (IP only)&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Quick testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;custom bridge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes (by name)&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Multi-container apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;host&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Performance (container uses host network)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;none&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;Air-gapped containers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;👉 &lt;strong&gt;Coming up:&lt;/strong&gt; We'll replace every single command above with a clean YAML file. Docker Compose — define your entire multi-container stack and launch it with one command.&lt;/p&gt;

&lt;p&gt;📚 &lt;strong&gt;Want More?&lt;/strong&gt; This guide covers the basics from &lt;strong&gt;Chapter 5: Docker Networking&lt;/strong&gt; in my book, &lt;em&gt;"Levelling Up with Docker"&lt;/em&gt; — 14 chapters of practical, hands-on Docker guides.&lt;/p&gt;

&lt;p&gt;📚 &lt;strong&gt;Grab the book:&lt;/strong&gt; &lt;a href="https://www.amazon.com/dp/B0GGZ76PHW" rel="noopener noreferrer"&gt;"Levelling Up with Docker" on Amazon&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Found this helpful?&lt;/strong&gt; 🙌&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://linkedin.com/in/davidtio" rel="noopener noreferrer"&gt;Share with your network&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twitter:&lt;/strong&gt; &lt;a href="https://x.com/intent/tweet?text=Just%20learned%20Docker%20networking!&amp;amp;url=https://blog.dtio.app/2026/04/docker-networking-explained.html" rel="noopener noreferrer"&gt;Tweet about it&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Questions?&lt;/strong&gt; Drop a comment below or reach out on LinkedIn&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>networking</category>
      <category>linux</category>
      <category>devops</category>
    </item>
    <item>
      <title>Podman on SLES 16: Installation, Storage, and First Rootless Container (2026 Guide)</title>
      <dc:creator>David Tio</dc:creator>
      <pubDate>Mon, 06 Apr 2026 00:56:42 +0000</pubDate>
      <link>https://dev.to/davidtio/podman-on-sles-16-installation-storage-and-first-rootless-container-2026-guide-2mki</link>
      <guid>https://dev.to/davidtio/podman-on-sles-16-installation-storage-and-first-rootless-container-2026-guide-2mki</guid>
      <description>&lt;h1&gt;
  
  
  Podman on SLES 16: Installation, Storage, and First Rootless Container (2026 Guide)
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Quick one-liner:&lt;/strong&gt; Install Podman on SLES 16 from the installation DVD, set up shared multi-user storage on a dedicated disk, and run your first rootless container — no Docker required.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(This guide targets SLES 16. The concepts apply to SLES 15 as well, but I've only verified the steps on SLES 16.)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Docker isn't the only container runtime. On SUSE Linux Enterprise Server, SLES ships Podman as the native container tool through its official Containers module — and it has genuine advantages over Docker.&lt;/p&gt;

&lt;p&gt;The biggest one: rootless by default.&lt;/p&gt;

&lt;p&gt;With Docker, the daemon runs as root. That means a container escape could give an attacker root access to your host. Podman runs containers under your regular user account — no root daemon, no single point of failure.&lt;/p&gt;

&lt;p&gt;There's also no daemon at all. Podman is daemonless — each container runs as a direct child process. Simpler, more secure, easier to debug.&lt;/p&gt;

&lt;p&gt;If you're in an enterprise environment running SLES, Podman is the natural fit. It's supported by SUSE, available on the installation DVD, and doesn't require third-party repositories. This guide gets you from a fresh SLES install to running your first container.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SLES 16&lt;/strong&gt; (minimal or server installation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DVD repository enabled&lt;/strong&gt; (see &lt;a href="https://blog.dtio.app/2026/03/sles-16-add-dvd-as-local-zypper.html" rel="noopener noreferrer"&gt;Add a DVD as a Local Zypper Repository&lt;/a&gt;) — or an active SCC subscription&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two virtual disks&lt;/strong&gt; (recommended): 40 GB for the OS, 20 GB for container storage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;15-20 minutes&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why Two Disks?
&lt;/h2&gt;

&lt;p&gt;Before we start — if you're setting this up on a VM, add a second virtual disk.&lt;/p&gt;

&lt;p&gt;Container images accumulate fast. A few images later, your root partition fills up and everything breaks. Keeping container storage on a separate disk means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Container data is isolated from the OS&lt;/li&gt;
&lt;li&gt;You can resize or wipe container storage without touching the OS&lt;/li&gt;
&lt;li&gt;Backups are simpler — back up the container disk separately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a production best practice worth building into your habits from day one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Mount the Second Disk
&lt;/h2&gt;

&lt;p&gt;If you added a second disk, set it up before installing Podman.&lt;/p&gt;

&lt;p&gt;Find the disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;lsblk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On this VM the layout looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
sr0     11:0    1 1024M  0 rom  
vda    254:0    0   40G  0 disk 
├─vda1 254:1    0    8M  0 part 
├─vda2 254:2    0   38G  0 part /var
│                               /usr/local
│                               /opt
│                               /home
│                               /srv
│                               /root
│                               /boot/grub2/i386-pc
│                               /boot/grub2/x86_64-efi
│                               /.snapshots
│                               /
└─vda3 254:3    0    2G  0 part [SWAP]
vdb    254:16   0   20G  0 disk 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;vda&lt;/code&gt; (40 GB) is the OS disk. &lt;code&gt;vdb&lt;/code&gt; (20 GB) is the dedicated disk for container storage.&lt;/p&gt;

&lt;p&gt;Format &lt;code&gt;vdb&lt;/code&gt; with XFS (SLES default):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mkfs.xfs /dev/vdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the mount point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/containers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add to &lt;code&gt;/etc/fstab&lt;/code&gt; for persistence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'/dev/vdb /var/lib/containers xfs defaults 0 0'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mount everything from fstab and verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-Th&lt;/span&gt; /var/lib/containers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2: Install Podman
&lt;/h2&gt;

&lt;p&gt;Podman ships on the SLES 16 installation DVD. Enable the DVD repository and install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;zypper mr &lt;span class="nt"&gt;-e&lt;/span&gt; SLES
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;zypper &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; podman fuse-overlayfs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;podman &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;podman version 5.4.2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: Set Up Shared Multi-User Storage
&lt;/h2&gt;

&lt;p&gt;This is where most guides stop — they just say "install and go." But in a real environment, multiple people will run rootless containers. Each user's container images and layers are stored separately (rootless Podman stores everything under each user's home directory by default). If you don't plan for this, each user's &lt;code&gt;~/.local&lt;/code&gt; fills up their own home partition independently and unpredictably.&lt;/p&gt;

&lt;p&gt;Here is how I like to set up Podman in an enterprise environment. Each user gets their own isolated space on a shared disk, and access is controlled through a group:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;As root (or sudo):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create the shared storage directory and the &lt;code&gt;podman&lt;/code&gt; group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/containers/storage
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;groupadd podman
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;root:podman /var/lib/containers/storage
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;2775 /var/lib/containers/storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;2775&lt;/code&gt; permission is important — the setgid bit means any subdirectory created inside &lt;code&gt;/var/lib/containers/storage&lt;/code&gt; automatically inherits the &lt;code&gt;podman&lt;/code&gt; group. That keeps things consistent as you add users.&lt;/p&gt;

&lt;p&gt;Add users who should have container access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; podman sysadmin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why Subordinate UIDs Matter
&lt;/h3&gt;

&lt;p&gt;In rootless mode, container processes run in their own &lt;strong&gt;user namespace&lt;/strong&gt;. Their UIDs get remapped to different UIDs on your host. This is a Linux kernel feature (&lt;code&gt;user_namespaces(7)&lt;/code&gt;) and works identically for both rootless Docker and rootless Podman. The mapping looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Inside container&lt;/th&gt;
&lt;th&gt;On your host&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UID 0 (root)&lt;/td&gt;
&lt;td&gt;Your user UID (e.g. &lt;code&gt;1000&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UID 1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;subuid_start&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UID N&lt;/td&gt;
&lt;td&gt;&lt;code&gt;subuid_start + N - 1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;subuid_start&lt;/code&gt; is defined in &lt;code&gt;/etc/subuid&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;grep &lt;/span&gt;sysadmin /etc/subuid
sysadmin:100000:65536
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SLES assigns &lt;code&gt;100000&lt;/code&gt; by default. This means a container process running as UID 1000 lands on your host as UID &lt;code&gt;100999&lt;/code&gt; (&lt;code&gt;100000 + 1000 - 1&lt;/code&gt;). If your bind-mounted directory is owned by you (&lt;code&gt;1000&lt;/code&gt;), that container process cannot write to it.&lt;/p&gt;

&lt;p&gt;I prefer explicit ranges so the math is clean and there's no guessing. First, remove any existing subordinate UID/GID entries, then set the new range:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"/^sysadmin:/d"&lt;/span&gt; /etc/subuid
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"/^sysadmin:/d"&lt;/span&gt; /etc/subgid
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;--add-subuids&lt;/span&gt; 100001-165536 sysadmin
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;--add-subgids&lt;/span&gt; 100001-165536 sysadmin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives 65,536 subordinate UIDs and GIDs. Now the mapping is clean and predictable:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Inside container&lt;/th&gt;
&lt;th&gt;On your host&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1000 (sysadmin)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;100001&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;101000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/subuid
sysadmin:100001:65536

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/subgid
sysadmin:100001:65536
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;As each user:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Log out and back in so the group change takes effect. Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;groups&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;podman&lt;/code&gt; in the list.&lt;/p&gt;

&lt;p&gt;Create your personal storage directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/containers/storage/sysadmin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create Podman's per-user storage config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/containers
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.config/containers/storage.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[storage]
driver = "overlay"
graphroot = "/var/lib/containers/storage/sysadmin"

[storage.options.overlay]
mount_program = "/usr/bin/fuse-overlayfs"
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file tells rootless Podman to store your images, containers, and layers on the shared disk instead of filling up your home directory.&lt;/p&gt;

&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;podman info | &lt;span class="nb"&gt;grep &lt;/span&gt;graphRoot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;/var/lib/containers/storage/sysadmin&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Run Your First Container
&lt;/h2&gt;

&lt;p&gt;Now run a container as your regular user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;podman run quay.io/podman/hello:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Embracing and extending the Podman community...

================================================================
                       Podman Podman Podman
================================================================

... (Podman hello output) ...

Have a great day!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Podman ran this rootless — no root daemon, no extra configuration.&lt;/p&gt;

&lt;p&gt;Check the container ran successfully:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;podman ps &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;CONTAINER ID  IMAGE                        COMMAND               CREATED        STATUS                     PORTS       NAMES
2671631a4e8a  quay.io/podman/hello:latest  /usr/local/bin/po...  26 seconds ago  Exited (0) 26 seconds ago              stoic_sammet
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a good moment to clarify the difference between &lt;code&gt;podman ps&lt;/code&gt; and &lt;code&gt;podman ps -a&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it shows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;podman ps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only &lt;strong&gt;running&lt;/strong&gt; containers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;podman ps -a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;All&lt;/strong&gt; containers — running, stopped, or exited&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you run &lt;code&gt;podman ps&lt;/code&gt; right now, it returns nothing — the hello container printed its message and exited immediately. It worked perfectly, it just isn't running anymore. &lt;code&gt;podman ps -a&lt;/code&gt; shows the full history, including containers that completed and stopped.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Verify Rootless Operation
&lt;/h2&gt;

&lt;p&gt;This is the key difference from Docker. In rootless mode, there's no persistent background daemon waiting for commands. Check the status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;systemctl status podman
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see the &lt;code&gt;podman.service&lt;/code&gt; exists but is inactive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="err"&gt;○&lt;/span&gt; &lt;span class="err"&gt;podman.service&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="err"&gt;Podman&lt;/span&gt; &lt;span class="err"&gt;API&lt;/span&gt; &lt;span class="err"&gt;Service&lt;/span&gt;
     &lt;span class="err"&gt;Loaded:&lt;/span&gt; &lt;span class="err"&gt;loaded&lt;/span&gt; &lt;span class="err"&gt;(/usr/lib/systemd/system/podman.service&lt;/span&gt;&lt;span class="c"&gt;; disabled; preset: disabled)&lt;/span&gt;
     &lt;span class="err"&gt;Active:&lt;/span&gt; &lt;span class="err"&gt;inactive&lt;/span&gt; &lt;span class="err"&gt;(dead)&lt;/span&gt;
&lt;span class="err"&gt;TriggeredBy:&lt;/span&gt; &lt;span class="err"&gt;○&lt;/span&gt; &lt;span class="err"&gt;podman.socket&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is normal — it's a socket-activated service. It starts only when needed (for example, when Podman Desktop or other tools connect to it) and shuts down when idle. Unlike Docker, there's no &lt;code&gt;dockerd&lt;/code&gt; process sitting at PID 1 consuming resources all day.&lt;/p&gt;

&lt;p&gt;Verify storage is on the shared disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-Th&lt;/span&gt; /var/lib/containers/storage/sysadmin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see your second disk, not the root partition.&lt;/p&gt;




&lt;h2&gt;
  
  
  Podman vs Docker: Key Differences
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Podman&lt;/th&gt;
&lt;th&gt;Docker&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Daemon&lt;/td&gt;
&lt;td&gt;None (daemonless)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dockerd&lt;/code&gt; runs as root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default user&lt;/td&gt;
&lt;td&gt;Rootless&lt;/td&gt;
&lt;td&gt;Root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI compatibility&lt;/td&gt;
&lt;td&gt;Drop-in replacement (&lt;code&gt;alias docker=podman&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;systemd integration&lt;/td&gt;
&lt;td&gt;Native (Quadlet)&lt;/td&gt;
&lt;td&gt;Requires extra config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLES support&lt;/td&gt;
&lt;td&gt;On the installation DVD&lt;/td&gt;
&lt;td&gt;Third-party repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;No root daemon, SELinux-ready&lt;/td&gt;
&lt;td&gt;Root daemon exposure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The CLI is fully compatible — most &lt;code&gt;docker&lt;/code&gt; commands work with &lt;code&gt;podman&lt;/code&gt;. You can set the alias:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;docker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;podman
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Try It: Parse JSON Without Installing Anything
&lt;/h2&gt;

&lt;p&gt;Instead of the usual &lt;code&gt;hello-world&lt;/code&gt;, let's verify rootless Podman with something useful.&lt;/p&gt;

&lt;p&gt;Create a sample JSON file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/sample.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
{"name":"David","company":"Transcend Solutions","role":"DevOps Engineer","skills":["Docker","Kubernetes","Linux"],"location":"Singapore","experience_years":15}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normally you'd need to install &lt;code&gt;jq&lt;/code&gt; to parse and pretty-print this JSON. With Podman, the tool comes with the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/sample.json | podman run ghcr.io/jqlang/jq &lt;span class="s1"&gt;'.'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image pulls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Trying to pull ghcr.io/jqlang/jq:latest...
Getting image source signatures
Copying blob e27c450974af done   | 
Copying blob ee0085cc4ebc done   | 
Copying config 3bada1936a done   | 
Writing manifest to image destination
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the output is completely blank. No error, no JSON — nothing.&lt;/p&gt;

&lt;p&gt;That's because &lt;code&gt;podman run&lt;/code&gt; doesn't pass standard input into the container by default. The &lt;code&gt;jq&lt;/code&gt; process started, received nothing, and exited silently. To pipe data in, you need the &lt;code&gt;-i&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/sample.json | podman run &lt;span class="nt"&gt;-i&lt;/span&gt; ghcr.io/jqlang/jq &lt;span class="s1"&gt;'.'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the output — beautifully formatted JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"David"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"company"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Transcend Solutions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DevOps Engineer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"skills"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Docker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Kubernetes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Linux"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Singapore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"experience_years"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No installation. No &lt;code&gt;sudo zypper install jq&lt;/code&gt;. No repository configuration. The &lt;code&gt;jq&lt;/code&gt; binary lives inside the container, and you used it without touching your host system.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's happening here:
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-i&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep stdin open so &lt;code&gt;jq&lt;/code&gt; can read the piped JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;'.'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The jq filter — &lt;code&gt;.&lt;/code&gt; means "print everything, formatted"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;You've got Podman installed with proper shared storage and running rootless containers on SLES 16.&lt;/p&gt;

&lt;p&gt;Coming up: running your first real workload — pulling images, managing container lifecycles, and understanding the differences between &lt;code&gt;podman run&lt;/code&gt; flags you'll use every day.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Author:&lt;/strong&gt; David Tio&lt;br&gt;
&lt;strong&gt;Tags:&lt;/strong&gt; Podman, SLES 16, SUSE, Containers, Rootless, Linux, Enterprise, DevOps, Tutorial&lt;br&gt;
&lt;strong&gt;Series:&lt;/strong&gt; Levelling Podman&lt;br&gt;
&lt;strong&gt;Word Count:&lt;/strong&gt; ~1,500&lt;/p&gt;

</description>
      <category>podman</category>
      <category>sles</category>
      <category>sles16</category>
      <category>linux</category>
    </item>
    <item>
      <title>Persistent VMs in Podman: Install Alpine to a qcow2 Disk Image</title>
      <dc:creator>David Tio</dc:creator>
      <pubDate>Wed, 01 Apr 2026 03:14:22 +0000</pubDate>
      <link>https://dev.to/davidtio/persistent-vms-in-podman-install-alpine-to-a-qcow2-disk-image-go6</link>
      <guid>https://dev.to/davidtio/persistent-vms-in-podman-install-alpine-to-a-qcow2-disk-image-go6</guid>
      <description>&lt;h1&gt;
  
  
  Persistent VMs in Podman: Install Alpine to a Disk Image
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Quick one-liner:&lt;/strong&gt; Create a &lt;code&gt;qcow2&lt;/code&gt; disk image with &lt;code&gt;qemu-img&lt;/code&gt;, install Alpine Linux into it, and boot from disk — so your VM survives container restarts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Post #2 proved that KVM hardware acceleration is fast. But there's a catch: every time the container stops, the VM state vanishes. The Alpine ISO is read-only — any changes you make inside the VM exist only in RAM. Stop the container and they're gone.&lt;/p&gt;

&lt;p&gt;That's fine for a boot-speed demo, but it's not a real VM. A real VM has a disk that persists between runs. The disk lives on the host filesystem, the container is just the runtime, and the two are completely independent. Stop and restart the container as many times as you want — the disk doesn't care.&lt;/p&gt;

&lt;p&gt;This post adds that layer. You'll create a &lt;code&gt;qcow2&lt;/code&gt; disk image, boot from ISO + disk to run the Alpine installer, then boot from disk alone to confirm it survived.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;qemu:base&lt;/code&gt; image from Post #1&lt;/li&gt;
&lt;li&gt;Alpine ISO from Post #2 at &lt;code&gt;~/Downloads/alpine-standard-3.23.3-x86_64.iso&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/vm&lt;/code&gt; directory (you'll create it below)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: Create the VM Directory and Disk Image
&lt;/h2&gt;

&lt;p&gt;First, create a dedicated directory for your VM disk images:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/vm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create the disk image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;podman run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; ~/vm:/vm:z &lt;span class="se"&gt;\&lt;/span&gt;
    qemu:base &lt;span class="se"&gt;\&lt;/span&gt;
    qemu-img create &lt;span class="nt"&gt;-f&lt;/span&gt; qcow2 /vm/alpine.qcow2 8G
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Formatting '/vm/alpine.qcow2', fmt=qcow2 cluster_size=65536 extended_l2=off compression_type=zlib size=8589934592 lazy_refcounts=off refcount_bits=16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What's qcow2?&lt;/strong&gt; It stands for QEMU Copy-On-Write version 2. The key property is thin provisioning: the file on your host starts tiny (a few hundred KB) and only grows as the VM actually writes data. Specifying &lt;code&gt;8G&lt;/code&gt; sets the maximum size the VM sees, not the space it consumes on disk immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Boot from ISO + Disk to Install
&lt;/h2&gt;

&lt;p&gt;Now boot with both the ISO and the disk attached. The &lt;code&gt;-boot d&lt;/code&gt; flag tells QEMU to boot from the CD-ROM first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;podman run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--device&lt;/span&gt; /dev/kvm &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; ~/vm:/vm:z &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; ~/Downloads:/iso:z &lt;span class="se"&gt;\&lt;/span&gt;
    qemu:base &lt;span class="se"&gt;\&lt;/span&gt;
    qemu-system-x86_64 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-enable-kvm&lt;/span&gt; &lt;span class="nt"&gt;-cpu&lt;/span&gt; host &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-nographic&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-m&lt;/span&gt; 512 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-cdrom&lt;/span&gt; /iso/alpine-standard-3.23.3-x86_64.iso &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-drive&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/vm/alpine.qcow2,format&lt;span class="o"&gt;=&lt;/span&gt;qcow2 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-boot&lt;/span&gt; d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alpine will boot from the ISO into a live environment. Log in as &lt;code&gt;root&lt;/code&gt; — no password required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Install Alpine
&lt;/h2&gt;

&lt;p&gt;Once you're at the shell, run the Alpine installer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# setup-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Work through the prompts. Most defaults are fine. The ones that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hostname:&lt;/strong&gt; anything, e.g. &lt;code&gt;alpine&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network:&lt;/strong&gt; &lt;code&gt;eth0&lt;/code&gt;, DHCP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proxy:&lt;/strong&gt; &lt;code&gt;none&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root password:&lt;/strong&gt; set something you'll remember&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timezone:&lt;/strong&gt; your choice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mirror:&lt;/strong&gt; pick the fastest (or just press Enter for the default)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH server:&lt;/strong&gt; &lt;code&gt;openssh&lt;/code&gt; or &lt;code&gt;none&lt;/code&gt; — your call&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setup a user:&lt;/strong&gt; enter a username — don't skip this; logging in as root is bad practice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full name:&lt;/strong&gt; optional, press Enter to skip&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User password:&lt;/strong&gt; set one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH key or URL:&lt;/strong&gt; &lt;code&gt;none&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disk:&lt;/strong&gt; &lt;code&gt;sda&lt;/code&gt; — this is your qcow2 image&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to use it:&lt;/strong&gt; &lt;code&gt;sys&lt;/code&gt; — full system install to disk&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Erase above disk and continue:&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the installer finishes, power off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# poweroff&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container exits. The &lt;code&gt;alpine.qcow2&lt;/code&gt; file on your host now contains a complete Alpine installation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Boot from Disk Only
&lt;/h2&gt;

&lt;p&gt;Drop the ISO flags entirely. The disk knows how to boot now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;podman run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--device&lt;/span&gt; /dev/kvm &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; ~/vm:/vm:z &lt;span class="se"&gt;\&lt;/span&gt;
    qemu:base &lt;span class="se"&gt;\&lt;/span&gt;
    qemu-system-x86_64 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-enable-kvm&lt;/span&gt; &lt;span class="nt"&gt;-cpu&lt;/span&gt; host &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-nographic&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-m&lt;/span&gt; 512 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-drive&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/vm/alpine.qcow2,format&lt;span class="o"&gt;=&lt;/span&gt;qcow2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alpine boots from the installed disk. Log in with the username you created during setup. Now write a file to prove the disk persists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"hello from install"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/persistence-test.txt
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/persistence-test.txt
hello from &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;su -
&lt;span class="c"&gt;# poweroff&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container exits. Run the exact same boot command again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;podman run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--device&lt;/span&gt; /dev/kvm &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; ~/vm:/vm:z &lt;span class="se"&gt;\&lt;/span&gt;
    qemu:base &lt;span class="se"&gt;\&lt;/span&gt;
    qemu-system-x86_64 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-enable-kvm&lt;/span&gt; &lt;span class="nt"&gt;-cpu&lt;/span&gt; host &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-nographic&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-m&lt;/span&gt; 512 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-drive&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/vm/alpine.qcow2,format&lt;span class="o"&gt;=&lt;/span&gt;qcow2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log in and check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/persistence-test.txt
hello from &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file survived. The container was destroyed and recreated, but the disk image on your host never changed. That's persistence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Why This Persists Across Container Restarts
&lt;/h2&gt;

&lt;p&gt;The container is ephemeral — &lt;code&gt;--rm&lt;/code&gt; means Podman deletes it the moment QEMU exits. But the disk image at &lt;code&gt;~/vm/alpine.qcow2&lt;/code&gt; lives on your host filesystem, completely outside the container lifecycle.&lt;/p&gt;

&lt;p&gt;The bind mount (&lt;code&gt;-v ~/vm:/vm:z&lt;/code&gt;) is just a path into the host. Writing to &lt;code&gt;/vm/alpine.qcow2&lt;/code&gt; inside the container is writing to &lt;code&gt;~/vm/alpine.qcow2&lt;/code&gt; on the host. When the container is gone, the file remains.&lt;/p&gt;

&lt;h3&gt;
  
  
  New Flags at a Glance
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-drive file=/vm/alpine.qcow2,format=qcow2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;QEMU&lt;/td&gt;
&lt;td&gt;Attaches the disk image as a block device (&lt;code&gt;sda&lt;/code&gt; inside the VM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-boot d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;QEMU&lt;/td&gt;
&lt;td&gt;Sets boot order to CD-ROM first; needed during install so Alpine boots from ISO, not the blank disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;format=qcow2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;QEMU &lt;code&gt;-drive&lt;/code&gt; option&lt;/td&gt;
&lt;td&gt;Tells QEMU the image format explicitly; avoids format auto-detection warnings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-v ~/vm:/vm:z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Podman&lt;/td&gt;
&lt;td&gt;Bind-mounts the host &lt;code&gt;~/vm&lt;/code&gt; directory; the disk image lives here, not inside the container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-v ~/Downloads:/iso:z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Podman&lt;/td&gt;
&lt;td&gt;Bind-mounts the ISO directory; only needed during the install step&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What You've Built
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ qcow2 disk image created on the host&lt;/li&gt;
&lt;li&gt;✅ Alpine Linux installed to disk inside a KVM container&lt;/li&gt;
&lt;li&gt;✅ VM boots from disk and survives container restarts&lt;/li&gt;
&lt;li&gt;✅ Host filesystem as the persistence layer&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;That install took a few minutes of interactive prompts. Every time you want a new Alpine VM, you'd repeat it from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Post #4:&lt;/strong&gt; We'll skip the installer entirely by using a cloud image — a pre-built disk image ready to boot in seconds.&lt;/p&gt;




&lt;p&gt;This guide is &lt;strong&gt;Part 3&lt;/strong&gt; of the &lt;strong&gt;KVM Virtual Machines on Podman&lt;/strong&gt; series.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1:&lt;/strong&gt; &lt;a href="//KVM-01-CONTAINER.md"&gt;Build a KVM-Ready Container Image from Scratch&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Part 2:&lt;/strong&gt; &lt;a href="//KVM-02-KVM-ACCELERATION.md"&gt;KVM Acceleration in a Rootless Podman Container&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Coming up in Part 4:&lt;/strong&gt; Cloud Images — Skip the Installer, Boot in Seconds&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Found this helpful?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://linkedin.com/in/davidtio" rel="noopener noreferrer"&gt;Share with your network&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twitter:&lt;/strong&gt; &lt;a href="https://twitter.com/intent/tweet?text=Persistent%20KVM%20VMs%20inside%20rootless%20Podman!&amp;amp;url=https://blog.dtio.app/2026/03/kvm-virtual-machines-on-podman-3.html" rel="noopener noreferrer"&gt;Tweet about it&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Questions?&lt;/strong&gt; Drop a comment below or reach out on LinkedIn&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Published:&lt;/strong&gt; 6 Apr 2026&lt;br&gt;
&lt;strong&gt;Author:&lt;/strong&gt; David Tio&lt;br&gt;
&lt;strong&gt;Tags:&lt;/strong&gt; KVM, QEMU, Podman, Virtualization, Containers, Alpine Linux, qcow2, Linux, Tutorial&lt;br&gt;
&lt;strong&gt;Series:&lt;/strong&gt; KVM Virtual Machines on Podman&lt;br&gt;
&lt;strong&gt;Word Count:&lt;/strong&gt; ~750&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;SEO Metadata:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Title:&lt;/strong&gt; Persistent VMs in Podman: Install Alpine to a qcow2 Disk Image (2026)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta Description:&lt;/strong&gt; Create a qcow2 disk image with qemu-img, install Alpine Linux inside a rootless Podman container, and boot from disk so your VM survives container restarts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target Keywords:&lt;/strong&gt; qcow2 podman vm, persistent vm podman, qemu-img create qcow2, alpine linux install qemu, podman kvm persistent disk, vm disk image container&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Series:&lt;/strong&gt; KVM Virtual Machines on Podman&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kvm</category>
      <category>podman</category>
      <category>linux</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>KVM Acceleration in a Rootless Podman Container: Before and After</title>
      <dc:creator>David Tio</dc:creator>
      <pubDate>Mon, 30 Mar 2026 12:53:58 +0000</pubDate>
      <link>https://dev.to/davidtio/kvm-acceleration-in-a-rootless-podman-container-before-and-after-hf4</link>
      <guid>https://dev.to/davidtio/kvm-acceleration-in-a-rootless-podman-container-before-and-after-hf4</guid>
      <description>&lt;h1&gt;
  
  
  KVM Acceleration in a Rootless Podman Container: Before and After
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Quick one-liner:&lt;/strong&gt; Pass &lt;code&gt;/dev/kvm&lt;/code&gt; into your Podman container, boot Alpine Linux with &lt;code&gt;-nographic&lt;/code&gt;, and time the difference between software emulation and hardware acceleration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;In Post #1, we built a custom &lt;code&gt;qemu:base&lt;/code&gt; container image with QEMU fully installed. I mentioned that if you tried to boot a VM without KVM it would be crawling slow. Let's test that theory.&lt;/p&gt;

&lt;p&gt;Without KVM, QEMU runs in pure software emulation mode. Every single CPU instruction your VM executes gets translated and re-executed by QEMU on the host. Your modern multi-GHz processor spends most of its time pretending to be a slower, imaginary processor. An OS that boots in 10 seconds on bare metal can take 5–10 minutes in software emulation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KVM changes everything.&lt;/strong&gt; KVM (Kernel-based Virtual Machine) is a Linux kernel module that exposes your CPU's hardware virtualization extensions — Intel VT-x or AMD-V — to user-space software like QEMU. Instead of translating instructions, QEMU hands them directly to the CPU. The VM runs at near-native speed.&lt;/p&gt;

&lt;p&gt;This post makes that difference measurable. You'll boot Alpine Linux twice — once without KVM, once with — and time both. The gap is dramatic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;qemu:base&lt;/code&gt; image from Post #1&lt;/li&gt;
&lt;li&gt;A host CPU with Intel VT-x or AMD-V (most CPUs made after 2010)&lt;/li&gt;
&lt;li&gt;Virtualization enabled in your BIOS/UEFI&lt;/li&gt;
&lt;li&gt;Alpine Linux 3.23.3 x86_64 ISO (~347 MB) downloaded to your machine&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: Get the Alpine ISO
&lt;/h2&gt;

&lt;p&gt;Alpine Linux is the perfect test ISO: it's tiny, boots fast, and drops you to a login prompt with minimal fanfare. That makes boot time easy to measure.&lt;/p&gt;

&lt;p&gt;Download the standard x86_64 ISO:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;wget https://dl-cdn.alpinelinux.org/alpine/v3.23/releases/x86_64/alpine-standard-3.23.3-x86_64.iso &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-O&lt;/span&gt; ~/Downloads/alpine-standard-3.23.3-x86_64.iso
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The download is ~347 MB. Once it's on disk, you'll mount &lt;code&gt;~/Downloads&lt;/code&gt; into the container as &lt;code&gt;/vms&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Boot WITHOUT KVM (baseline)
&lt;/h2&gt;

&lt;p&gt;Let's establish the baseline. This is pure software emulation — no KVM, no hardware acceleration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;time &lt;/span&gt;podman run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; ~/Downloads:/vms:z &lt;span class="se"&gt;\&lt;/span&gt;
    qemu:base &lt;span class="se"&gt;\&lt;/span&gt;
    qemu-system-x86_64 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-nographic&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-m&lt;/span&gt; 512 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-cdrom&lt;/span&gt; /vms/alpine-standard-3.23.3-x86_64.iso
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch the output. QEMU will print boot messages, then Alpine's init system will work through its startup sequence. You'll eventually see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;localhost login:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you see the login prompt, press &lt;code&gt;Ctrl+A&lt;/code&gt; then &lt;code&gt;X&lt;/code&gt; to exit QEMU. The &lt;code&gt;time&lt;/code&gt; command will print how long it took.&lt;/p&gt;

&lt;p&gt;On my machine this came in at &lt;strong&gt;~22 seconds&lt;/strong&gt;. Alpine is small enough that even software emulation is bearable. Write your number down — the comparison with KVM is still telling.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Verify KVM is Available on Your Host
&lt;/h2&gt;

&lt;p&gt;Before adding &lt;code&gt;--device /dev/kvm&lt;/code&gt;, check that KVM is actually available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /dev/kvm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;crw-rw----+ 1 root kvm 10, 232 Mar 30 09:00 /dev/kvm
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;/dev/kvm&lt;/code&gt; doesn't exist, either:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Virtualization is disabled in your BIOS.&lt;/strong&gt; Reboot, enter your UEFI settings, and look for "Intel Virtualization Technology", "VT-x", or "AMD-V". Enable it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The KVM kernel module isn't loaded.&lt;/strong&gt; Run &lt;code&gt;sudo modprobe kvm_intel&lt;/code&gt; (or &lt;code&gt;kvm_amd&lt;/code&gt; for AMD CPUs).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Also check that your user can access the device:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; /dev/kvm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rootless Podman passes device permissions through automatically, but your user needs read-write access to &lt;code&gt;/dev/kvm&lt;/code&gt; on the host. If you're in the &lt;code&gt;kvm&lt;/code&gt; group, you're set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;groups&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;kvm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If not: &lt;code&gt;sudo usermod -aG kvm $USER&lt;/code&gt;, then log out and back in.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Boot WITH KVM
&lt;/h2&gt;

&lt;p&gt;Same command, two additions: &lt;code&gt;--device /dev/kvm&lt;/code&gt; for Podman, and &lt;code&gt;-enable-kvm -cpu host&lt;/code&gt; for QEMU.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;time &lt;/span&gt;podman run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--device&lt;/span&gt; /dev/kvm &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; ~/Downloads:/vms:z &lt;span class="se"&gt;\&lt;/span&gt;
    qemu:base &lt;span class="se"&gt;\&lt;/span&gt;
    qemu-system-x86_64 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-enable-kvm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-cpu&lt;/span&gt; host &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-nographic&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-m&lt;/span&gt; 512 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-cdrom&lt;/span&gt; /vms/alpine-standard-3.23.3-x86_64.iso
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is immediate. Boot messages scroll by quickly. Alpine's init sequence runs in seconds.&lt;/p&gt;

&lt;p&gt;Press &lt;code&gt;Ctrl+A&lt;/code&gt; then &lt;code&gt;X&lt;/code&gt; to exit and check the time output. On my machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Without KVM
real    0m21.868s

# With KVM
real    0m7.814s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3x faster&lt;/strong&gt; — and that's on a lightweight OS that was already tolerable in software emulation. On a heavier OS the gap is far wider.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: What the Flags Do
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--device /dev/kvm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Podman&lt;/td&gt;
&lt;td&gt;Passes the KVM character device into the container namespace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-enable-kvm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;QEMU&lt;/td&gt;
&lt;td&gt;Tells QEMU to use the KVM kernel module instead of software emulation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-cpu host&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;QEMU&lt;/td&gt;
&lt;td&gt;Exposes the host's actual CPU model and features to the VM (required for full KVM benefit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-nographic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;QEMU&lt;/td&gt;
&lt;td&gt;Disables the graphical window — redirects all serial output to the terminal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-m 512&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;QEMU&lt;/td&gt;
&lt;td&gt;Allocates 512 MB RAM to the VM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-cdrom /vms/alpine-standard-3.23.3-x86_64.iso&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;QEMU&lt;/td&gt;
&lt;td&gt;Boots from the mounted ISO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-v ~/Downloads:/vms:z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Podman&lt;/td&gt;
&lt;td&gt;Bind-mounts your Downloads directory; &lt;code&gt;:z&lt;/code&gt; sets SELinux relabeling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;-cpu host&lt;/code&gt; and not &lt;code&gt;-cpu qemu64&lt;/code&gt;?&lt;/strong&gt; The default QEMU CPU model (&lt;code&gt;qemu64&lt;/code&gt;) is a minimal baseline that works everywhere but exposes no modern CPU extensions. With &lt;code&gt;-cpu host&lt;/code&gt;, QEMU passes through all of your CPU's features — AVX, AES-NI, etc. — which is both faster and more realistic for testing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does rootless Podman allow &lt;code&gt;/dev/kvm&lt;/code&gt;?&lt;/strong&gt; Podman uses the &lt;code&gt;--device&lt;/code&gt; flag to grant access to specific devices without requiring &lt;code&gt;--privileged&lt;/code&gt;. The container gets read-write access to &lt;code&gt;/dev/kvm&lt;/code&gt; only, nothing else. This is much safer than running the whole container as root.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You've Built
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ KVM device passed into a rootless Podman container&lt;/li&gt;
&lt;li&gt;✅ Alpine Linux booted inside the container with hardware acceleration&lt;/li&gt;
&lt;li&gt;✅ Before/after timing comparison showing the real-world difference&lt;/li&gt;
&lt;li&gt;✅ Hardware-accelerated VM running without root privileges&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;Right now, every time you stop the container, the VM state disappears. Alpine loses any changes you made. The ISO is read-only. The VM has no persistent disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Post #3:&lt;/strong&gt; We'll create a persistent disk image with &lt;code&gt;qemu-img&lt;/code&gt;, attach it to the VM, and install Alpine properly — so the VM survives container restarts.&lt;/p&gt;




&lt;p&gt;This guide is &lt;strong&gt;Part 2&lt;/strong&gt; of the &lt;strong&gt;KVM Virtual Machines on Podman&lt;/strong&gt; series.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1:&lt;/strong&gt; &lt;a href="//KVM-01-CONTAINER.md"&gt;Build a KVM-Ready Container Image from Scratch&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Coming up in Part 3:&lt;/strong&gt; Persistent Disk Images — Keep Your VM Between Runs&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Found this helpful?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://linkedin.com/in/davidtio" rel="noopener noreferrer"&gt;Share with your network&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twitter:&lt;/strong&gt; &lt;a href="https://twitter.com/intent/tweet?text=Running%20KVM-accelerated%20VMs%20inside%20rootless%20Podman!" rel="noopener noreferrer"&gt;Tweet about it&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Questions?&lt;/strong&gt; Drop a comment below or reach out on LinkedIn&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Published:&lt;/strong&gt; 30 Mar 2026&lt;br&gt;
&lt;strong&gt;Author:&lt;/strong&gt; David Tio&lt;br&gt;
&lt;strong&gt;Tags:&lt;/strong&gt; KVM, QEMU, Podman, Virtualization, Containers, Alpine Linux, Linux, Tutorial&lt;br&gt;
&lt;strong&gt;Series:&lt;/strong&gt; KVM Virtual Machines on Podman&lt;br&gt;
&lt;strong&gt;Word Count:&lt;/strong&gt; ~900&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;SEO Metadata:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Title:&lt;/strong&gt; KVM Acceleration in a Rootless Podman Container: Before and After (2026)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta Description:&lt;/strong&gt; Enable KVM hardware acceleration in a rootless Podman container. Boot Alpine Linux with and without KVM, time the difference, and understand every flag involved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target Keywords:&lt;/strong&gt; kvm podman rootless, enable kvm container, --device /dev/kvm podman, qemu kvm acceleration, alpine linux qemu container, hardware virtualization podman&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Series:&lt;/strong&gt; KVM Virtual Machines on Podman&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kvm</category>
      <category>podman</category>
      <category>linux</category>
      <category>virtualization</category>
    </item>
    <item>
      <title>Building a Blog Platform with Docker #1: Flask Setup</title>
      <dc:creator>David Tio</dc:creator>
      <pubDate>Mon, 30 Mar 2026 10:41:42 +0000</pubDate>
      <link>https://dev.to/davidtio/building-a-blog-platform-with-docker-1-flask-setup-5h10</link>
      <guid>https://dev.to/davidtio/building-a-blog-platform-with-docker-1-flask-setup-5h10</guid>
      <description>&lt;h1&gt;
  
  
  Building a Blog Platform with Docker #1: Flask Setup
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Quick one-liner:&lt;/strong&gt; Get a basic Flask app running with separate CSS — no Docker yet, just Python and a stylesheet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;I'm building a new blog platform.&lt;/p&gt;

&lt;p&gt;The reason is simple: I'm tired of writing HTML by hand. In 2026. For my tech blog. It's embarrassing.&lt;/p&gt;

&lt;p&gt;I also want series grouping (so readers can actually navigate my Docker tutorials), and I want to own the platform instead of renting space on Blogger. Plus, I wrote &lt;em&gt;Levelling Docker&lt;/em&gt; — might as well apply the same "learn by building" approach to my own infrastructure.&lt;/p&gt;

&lt;p&gt;This is the first post in what will be an occasional series. I'll build features, write about them, and share the code. No fixed schedule. No promises about how many posts. We'll see where it goes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Starting Simple
&lt;/h2&gt;

&lt;p&gt;Today's goal: Get a Flask app running that says "Welcome to David Tio's Blog".&lt;/p&gt;

&lt;p&gt;That's it. No Docker yet. No database. No Markdown. Just a basic Python web app.&lt;/p&gt;

&lt;p&gt;Docker comes later — only when the app is more or less ready.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;For this post I'll be using terminal and vscodium. You can use any recent Linux distro and any text editor if you want to follow along.&lt;/p&gt;

&lt;p&gt;Create a folder for the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;tiohub-blog
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;tiohub-blog
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;templates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set up a virtual environment (always use venv, trust me):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that venv is activated, I will install flask into the virtual environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;flask
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now create the Flask app, &lt;code&gt;app.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;render_template&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;render_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the template, &lt;code&gt;templates/index.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Welcome to David Tio's Blog&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Welcome to David Tio's Blog&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Building a blog platform with Docker.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;a href="http://localhost:8000" rel="noopener noreferrer"&gt;http://localhost:8000&lt;/a&gt;. You'll see... text. Black on white. Very 1993.&lt;/p&gt;

&lt;p&gt;It works. It's also ugly. Let's add some styles.&lt;/p&gt;




&lt;h2&gt;
  
  
  Add a Stylesheet
&lt;/h2&gt;

&lt;p&gt;I don't do inline CSS. Create a separate file from the start.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; static/css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;static/css/style.css&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0f172a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e5e7eb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;system-ui&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;800px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#14b8a6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;border-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#14b8a6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update &lt;code&gt;index.html&lt;/code&gt; to link it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Welcome to David Tio's Blog&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{{ url_for('static', filename='css/style.css') }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Welcome to David Tio's Blog&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Building a blog platform with Docker.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;url_for&lt;/code&gt; thing? Flask generates the correct URL for static files automatically. No hardcoded paths.&lt;/p&gt;

&lt;p&gt;Refresh. Dark background, teal heading, centered layout. Much better.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft86n0nzmj1hbia9t06fj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft86n0nzmj1hbia9t06fj.png" alt="Flask app with dark theme running on localhost:8000" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  A Few Things to Note
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why separate CSS files?&lt;/strong&gt; You could inline the styles. For a single page, it's fine. But I've learned the hard way — inline CSS creeps. Next thing you know, your template is 200 lines and half of it is &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tags.&lt;/p&gt;

&lt;p&gt;Separate files mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easier to find your styles&lt;/li&gt;
&lt;li&gt;Multiple templates can share the same stylesheet&lt;/li&gt;
&lt;li&gt;HTML stays focused on structure&lt;/li&gt;
&lt;li&gt;Ready for Tailwind or a build step later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;url_for&lt;/code&gt;?&lt;/strong&gt; You could hardcode &lt;code&gt;/static/css/style.css&lt;/code&gt;. But then if you change your static folder structure, you have to update every template. &lt;code&gt;url_for&lt;/code&gt; handles that for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why debug mode?&lt;/strong&gt; The &lt;code&gt;debug=True&lt;/code&gt; flag auto-reloads when you change code. It's great for development. Don't use it in production — we'll fix that when we deploy.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's the Planned Stack?
&lt;/h2&gt;

&lt;p&gt;For now: Flask + Python + a CSS file. That's it.&lt;/p&gt;

&lt;p&gt;I'm thinking SQLite for the database (blogs don't need PostgreSQL). Tailwind CSS for styling eventually (via CDN, no build step). Traefik for routing when we add Docker. Markdown for writing posts.&lt;/p&gt;

&lt;p&gt;But here's the thing: this might change. I might swap Traefik for something else. I might decide SQLite isn't enough. I'll figure it out as I build.&lt;/p&gt;

&lt;p&gt;The beauty of starting simple is: you can pivot without losing weeks of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coming Up
&lt;/h2&gt;

&lt;p&gt;Next post will probably be Tailwind CSS. I want a proper navigation bar, a footer, maybe some nicer typography.&lt;/p&gt;

&lt;p&gt;After that: Markdown support. I want to write &lt;code&gt;.md&lt;/code&gt; files and have them render as HTML.&lt;/p&gt;

&lt;p&gt;Then: Docker. I'll containerize the Flask app and show you why it's worth the effort.&lt;/p&gt;

&lt;p&gt;No timeline on any of this. I'll post when I've built something worth sharing.&lt;/p&gt;




&lt;p&gt;If you're following along and want to see something specific, drop a comment or reach out. This is a public build — your feedback might shape what I build next.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Found this helpful?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://linkedin.com/in/davidtio" rel="noopener noreferrer"&gt;Share with your network&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twitter:&lt;/strong&gt; &lt;a href="https://twitter.com/intent/tweet?text=Building%20a%20blog%20platform%20with%20Docker!" rel="noopener noreferrer"&gt;Tweet about it&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Questions?&lt;/strong&gt; Drop a comment below or reach out on LinkedIn&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Published:&lt;/strong&gt; 29 Mar 2026&lt;br&gt;
&lt;strong&gt;Author:&lt;/strong&gt; David Tio&lt;br&gt;
&lt;strong&gt;Tags:&lt;/strong&gt; Python, Flask, Docker, Blog Platform, Build Log&lt;br&gt;
&lt;strong&gt;Word Count:&lt;/strong&gt; ~800&lt;/p&gt;




&lt;h2&gt;
  
  
  SEO Metadata
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Title:&lt;/strong&gt; Building a Blog Platform with Docker #1: Flask Setup (2026)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta Description:&lt;/strong&gt; Follow along as I build a blog platform from scratch. Episode 1: Flask setup with separate CSS. Build log with code examples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target Keywords:&lt;/strong&gt; flask setup, python blog build log, docker blog series, flask css separate file, building blog platform 2026&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Word Count:&lt;/strong&gt; ~800&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>flask</category>
      <category>docker</category>
      <category>webdev</category>
    </item>
    <item>
      <title>SLES 16: Add a DVD as a Local Zypper Repository (No Subscription Needed)</title>
      <dc:creator>David Tio</dc:creator>
      <pubDate>Fri, 27 Mar 2026 23:49:27 +0000</pubDate>
      <link>https://dev.to/davidtio/sles-16-add-a-dvd-as-a-local-zypper-repository-no-subscription-needed-fpp</link>
      <guid>https://dev.to/davidtio/sles-16-add-a-dvd-as-a-local-zypper-repository-no-subscription-needed-fpp</guid>
      <description>&lt;h1&gt;
  
  
  💿 SLES 16: Add a DVD as a Local Zypper Repository (No Subscription Needed)
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Quick one-liner:&lt;/strong&gt; Mount the SLES 16 installation DVD (or ISO) as a local zypper repository so you can install packages without a subscription — perfect for air-gapped environments, home lab testing, or grabbing packages you skipped during installation.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 Why This Matters
&lt;/h2&gt;

&lt;p&gt;SLES out of the box is subscription-gated. Try to install anything with &lt;code&gt;zypper&lt;/code&gt; on a fresh system and you'll hit this immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warning: There are no enabled repositories defined.
Use 'zypper addrepo' or 'zypper modifyrepo' commands to add or enable repositories.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This affects you in three common situations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔒 Air-gapped environments&lt;/strong&gt; — Your network has no path to the SUSE Customer Center. There's no RMT server. &lt;code&gt;zypper&lt;/code&gt; simply can't reach anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;💸 No subscription&lt;/strong&gt; — You're evaluating SLES in a home lab, or setting up a test box without a paid licence. The online repos are locked behind a registered system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📦 Missed packages from installation&lt;/strong&gt; — You kept the installer lean and now realise you need &lt;code&gt;gcc&lt;/code&gt;, &lt;code&gt;vim&lt;/code&gt;, &lt;code&gt;rsync&lt;/code&gt;, or something else that was on the DVD all along. Why re-run the installer when the media is right there?&lt;/p&gt;

&lt;p&gt;The DVD you used to install SLES 16 already contains hundreds of packages. Adding it as a local zypper repository unlocks all of them instantly — no internet, no subscription, no reinstall.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OS:&lt;/strong&gt; SLES 16 installed and running&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media:&lt;/strong&gt; SLES 16 Full installation DVD (physical) or ISO file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access:&lt;/strong&gt; &lt;code&gt;sudo&lt;/code&gt; privileges&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; ~5 minutes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔍 Before You Start: Check for an Existing Repository
&lt;/h2&gt;

&lt;p&gt;SLES 16 often creates a DVD repository automatically during installation — it's just disabled by default. Check if it's already there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/zypper/repos.d/SLES.repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the file exists, you'll see something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[SLES]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;SUSE Linux Enterprise Server 16.0&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;autorefresh&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;baseurl&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;dvd:/install&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;rpm-md&lt;/span&gt;
&lt;span class="py"&gt;keeppackages&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;zypper modifyrepo &lt;span class="nt"&gt;--enable&lt;/span&gt; SLES
&lt;span class="c"&gt;# or the short form:&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;zypper mr &lt;span class="nt"&gt;-e&lt;/span&gt; SLES
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then skip straight to &lt;strong&gt;Step 5: Install Packages&lt;/strong&gt; below — you don't need to mount anything manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the file doesn't exist&lt;/strong&gt;, follow Steps 1–4 below to mount the DVD and add the repository manually.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔧 Step 1: Mount the DVD
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Physical DVD drive:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/dvd
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount /dev/sr0 /mnt/dvd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm the drive device with &lt;code&gt;lsblk&lt;/code&gt; if &lt;code&gt;/dev/sr0&lt;/code&gt; isn't right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ISO file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/dvd
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-o&lt;/span&gt; loop /path/to/SLES-16.0-Full-x86_64.iso /mnt/dvd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the mount:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /mnt/dvd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.signature  EFI  LiveOS  boot  install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The packages and repository metadata live inside the &lt;code&gt;install/&lt;/code&gt; subdirectory. You can confirm it's a valid zypper repository with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /mnt/dvd/install/repodata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ➕ Step 2: Add the Repository
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;zypper ar /mnt/dvd SLES16-DVD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;zypper discovers the repository metadata automatically — no need to point it at the &lt;code&gt;install/&lt;/code&gt; subdirectory, and no GPG trust prompt.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔄 Step 3: Refresh the Repository
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;zypper ref SLES16-DVD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Repository 'SLES16-DVD' is up to date.
All repositories have been refreshed.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  📌 Step 4: Make the Mount Persistent (Optional)
&lt;/h2&gt;

&lt;p&gt;The mount disappears after a reboot. For a permanent setup, add it to &lt;code&gt;/etc/fstab&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For a physical DVD:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"/dev/sr0  /mnt/dvd  iso9660  ro,noauto  0 0"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For an ISO file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"/path/to/SLES-16.0-Full-x86_64.iso  /mnt/dvd  iso9660  ro,loop,noauto  0 0"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;noauto&lt;/code&gt; flag means it won't try to mount at boot (which would fail if the DVD isn't in the drive). Mount it manually when you need it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mount /mnt/dvd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  📥 Step 5: Install Packages
&lt;/h2&gt;

&lt;p&gt;You're ready. Install anything available on the DVD the same way you normally would:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;zypper &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &amp;lt;package-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;zypper &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; vim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Loading repository data...
Reading installed packages...
Resolving package dependencies...

The following NEW package is going to be installed:
  vim

1 new package to install.
Overall download size: 1.7 MiB. Already cached: 0 B. After the operation, additional 5.3 MiB will be used.
Continue? [y/n/v/...? shows all options] (y): y
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;zypper reads directly from the mounted media — no internet, no subscription check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;💡 Not sure what's available?&lt;/strong&gt; Search the repo before installing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# If you enabled the default repository:&lt;/span&gt;
zypper search &lt;span class="nt"&gt;--repo&lt;/span&gt; SLES &amp;lt;keyword&amp;gt;

&lt;span class="c"&gt;# If you added it manually:&lt;/span&gt;
zypper search &lt;span class="nt"&gt;--repo&lt;/span&gt; SLES16-DVD &amp;lt;keyword&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🗑️ Removing the Repository
&lt;/h2&gt;

&lt;p&gt;When you no longer need it, clean up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# If you added it manually:&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;zypper rr SLES16-DVD
&lt;span class="nb"&gt;sudo &lt;/span&gt;umount /mnt/dvd

&lt;span class="c"&gt;# If you enabled the default repository:&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;zypper mr &lt;span class="nt"&gt;-d&lt;/span&gt; SLES
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ⚠️ What You Can and Can't Install
&lt;/h2&gt;

&lt;p&gt;The DVD contains everything that shipped with SLES 16 at release — base packages, development tools, and common server software. That covers the vast majority of day-to-day needs.&lt;/p&gt;

&lt;p&gt;What it won't have:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Limitation&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🔒 Security updates&lt;/td&gt;
&lt;td&gt;DVD packages are release-day versions only. Updates require a subscription or a local mirror.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📦 Third-party packages&lt;/td&gt;
&lt;td&gt;Anything outside the SLES base (e.g. Docker CE, custom RPMs) needs a separate repo.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🧩 PackageHub / Modules&lt;/td&gt;
&lt;td&gt;These are subscription-only channels and aren't on the Full DVD.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a fully up-to-date air-gapped environment, pair this with a local &lt;strong&gt;RMT (Repository Mirroring Tool)&lt;/strong&gt; server that you sync once and distribute internally.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Need Docker on this SLES system? Check out &lt;a href="https://blog.dtio.app/2026/03/how-to-install-docker-rootless-on-sles.html" rel="noopener noreferrer"&gt;How to Install Docker Rootless on SLES 15/16&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;🙌 Found this useful? Share it with anyone setting up SLES in an air-gapped environment.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;SEO Metadata:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Title:&lt;/strong&gt; SLES 16: Add a DVD as a Local Zypper Repository (No Subscription Needed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta Description:&lt;/strong&gt; Mount the SLES 16 DVD or ISO as a local zypper repository to install packages without a subscription. Works in air-gapped environments. Step-by-step guide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target Keywords:&lt;/strong&gt; sles 16 local repository, zypper add dvd repo, sles no subscription install packages, sles 16 airgap repository, zypper ar iso sles, sles offline package install&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Word Count:&lt;/strong&gt; ~750&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sles</category>
      <category>linux</category>
      <category>sysadmin</category>
      <category>airgap</category>
    </item>
    <item>
      <title>Build a KVM-Ready Container Image from Scratch</title>
      <dc:creator>David Tio</dc:creator>
      <pubDate>Wed, 25 Mar 2026 14:49:16 +0000</pubDate>
      <link>https://dev.to/davidtio/build-a-kvm-ready-container-image-from-scratch-2c6g</link>
      <guid>https://dev.to/davidtio/build-a-kvm-ready-container-image-from-scratch-2c6g</guid>
      <description>&lt;h1&gt;
  
  
  Build a KVM-Ready Container Image from Scratch
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Quick one-liner:&lt;/strong&gt; Learn how to build a custom Podman container image with KVM/QEMU installed — the first step to running hardware-accelerated virtual machines inside containers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;You've probably heard that containers and virtual machines are different things. Containers share the host kernel. VMs have their own kernel. They're opposites, right?&lt;/p&gt;

&lt;p&gt;Well, here's the thing: sometimes you need both.&lt;/p&gt;

&lt;p&gt;Maybe you need to test software on a different architecture. Or run a legacy OS that won't work in a container. Or isolate something even more securely than containers provide.&lt;/p&gt;

&lt;p&gt;That's where KVM and QEMU come in. QEMU is a free, open-source emulator that can run virtual machines. KVM (Kernel-based Virtual Machine) is the Linux kernel feature that gives QEMU direct access to your CPU's hardware virtualization extensions (Intel VT-x or AMD-V). And yes — you can run them inside a container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But here's the catch:&lt;/strong&gt; The official QEMU images are built for specific use cases. If you want full control over what's installed and how it's configured, you need to build your own.&lt;/p&gt;

&lt;p&gt;This guide walks you through building a custom Podman container image with QEMU and KVM support installed from scratch. No black boxes. No mystery dependencies. Just you, a Containerfile, and a working KVM setup.&lt;/p&gt;

&lt;p&gt;By the end, you'll have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A custom Containerfile tailored for KVM/QEMU&lt;/li&gt;
&lt;li&gt;A working Podman image with QEMU installed&lt;/li&gt;
&lt;li&gt;Understanding of what each layer does&lt;/li&gt;
&lt;li&gt;A foundation to build on in future posts (next: enable KVM acceleration!)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Podman installed&lt;/strong&gt; (rootless mode is the default — see your distro's Podman package)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5-10 minutes&lt;/strong&gt; to build the image&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terminal access&lt;/strong&gt; to your Podman host&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Basic Containerfile knowledge&lt;/strong&gt; (FROM, RUN, CMD instructions)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: Create Your Project Directory
&lt;/h2&gt;

&lt;p&gt;First, let's set up a clean workspace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/qemu-container
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/qemu-container
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're going to build everything in this directory. When you're done, you can delete it or keep it for reference.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Write the Containerfile
&lt;/h2&gt;

&lt;p&gt;Create a file named &lt;code&gt;Containerfile&lt;/code&gt; (no extension) in your project directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;nano Containerfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what goes in it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# QEMU Container Image — Base Setup
# Build: podman build -t qemu-base .
# Run:   podman run --rm -it qemu-base

FROM ubuntu:24.04

LABEL maintainer="Your Name &amp;lt;your.email@example.com&amp;gt;"
LABEL description="QEMU emulator in a Podman container"
LABEL version="1.0"

# Prevent interactive prompts during package installation
ENV DEBIAN_FRONTEND=noninteractive

# Update package lists and install QEMU
RUN apt-get update &amp;amp;&amp;amp; \
    apt-get install -y --no-install-recommends \
        qemu-system-x86 \
        qemu-utils \
        qemu-system-common \
        libvirt-daemon-system \
        libvirt-clients \
        bridge-utils \
        virt-manager \
    &amp;amp;&amp;amp; apt-get clean \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

# Set working directory for VM files
WORKDIR /vms

# Default command — show QEMU version
CMD ["qemu-system-x86_64", "--version"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let me break down what each section does:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FROM ubuntu:24.04&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start from Ubuntu 24.04 LTS — stable, well-documented, good QEMU support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ENV DEBIAN_FRONTEND=noninteractive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prevents package installation from hanging on configuration prompts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RUN apt-get update &amp;amp;&amp;amp; apt-get install -y&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Updates package lists and installs QEMU packages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--no-install-recommends&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Skips optional packages — keeps the image smaller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qemu-system-x86&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The main QEMU emulator for x86_64 machines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qemu-utils&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Utilities like &lt;code&gt;qemu-img&lt;/code&gt; for managing disk images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qemu-system-common&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Common files shared by QEMU system emulators&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;libvirt-daemon-system&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Libvirt daemon for managing virtualization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;libvirt-clients&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Client tools like &lt;code&gt;virsh&lt;/code&gt; to interact with libvirt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bridge-utils&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Network bridge utilities for VM networking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;virt-manager&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Virtual Machine Manager GUI (optional, useful for testing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apt-get clean &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cleans up package cache — reduces image size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WORKDIR /vms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sets default working directory for VM files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CMD ["qemu-system-x86_64", "--version"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shows QEMU version when container starts (useful for testing)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why Ubuntu?&lt;/strong&gt; You could use Alpine, Debian, or Fedora. But Ubuntu has the best documentation, largest community, and most stable QEMU packages. For a learning setup, it's the right choice.&lt;/p&gt;

&lt;p&gt;Save the file and exit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Build the Image
&lt;/h2&gt;

&lt;p&gt;Now build the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;podman build &lt;span class="nt"&gt;-t&lt;/span&gt; qemu-base &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see output like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;STEP 1/10: FROM ubuntu:24.04
STEP 2/10: LABEL maintainer="Your Name &amp;lt;your.email@example.com&amp;gt;"
...
STEP 10/10: CMD ["qemu-system-x86_64", "--version"]
COMMIT qemu-base
--&amp;gt; a1b2c3d4e5f6
Successfully built qemu-base
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build downloads the base Ubuntu image, installs QEMU and all dependencies, then commits the result as &lt;code&gt;qemu-base&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First build tip:&lt;/strong&gt; The first time you build, it'll take a few minutes to download packages. Subsequent builds are faster because Podman caches layers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Test the Image
&lt;/h2&gt;

&lt;p&gt;Let's verify QEMU is actually installed and working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;podman run &lt;span class="nt"&gt;--rm&lt;/span&gt; qemu-base
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see QEMU's version information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;QEMU emulator version 8.2.2 (Debian 1:8.2.2+ds-0ubuntu1.13)
Copyright (c) 2003-2023 Fabrice Bellard and the QEMU Project developers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Success!&lt;/strong&gt; QEMU is installed and working inside the container.&lt;/p&gt;

&lt;p&gt;But wait — that's just the version check. Let's actually run QEMU interactively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;podman run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; qemu-base /bin/bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're now inside the container. Try running QEMU directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@container-id:/vms# qemu-system-x86_64 &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same version output. Good.&lt;/p&gt;

&lt;p&gt;Now let's try something more interesting — boot a tiny test VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@container-id:/vms# qemu-system-x86_64 &lt;span class="nt"&gt;-cpu&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lists all CPU models QEMU can emulate. You should see a long list including &lt;code&gt;qemu64&lt;/code&gt;, &lt;code&gt;host&lt;/code&gt;, &lt;code&gt;Nehalem&lt;/code&gt;, &lt;code&gt;Haswell&lt;/code&gt;, and many more.&lt;/p&gt;

&lt;p&gt;Exit the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@container-id:/vms# &lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 5: Check Image Size
&lt;/h2&gt;

&lt;p&gt;Let's see how big this image is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;podman images qemu-base
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;REPOSITORY           TAG         IMAGE ID      CREATED        SIZE
localhost/qemu-base  latest      568a6950c2ea  5 minutes ago  439 MB
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;439 MB&lt;/strong&gt; — pretty reasonable for a full QEMU setup with GUI tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want it smaller?&lt;/strong&gt; Remove &lt;code&gt;virt-manager&lt;/code&gt; and &lt;code&gt;libvirt&lt;/code&gt; packages if you only need command-line QEMU. That shaves off ~100 MB. But for learning, the full setup is worth it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Tag and Organize
&lt;/h2&gt;

&lt;p&gt;Let's give this image a better tag for future use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;podman tag qemu-base qemu:base
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can refer to it as &lt;code&gt;qemu:base&lt;/code&gt; instead of &lt;code&gt;qemu-base&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;List your images:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;podman images qemu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see both tags pointing to the same image ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
qemu         base      a1b2c3d4e5f6   3 minutes ago   1.2 GB
qemu-base    latest    a1b2c3d4e5f6   3 minutes ago   1.2 GB
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What You've Built
&lt;/h2&gt;

&lt;p&gt;You now have a working QEMU container image with:&lt;/p&gt;

&lt;p&gt;✅ QEMU system emulator (x86_64)&lt;br&gt;
✅ Disk image utilities (&lt;code&gt;qemu-img&lt;/code&gt;)&lt;br&gt;
✅ Libvirt management tools&lt;br&gt;
✅ Network bridge support&lt;br&gt;
✅ Clean, documented Containerfile&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But here's the thing:&lt;/strong&gt; Right now, this is just an image. You can run QEMU commands, but you can't actually boot a VM yet.&lt;/p&gt;

&lt;p&gt;Why? Because you don't have a disk image to boot.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;You've got QEMU installed in a container. But if you try to boot a VM right now, it'll be &lt;strong&gt;painfully slow&lt;/strong&gt; — like, 10 minutes to boot an OS that normally boots in 30 seconds.&lt;/p&gt;

&lt;p&gt;Why? Because you're using pure software emulation. Every CPU instruction is translated by QEMU instead of running directly on your hardware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next time:&lt;/strong&gt; We'll enable KVM acceleration — Intel VT-x or AMD-V hardware virtualization — and speed up VM boot times by 10-20x.&lt;/p&gt;

&lt;p&gt;But there's a catch: KVM requires special device access from inside the container. And that's where things get interesting with Podman.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want More?
&lt;/h2&gt;

&lt;p&gt;This guide is &lt;strong&gt;Part 1&lt;/strong&gt; of the &lt;strong&gt;KVM Virtual Machines on Podman&lt;/strong&gt; series. Each post builds on the last, adding one capability at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coming up in Part 2:&lt;/strong&gt; Enable KVM Acceleration: 10x Faster VMs in Rootless Podman&lt;/p&gt;

&lt;p&gt;📚 &lt;strong&gt;Prefer the full book?&lt;/strong&gt; Check out &lt;em&gt;"Levelling Up with Docker"&lt;/em&gt; on Amazon for 14 chapters of practical Docker guides.&lt;/p&gt;

&lt;p&gt;📖 &lt;strong&gt;Missed a post?&lt;/strong&gt; Start from the beginning: &lt;a href="//../BLOG-POST-04-FIRST-CONTAINER.md"&gt;Run Your First Docker Container&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Found this helpful?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://linkedin.com/in/davidtio" rel="noopener noreferrer"&gt;Share with your network&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twitter:&lt;/strong&gt; &lt;a href="https://twitter.com/intent/tweet?text=Built%20my%20own%20KVM-ready%20container%20image!" rel="noopener noreferrer"&gt;Tweet about it&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Questions?&lt;/strong&gt; Drop a comment below or reach out on LinkedIn&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Published:&lt;/strong&gt; 23 Mar 2026&lt;br&gt;
&lt;strong&gt;Author:&lt;/strong&gt; David Tio&lt;br&gt;
&lt;strong&gt;Tags:&lt;/strong&gt; KVM, QEMU, Podman, Virtualization, Containers, Linux, Tutorial&lt;br&gt;
&lt;strong&gt;Series:&lt;/strong&gt; KVM Virtual Machines on Podman&lt;br&gt;
&lt;strong&gt;Word Count:&lt;/strong&gt; ~800&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;SEO Metadata:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Title:&lt;/strong&gt; Build a KVM-Ready Container Image from Scratch (2026 Guide)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta Description:&lt;/strong&gt; Learn how to build a custom Podman container image with KVM/QEMU support. Step-by-step guide to creating a Containerfile, building the image, and preparing for hardware-accelerated virtualization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target Keywords:&lt;/strong&gt; kvm podman container, build qemu image, podman build kvm, qemu-system-x86_64 container, rootless kvm, virtualization in containers, hardware acceleration podman&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Series:&lt;/strong&gt; KVM Virtual Machines on Podman&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kvm</category>
      <category>podman</category>
      <category>linux</category>
      <category>virtualization</category>
    </item>
    <item>
      <title>Docker Environment Management: Images, Logs, and Cleanup (2026 Guide)</title>
      <dc:creator>David Tio</dc:creator>
      <pubDate>Mon, 23 Mar 2026 10:42:51 +0000</pubDate>
      <link>https://dev.to/davidtio/docker-environment-management-images-logs-and-cleanup-2026-guide-2hba</link>
      <guid>https://dev.to/davidtio/docker-environment-management-images-logs-and-cleanup-2026-guide-2hba</guid>
      <description>&lt;h1&gt;
  
  
  Docker Environment Management: Images, Logs, and Cleanup (2026 Guide)
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Quick one-liner:&lt;/strong&gt; Learn how to manage your Docker environment like a pro — list and remove images, view container logs, use environment variables, and reclaim disk space.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;After running a few containers, your Docker host starts accumulating stuff — images, stopped containers, unused volumes, build caches.&lt;/p&gt;

&lt;p&gt;I learned this the hard way. A few months into using Docker, I ran &lt;code&gt;df -h&lt;/code&gt; and discovered my home directory was 90% full. Docker had quietly consumed gigabytes of images, orphaned volumes, and build layers.&lt;/p&gt;

&lt;p&gt;That's when I learned the prune commands.&lt;/p&gt;

&lt;p&gt;This guide covers the essential skills for managing your Docker environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Listing and removing images&lt;/li&gt;
&lt;li&gt;Viewing container logs&lt;/li&gt;
&lt;li&gt;Using environment variables (critical for databases)&lt;/li&gt;
&lt;li&gt;Setting restart policies&lt;/li&gt;
&lt;li&gt;Cleaning up disk space safely&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker installed&lt;/strong&gt; (rootless mode recommended)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Basic Docker familiarity&lt;/strong&gt; (run, stop, rm commands)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5 minutes&lt;/strong&gt; to work through the examples&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Managing Images
&lt;/h2&gt;

&lt;p&gt;Every time you run a container from an image you haven't used before, Docker downloads it and stores it locally. Over time, these images consume significant disk space.&lt;/p&gt;

&lt;h3&gt;
  
  
  List All Images
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker images
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IMAGE                      ID             DISK USAGE   CONTENT SIZE
nginx:latest               dec7a90bd097        240MB         65.8MB
redis:latest               315270d16608        204MB         55.3MB
ghcr.io/jqlang/jq:latest   4f34c6d23f4b       3.33MB         1.03MB
hello-world:latest         85404b3c5395       25.9kB         9.52kB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;SIZE&lt;/strong&gt; column shows the compressed size on disk. Just four images from following this series and you're already at ~450MB. Multiply that across months of pulling images and you can see why knowing how to manage them matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remove an Image
&lt;/h3&gt;

&lt;p&gt;To remove an image you no longer need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker rmi hello-world:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If a container is still using the image (even a stopped one), Docker will refuse to remove it. Stop and remove the container first, or use the &lt;code&gt;-f&lt;/code&gt; flag to force removal.&lt;/p&gt;

&lt;p&gt;To remove all unused images at once, see the Cleaning Up Disk Space section below.&lt;/p&gt;




&lt;h2&gt;
  
  
  Viewing Container Logs
&lt;/h2&gt;

&lt;p&gt;When a container fails to start or behaves unexpectedly, the first thing to check is its logs. Docker captures everything a container writes to standard output and standard error.&lt;/p&gt;

&lt;h3&gt;
  
  
  View Logs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker logs dtnginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the nginx startup messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
&lt;/span&gt;&lt;span class="gp"&gt;/docker-entrypoint.sh: Configuration complete;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ready &lt;span class="k"&gt;for &lt;/span&gt;start up
&lt;span class="gp"&gt;2026/03/23 10:08:54 [notice] 1#&lt;/span&gt;1: using the &lt;span class="s2"&gt;"epoll"&lt;/span&gt; event method
&lt;span class="gp"&gt;2026/03/23 10:08:54 [notice] 1#&lt;/span&gt;1: nginx/1.29.6
&lt;span class="gp"&gt;2026/03/23 10:08:54 [notice] 1#&lt;/span&gt;1: built by gcc 14.2.0 &lt;span class="o"&gt;(&lt;/span&gt;Debian 14.2.0-19&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;2026/03/23 10:08:54 [notice] 1#&lt;/span&gt;1: OS: Linux 6.8.0-100-generic
&lt;span class="gp"&gt;2026/03/23 10:08:54 [notice] 1#&lt;/span&gt;1: getrlimit&lt;span class="o"&gt;(&lt;/span&gt;RLIMIT_NOFILE&lt;span class="o"&gt;)&lt;/span&gt;: 1048576:1048576
&lt;span class="gp"&gt;2026/03/23 10:08:54 [notice] 1#&lt;/span&gt;1: start worker processes
&lt;span class="gp"&gt;2026/03/23 10:08:54 [notice] 1#&lt;/span&gt;1: start worker process 29
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key line to look for is &lt;code&gt;Configuration complete; ready for start up&lt;/code&gt; — that confirms nginx started successfully. Everything after that is the worker process startup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Follow Logs in Real Time
&lt;/h3&gt;

&lt;p&gt;To watch logs as they're written (like &lt;code&gt;tail -f&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker logs &lt;span class="nt"&gt;-f&lt;/span&gt; dtnginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;Ctrl+C&lt;/code&gt; to stop following.&lt;/p&gt;

&lt;h3&gt;
  
  
  Show Last N Lines
&lt;/h3&gt;

&lt;p&gt;To see only the last 10 lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker logs &lt;span class="nt"&gt;--tail&lt;/span&gt; 10 dtnginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combine flags for real-time tailing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker logs &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;--tail&lt;/span&gt; 20 dtnginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Environment Variables
&lt;/h2&gt;

&lt;p&gt;Many Docker images are configured through environment variables rather than configuration files. This makes containers flexible — the same image can behave differently depending on the variables you pass at runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Environment Variables
&lt;/h3&gt;

&lt;p&gt;Use the &lt;code&gt;-e&lt;/code&gt; flag to set environment variables when running a container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker container run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; dtpostgres &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;testdb &lt;span class="se"&gt;\&lt;/span&gt;
    postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This starts PostgreSQL with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Root password set to &lt;code&gt;docker&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A database called &lt;code&gt;testdb&lt;/code&gt; automatically created&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Without &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt;, the PostgreSQL container will refuse to start. It's a security requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify Environment Variables
&lt;/h3&gt;

&lt;p&gt;You can verify the variables inside a running container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;dtpostgres &lt;span class="nb"&gt;env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker&lt;/span&gt;
&lt;span class="py"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;testdb&lt;/span&gt;
&lt;span class="err"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Connect and Use the Database
&lt;/h3&gt;

&lt;p&gt;Once the container is running, connect using the PostgreSQL client inside the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; dtpostgres psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-d&lt;/span&gt; testdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're now inside the PostgreSQL shell. Create a table and insert some data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;   &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Alice'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Bob'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="c1"&gt;----+-------&lt;/span&gt;
  &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Alice&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Bob&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit the PostgreSQL shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirms that environment variables aren't just startup flags — they configure a working database that you can immediately connect to and use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Environment Variables
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;postgres&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Required — database password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;postgres&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POSTGRES_DB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional — database to create on startup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;postgres&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POSTGRES_USER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional — custom username (default: postgres)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mysql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MYSQL_ROOT_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Required — root password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redis&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;No env vars required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nginx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;No env vars required&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Each image documents its supported environment variables on its Docker Hub page. Always check before running.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleanup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker stop dtpostgres
&lt;span class="nv"&gt;$ &lt;/span&gt;docker &lt;span class="nb"&gt;rm &lt;/span&gt;dtpostgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Restart Policies
&lt;/h2&gt;

&lt;p&gt;By default, a container stays stopped if it crashes or if the Docker host reboots. Restart policies tell Docker to automatically restart containers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Available Restart Policies
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Do not restart (default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;on-failure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Restart only if the container exits with a non-zero exit code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;always&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Always restart, including after host reboot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unless-stopped&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Like &lt;code&gt;always&lt;/code&gt;, but does not restart if you manually stopped the container&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Run Container with Restart Policy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--restart&lt;/span&gt; unless-stopped &lt;span class="nt"&gt;--name&lt;/span&gt; dtnginx nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you reboot your Docker host, this container will start automatically. If you manually run &lt;code&gt;docker stop dtnginx&lt;/code&gt;, it will stay stopped until you explicitly start it again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Update Restart Policy of Existing Container
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker update &lt;span class="nt"&gt;--restart&lt;/span&gt; unless-stopped dtnginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cleanup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker stop dtnginx
&lt;span class="nv"&gt;$ &lt;/span&gt;docker &lt;span class="nb"&gt;rm &lt;/span&gt;dtnginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Cleaning Up Disk Space
&lt;/h2&gt;

&lt;p&gt;After working with Docker for a while, your host will have accumulated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stopped containers&lt;/li&gt;
&lt;li&gt;Unused images&lt;/li&gt;
&lt;li&gt;Dangling build layers&lt;/li&gt;
&lt;li&gt;Orphaned volumes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Docker provides prune commands to reclaim this space.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check Disk Usage
&lt;/h3&gt;

&lt;p&gt;See how much disk space Docker is using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker system &lt;span class="nb"&gt;df&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          5         2         1.2GB     800MB (66%)
Containers      3         1         50MB      40MB (80%)
Local Volumes   2         1         200MB     100MB (50%)
Build Cache     10        0         300MB     300MB (100%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;RECLAIMABLE&lt;/strong&gt; column shows how much space you can free up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remove Stopped Containers
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker container prune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Remove Unused Images
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker image prune &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Clean Up Everything at Once
&lt;/h3&gt;

&lt;p&gt;To clean up stopped containers, unused images, networks, and build cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker system prune &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker will ask for confirmation before proceeding. This is a safe way to reclaim disk space, but make sure you don't need any of the stopped containers or unused images before running it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exercise
&lt;/h2&gt;

&lt;p&gt;This exercise demonstrates something important — and sets up exactly why you'll need Docker volumes in the next post.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your Tasks
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Start a MySQL container named &lt;code&gt;dtmysql&lt;/code&gt; with &lt;code&gt;MYSQL_ROOT_PASSWORD=docker&lt;/code&gt; and &lt;code&gt;MYSQL_DATABASE=testdb&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Verify the environment variables are set inside the running container&lt;/li&gt;
&lt;li&gt;Connect to MySQL and create a &lt;code&gt;users&lt;/code&gt; table, insert a row with the name &lt;code&gt;Alice&lt;/code&gt;, then query it back&lt;/li&gt;
&lt;li&gt;Stop and delete the container&lt;/li&gt;
&lt;li&gt;Start a fresh &lt;code&gt;dtmysql&lt;/code&gt; container with the same environment variables and try to query the &lt;code&gt;users&lt;/code&gt; table again&lt;/li&gt;
&lt;li&gt;Clean up&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Try it yourself before reading the solution below.&lt;/p&gt;




&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Start MySQL:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; dtmysql &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;testdb &lt;span class="se"&gt;\&lt;/span&gt;
    mysql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Verify env vars:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;dtmysql &lt;span class="nb"&gt;env&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;MYSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Connect and insert data:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; dtmysql mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-pdocker&lt;/span&gt; testdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the MySQL shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Alice'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;-------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;-------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Alice&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;-------+&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Stop and delete the container:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker stop dtmysql
&lt;span class="nv"&gt;$ &lt;/span&gt;docker &lt;span class="nb"&gt;rm &lt;/span&gt;dtmysql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Start a fresh container and query:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; dtmysql &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;testdb &lt;span class="se"&gt;\&lt;/span&gt;
    mysql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait a few seconds for MySQL to initialise, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; dtmysql mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-pdocker&lt;/span&gt; testdb &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM users;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt; &lt;span class="mi"&gt;1146&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="n"&gt;S02&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;Table&lt;/span&gt; &lt;span class="s1"&gt;'testdb.users'&lt;/span&gt; &lt;span class="n"&gt;doesn&lt;/span&gt;&lt;span class="s1"&gt;'t exist
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Alice is gone.&lt;/strong&gt; The container was deleted, and everything inside it went with it.&lt;/p&gt;

&lt;p&gt;This is the fundamental problem with containers: they're ephemeral by design. For stateless apps like nginx, that's fine. For databases, it's a disaster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coming up next:&lt;/strong&gt; Docker volumes — the solution to exactly this problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Clean up:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker stop dtmysql &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker &lt;span class="nb"&gt;rm &lt;/span&gt;dtmysql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Now you can manage your Docker environment effectively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep images organized&lt;/strong&gt; — remove what you don't need&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debug with logs&lt;/strong&gt; — find issues quickly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure with env vars&lt;/strong&gt; — run databases and other configurable images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-restart containers&lt;/strong&gt; — survive reboots&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reclaim disk space&lt;/strong&gt; — prune safely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Coming up:&lt;/strong&gt; Docker persistent volumes — keep your data safe across container restarts and removals.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want More?
&lt;/h2&gt;

&lt;p&gt;This guide covers the basics from &lt;strong&gt;Chapter 4: Managing Docker Environment&lt;/strong&gt; in my book, &lt;em&gt;"Levelling Up with Docker"&lt;/em&gt; — 14 chapters of practical, hands-on Docker guides.&lt;/p&gt;

&lt;p&gt;📚 &lt;strong&gt;Grab the book:&lt;/strong&gt; &lt;a href="https://www.amazon.com/dp/B0GGZ76PHW" rel="noopener noreferrer"&gt;"Levelling Up with Docker" on Amazon&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Found this helpful?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://linkedin.com/in/davidtio" rel="noopener noreferrer"&gt;Share with your network&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twitter:&lt;/strong&gt; &lt;a href="https://twitter.com/intent/tweet?text=Just%20learned%20Docker%20environment%20management!%20%F0%9F%90%B3&amp;amp;url=https://blog.dtio.app/posts/PLACEHOLDER" rel="noopener noreferrer"&gt;Tweet about it&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Questions?&lt;/strong&gt; Drop a comment below or reach out on LinkedIn&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Published:&lt;/strong&gt; 23 Mar 2026&lt;br&gt;
&lt;strong&gt;Author:&lt;/strong&gt; David Tio&lt;br&gt;
&lt;strong&gt;Word Count:&lt;/strong&gt; ~1,100&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;SEO Metadata:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Title:&lt;/strong&gt; Docker Environment Management: Images, Logs, and Cleanup (2026 Guide)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta Description:&lt;/strong&gt; Learn how to manage your Docker environment — list and remove images, view container logs, use environment variables, and reclaim disk space. Includes a hands-on MySQL exercise that shows exactly why volumes matter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target Keywords:&lt;/strong&gt; docker images command, docker logs, docker environment variables, docker system prune, docker image prune, docker cleanup, docker env flag, beginner docker tutorial 2026&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>linux</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Run Your First Docker Container (2026 Step-by-Step)</title>
      <dc:creator>David Tio</dc:creator>
      <pubDate>Mon, 16 Mar 2026 01:54:00 +0000</pubDate>
      <link>https://dev.to/davidtio/run-your-first-docker-container-2026-step-by-step-14ab</link>
      <guid>https://dev.to/davidtio/run-your-first-docker-container-2026-step-by-step-14ab</guid>
      <description>&lt;h1&gt;
  
  
  Run Your First Docker Container (2026 Step-by-Step)
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Quick one-liner:&lt;/strong&gt; Learn how to run your first Docker container — from finding images on Docker Hub to accessing the container shell and cleaning up properly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Now that Docker is installed, it's time to run your first container.&lt;/p&gt;

&lt;p&gt;If you're like most people starting with Docker, you might be tempted to just copy-paste commands without understanding what they do. I've been there. But here's the thing: knowing what each flag does means you'll understand what's happening when you run a container.&lt;/p&gt;

&lt;p&gt;This guide walks you through running your first real container, step by step. No copy-paste without explanation. Every command broken down.&lt;/p&gt;

&lt;p&gt;By the end, you'll know how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Find and pull images from Docker Hub&lt;/li&gt;
&lt;li&gt;Run containers with the right flags&lt;/li&gt;
&lt;li&gt;Access a running container's shell&lt;/li&gt;
&lt;li&gt;Stop and clean up properly&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker installed&lt;/strong&gt; (rootless mode recommended — see &lt;a href="//BLOG-POST-02-UBUNTU.md"&gt;Ubuntu guide&lt;/a&gt; or &lt;a href="//BLOG-POST-03-SLES.md"&gt;SLES guide&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5 minutes&lt;/strong&gt; to run your first container&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terminal access&lt;/strong&gt; to your Docker host&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Finding Images on Docker Hub
&lt;/h2&gt;

&lt;p&gt;Docker Hub is the primary registry for Docker images. Think of it as an app store for containers.&lt;/p&gt;

&lt;p&gt;You can browse at &lt;a href="https://hub.docker.com" rel="noopener noreferrer"&gt;hub.docker.com&lt;/a&gt; or search from the command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker search nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for images with the &lt;strong&gt;Official&lt;/strong&gt; badge — these are maintained by the software's creators and are regularly updated with security patches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My advice:&lt;/strong&gt; If there's an official image, use it. It's more likely to be secure, up-to-date, and well-maintained.&lt;/p&gt;

&lt;p&gt;If there's no official image, look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High download counts&lt;/li&gt;
&lt;li&gt;Recent updates&lt;/li&gt;
&lt;li&gt;Good star ratings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each image page shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Available tags (versions) — for example, &lt;code&gt;nginx:latest&lt;/code&gt;, &lt;code&gt;nginx:1.25&lt;/code&gt;, &lt;code&gt;nginx:alpine&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Usage instructions and supported environment variables&lt;/li&gt;
&lt;li&gt;Pull count and last update date&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; When no tag is specified, Docker uses &lt;code&gt;latest&lt;/code&gt; by default. For production, pin to a specific version like &lt;code&gt;nginx:1.25&lt;/code&gt; so your builds don't break when a new version drops.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running Your First Container
&lt;/h2&gt;

&lt;p&gt;Let's run nginx — the most popular web server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker container run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; dtnginx nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let me break down each flag:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Detach&lt;/strong&gt; — runs in the background so your terminal stays free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--rm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Auto-remove&lt;/strong&gt; — deletes the container when it stops (keeps things clean)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--name dtnginx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Name it&lt;/strong&gt; — use "dtnginx" instead of a random ID like "a1b2c3d4e5f6"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nginx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;The image&lt;/strong&gt; — pulled from Docker Hub if not already present&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first time you run this, Docker downloads the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
09f376ebb190: Pull complete
5529e0792248: Pull complete
Status: Downloaded newer image for nginx:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And... done. The container is now running in the background. You didn't see anything pop up, but trust me, it's there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checking Container Status
&lt;/h2&gt;

&lt;p&gt;Let's verify it's actually running.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;CONTAINER ID   IMAGE   COMMAND                  CREATED   STATUS         PORTS   NAMES
a1b2c3d4e5f6   nginx   "/docker-entrypoint..."   5 sec    Up 4 seconds   80/tcp   dtnginx
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CONTAINER ID&lt;/strong&gt; — you can use this to manage the container. But since we gave it a name (&lt;code&gt;dtnginx&lt;/code&gt;), just use that. It's way easier to remember.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PORTS&lt;/strong&gt; — shows nginx is listening on port 80 inside the container. You can't access it from your host yet — we'll cover port forwarding in a later video.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;STATUS&lt;/strong&gt; — "Up 4 seconds" means it's running.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To see all containers (including stopped ones):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker ps &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Accessing the Container Shell
&lt;/h2&gt;

&lt;p&gt;Here's something cool — you can open a shell inside a running container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; dtnginx /bin/bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let me explain the flags:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-i&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keeps standard input open — so you can type commands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-t&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Allocates a terminal — so you get a proper prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Together, &lt;code&gt;-it&lt;/code&gt; is what you'll use 99% of the time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; The best command to access a container depends on what's inside.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;nginx, Ubuntu, Debian&lt;/strong&gt; → &lt;code&gt;/bin/bash&lt;/code&gt; works&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alpine-based images&lt;/strong&gt; → Use &lt;code&gt;/bin/sh&lt;/code&gt; instead (bash isn't installed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt; → Has &lt;code&gt;/bin/bash&lt;/code&gt; and &lt;code&gt;/bin/sh&lt;/code&gt;, but &lt;code&gt;redis-cli&lt;/code&gt; is more useful for interacting with the database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Always check the image documentation on Docker Hub to see what's available.&lt;/p&gt;

&lt;p&gt;You're now inside the container. Check the prompt — it probably says something like &lt;code&gt;root@a1b2c3d4e5f6&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You're root inside this container. But remember, this is isolated from your host system.&lt;/p&gt;

&lt;p&gt;Let's verify nginx is actually running. The official nginx image includes &lt;code&gt;curl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dtnginx# curl localhost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the default nginx welcome page HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Welcome to nginx!&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit the container shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dtnginx# &lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you're back on your host system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stopping and Cleaning Up
&lt;/h2&gt;

&lt;p&gt;To stop the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker stop dtnginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because we used &lt;code&gt;--rm&lt;/code&gt; when we created it, the container is automatically removed after stopping.&lt;/p&gt;

&lt;p&gt;Let's confirm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker ps &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don't see &lt;code&gt;dtnginx&lt;/code&gt;, that's good — it was automatically removed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you had started a container &lt;strong&gt;without&lt;/strong&gt; &lt;code&gt;--rm&lt;/code&gt;, it would stick around in a stopped state. In that case, you'd need to clean it up manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker &lt;span class="nb"&gt;rm &lt;/span&gt;dtnginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good habit to get into — clean up your stopped containers. If you have the containers from previous tutorials that are not removed and randomly named, you can remove them using the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker container &lt;span class="nb"&gt;rm &lt;/span&gt;container_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Exercise
&lt;/h2&gt;

&lt;p&gt;Try it yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 1: nginx with Shell Access
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run an nginx container&lt;/strong&gt; named &lt;code&gt;dtnginx&lt;/code&gt; in detached mode without the &lt;code&gt;--rm&lt;/code&gt; flag&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access the shell&lt;/strong&gt; with &lt;code&gt;docker exec -it dtnginx /bin/bash&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify nginx is running&lt;/strong&gt; by running &lt;code&gt;curl localhost&lt;/code&gt; inside the container&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit&lt;/strong&gt; the container shell&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop the container&lt;/strong&gt; with &lt;code&gt;docker stop dtnginx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify that it was not removed&lt;/strong&gt; with &lt;code&gt;docker ps -a&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove the container&lt;/strong&gt; with &lt;code&gt;docker rm dtnginx&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Part 2: Redis with redis-cli (No Shell Needed)
&lt;/h3&gt;

&lt;p&gt;Not all containers require a shell to be useful. Redis is a great example — while it does have &lt;code&gt;/bin/bash&lt;/code&gt; and &lt;code&gt;/bin/sh&lt;/code&gt; available, the native &lt;code&gt;redis-cli&lt;/code&gt; tool is far more useful for interacting with the database.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run a Redis container&lt;/strong&gt; named &lt;code&gt;dtredis&lt;/code&gt; with the &lt;code&gt;--rm&lt;/code&gt; flag:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; dtredis redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Access Redis&lt;/strong&gt; using &lt;code&gt;redis-cli&lt;/code&gt; directly:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; dtredis redis-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Set and get a key&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;   127.0.0.1:6379&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;SET mykey &lt;span class="s2"&gt;"Hello from Docker!"&lt;/span&gt;
&lt;span class="go"&gt;   OK
&lt;/span&gt;&lt;span class="gp"&gt;   127.0.0.1:6379&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;GET mykey
&lt;span class="go"&gt;   "Hello from Docker!"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Exit redis-cli&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;   127.0.0.1:6379&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stop the container&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   docker stop dtredis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; The command you use to access a container depends on what's inside. For shells, try &lt;code&gt;/bin/bash&lt;/code&gt; first, then &lt;code&gt;/bin/sh&lt;/code&gt;. For databases and specialized applications, use their native CLI tools (&lt;code&gt;redis-cli&lt;/code&gt;, &lt;code&gt;psql&lt;/code&gt;, &lt;code&gt;mysql&lt;/code&gt;, etc.) — they're often more useful than a generic shell.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Coming up:&lt;/strong&gt; Learn how to manage your Docker environment — listing images, viewing logs, using environment variables, and cleaning up disk space. Then we'll cover Docker volumes to keep your data safe across container restarts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want More?
&lt;/h2&gt;

&lt;p&gt;This guide covers the basics from &lt;strong&gt;Chapter 3: Running Docker Images&lt;/strong&gt; in my book, &lt;em&gt;"Levelling Up with Docker"&lt;/em&gt; — 14 chapters of practical, hands-on Docker guides.&lt;/p&gt;

&lt;p&gt;📚 &lt;strong&gt;Grab the book:&lt;/strong&gt; &lt;a href="https://www.amazon.com/dp/B0GGZ76PHW" rel="noopener noreferrer"&gt;"Levelling Up with Docker" on Amazon&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Found this helpful?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://linkedin.com/in/davidtio" rel="noopener noreferrer"&gt;Share with your network&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twitter:&lt;/strong&gt; &lt;a href="https://twitter.com/intent/tweet?text=Just%20ran%20my%20first%20Docker%20container!" rel="noopener noreferrer"&gt;Tweet about it&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Questions?&lt;/strong&gt; Drop a comment below or reach out on LinkedIn&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Published:&lt;/strong&gt; 17 Mar 2026&lt;br&gt;
&lt;strong&gt;Author:&lt;/strong&gt; David Tio&lt;br&gt;
&lt;strong&gt;Tags:&lt;/strong&gt; Docker, Beginners, Tutorial, Linux, DevOps, Container&lt;br&gt;
&lt;strong&gt;Word Count:&lt;/strong&gt; ~900&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;SEO Metadata:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Title:&lt;/strong&gt; Run Your First Docker Container (2026 Step-by-Step)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta Description:&lt;/strong&gt; Learn how to run your first Docker container — from finding images on Docker Hub to accessing the container shell and cleaning up properly. Step-by-step guide for beginners.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target Keywords:&lt;/strong&gt; docker run command, first docker container, docker exec, docker ps, docker stop, docker hub search, beginner docker tutorial 2026&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Word Count:&lt;/strong&gt; ~900&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>tutorial</category>
      <category>linux</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How to Install Docker Rootless on SLES 15/16 (2026 Guide)</title>
      <dc:creator>David Tio</dc:creator>
      <pubDate>Mon, 09 Mar 2026 01:07:01 +0000</pubDate>
      <link>https://dev.to/davidtio/how-to-install-docker-rootless-on-sles-1516-2026-guide-44al</link>
      <guid>https://dev.to/davidtio/how-to-install-docker-rootless-on-sles-1516-2026-guide-44al</guid>
      <description>&lt;h1&gt;
  
  
  How to Install Docker Rootless on SLES 15/16 (2026 Guide)
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/rootless-banner.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/rootless-banner.png" alt="Docker Rootless Installation"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick one-liner:&lt;/strong&gt; Install Docker in rootless mode on SUSE Linux Enterprise Server (SLES) 15 or 16 using openSUSE repositories — no root privileges required for daily container operations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;When I first started with Docker, I ran everything as root. It was easy, it worked, and I didn't think twice about it. Then I learned that a container escape vulnerability could give an attacker full root access to my entire system. That's when I switched to rootless Docker — and you should too.&lt;/p&gt;

&lt;p&gt;Rootless Docker runs the Docker daemon entirely under your regular user account. No &lt;code&gt;sudo&lt;/code&gt; required. No root privileges for container operations. If a container gets compromised, the attacker is stuck with your user's permissions — not root.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why SLES?&lt;/strong&gt; This guide was written based on community votes — many of you asked for SUSE-specific instructions. Here's the thing: Docker CE doesn't publish official packages for SLES. Instead, we use the &lt;strong&gt;openSUSE Virtualization:containers&lt;/strong&gt; and &lt;strong&gt;security:netfilter&lt;/strong&gt; repositories, which provide &lt;code&gt;docker-stable&lt;/code&gt; and &lt;code&gt;docker-stable-rootless-extras&lt;/code&gt; packages that work perfectly on SLES.&lt;/p&gt;

&lt;p&gt;This guide walks you through the entire process step by step for SUSE Linux Enterprise Server (SLES) 15 and 16.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operating System:&lt;/strong&gt; SUSE Linux Enterprise Server (SLES) 15 or 16&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disk Space:&lt;/strong&gt; At least 20 GB free in your home directory (check with &lt;code&gt;df -h ~&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; 15-20 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access:&lt;/strong&gt; Sudo privileges for initial installation only&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repositories:&lt;/strong&gt; Access to openSUSE repositories (default on most SLES installations)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: Remove Old Docker Packages
&lt;/h2&gt;

&lt;p&gt;Before installing Docker from the openSUSE repositories, remove any conflicting packages from your distribution's default repos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For SLES:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;zypper remove docker docker-client docker-client-latest &lt;span class="se"&gt;\&lt;/span&gt;
    docker-common docker-latest docker-latest-logrotate &lt;span class="se"&gt;\&lt;/span&gt;
    docker-logrotate docker-engine podman runc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures a clean starting point and prevents package conflicts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Add openSUSE Repositories
&lt;/h2&gt;

&lt;p&gt;Docker CE doesn't publish official packages for SLES. Instead, we use the openSUSE repositories which maintain up-to-date Docker packages for SLES.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add the Virtualization:containers repository:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;zypper addrepo &lt;span class="se"&gt;\&lt;/span&gt;
    https://download.opensuse.org/repositories/&lt;span class="se"&gt;\&lt;/span&gt;
Virtualization:/containers/16.0/&lt;span class="se"&gt;\&lt;/span&gt;
Virtualization:containers.repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For SLES 15.x, use this instead:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;zypper addrepo &lt;span class="se"&gt;\&lt;/span&gt;
    https://download.opensuse.org/repositories/&lt;span class="se"&gt;\&lt;/span&gt;
Virtualization:/containers/15.7/&lt;span class="se"&gt;\&lt;/span&gt;
Virtualization:containers.repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Add the security:netfilter repository (required for rootless extras):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;zypper addrepo &lt;span class="se"&gt;\&lt;/span&gt;
    https://download.opensuse.org/repositories/&lt;span class="se"&gt;\&lt;/span&gt;
security:netfilter/16.0/&lt;span class="se"&gt;\&lt;/span&gt;
security:netfilter.repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For SLES 15.x, use this instead:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;zypper addrepo &lt;span class="se"&gt;\&lt;/span&gt;
    https://download.opensuse.org/repositories/&lt;span class="se"&gt;\&lt;/span&gt;
security:netfilter/15.7/&lt;span class="se"&gt;\&lt;/span&gt;
security:netfilter.repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Refresh the repositories:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;zypper refresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Browse available versions:&lt;/strong&gt; If you're using a different SLES version, browse the available releases at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://download.opensuse.org/repositories/Virtualization:/containers/" rel="noopener noreferrer"&gt;https://download.opensuse.org/repositories/Virtualization:/containers/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://download.opensuse.org/repositories/security:netfilter/" rel="noopener noreferrer"&gt;https://download.opensuse.org/repositories/security:netfilter/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 3: Install Docker Rootless
&lt;/h2&gt;

&lt;p&gt;Install the &lt;code&gt;docker-stable-rootless-extras&lt;/code&gt; package. This pulls in &lt;code&gt;docker-stable&lt;/code&gt; and all other required dependencies automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;zypper &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker-stable-rootless-extras
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What gets installed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docker-stable&lt;/code&gt; — The Docker daemon and CLI&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker-stable-rootless-extras&lt;/code&gt; — Rootless mode support files&lt;/li&gt;
&lt;li&gt;All required dependencies (containerd, runc, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Unlike Docker CE packages, the openSUSE &lt;code&gt;docker-stable&lt;/code&gt; package does &lt;strong&gt;not&lt;/strong&gt; include the Docker Compose plugin. We'll install that separately in the next step.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Install Docker Compose (Optional but Recommended)
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;docker-stable&lt;/code&gt; package doesn't include Docker Compose. Install it separately and register it as a CLI plugin to use the modern &lt;code&gt;docker compose&lt;/code&gt; command (v2 syntax).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install docker-compose:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;zypper &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker-compose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Register as a Docker CLI plugin:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.docker/cli-plugins
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-sf&lt;/span&gt; /usr/bin/docker-compose ~/.docker/cli-plugins/docker-compose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verify the plugin works:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the Docker Compose version (e.g., &lt;code&gt;Docker Compose version v2.x.x&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; The symlink makes &lt;code&gt;docker compose&lt;/code&gt; (without hyphen) available as a Docker CLI plugin. This is the modern v2 syntax used throughout this guide and in Docker Compose files.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Set Up Rootless Docker
&lt;/h2&gt;

&lt;p&gt;Here's where rootless mode actually gets enabled. From this point on, &lt;strong&gt;no &lt;code&gt;sudo&lt;/code&gt; is required&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;First, disable the system-wide Docker daemon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl disable &lt;span class="nt"&gt;--now&lt;/span&gt; docker.service docker.socket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now run the rootless setup script as your regular user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dockerd-rootless-setuptool.sh &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see output ending with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[INFO] Installed docker.service successfully.
[INFO] To control docker.service, run: `systemctl --user (start|stop|restart) docker.service`
[INFO] To run docker.service on system startup, run: `sudo loginctl enable-linger [username]`
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable your user's Docker service to start automatically on boot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable lingering so your user services start at boot even without a login session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;loginctl enable-linger &lt;span class="o"&gt;[&lt;/span&gt;username]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;[username]&lt;/code&gt; with your actual username.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;Here's how to confirm everything worked:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Switch to rootless context:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker context use rootless
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Test with a real container (jq demo):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of the usual &lt;code&gt;hello-world&lt;/code&gt;, let's verify with something useful. I've got a JSON file — &lt;code&gt;sample.json&lt;/code&gt;. Normally you'd need to install &lt;code&gt;jq&lt;/code&gt; to parse it. But with Docker, the tool comes with the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;sample.json | docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; stedolan/jq &lt;span class="s1"&gt;'.'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First time, you'll see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Unable to find image 'stedolan/jq:latest' locally
Downloaded newer image for stedolan/jq:latest
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the output — beautifully formatted JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"David"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"company"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Transcend Solutions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DevOps Engineer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"skills"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Docker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Kubernetes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Linux"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Singapore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"experience_years"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No installation. No sudo. Same command on any system with Docker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confirm rootless mode:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker info 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"rootless"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rootless
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Check your context:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker context show
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rootless
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verify data directory:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker info 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Docker Root Dir"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rootless Docker stores everything under your home directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Docker Root Dir: /home/youruser/.local/share/docker
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Instead of &lt;code&gt;/var/lib/docker&lt;/code&gt; for system Docker)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;List running containers:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows an empty table (no containers running yet):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;CONTAINER   IMAGE   COMMAND   CREATED   STATUS   PORTS   NAMES
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verify Docker starts at boot:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see "active (running)" and "enabled".&lt;/p&gt;




&lt;h2&gt;
  
  
  Rootless Limitations to Know
&lt;/h2&gt;

&lt;p&gt;Running Docker in rootless mode has a few trade-offs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Limitation&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;th&gt;Workaround&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No ports below 1024&lt;/td&gt;
&lt;td&gt;Can't bind to ports 80, 443 directly&lt;/td&gt;
&lt;td&gt;Use a rootful reverse proxy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage in home directory&lt;/td&gt;
&lt;td&gt;Images/volumes use &lt;code&gt;~/.local/share/docker&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Ensure adequate home directory space&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No &lt;code&gt;ping&lt;/code&gt; from containers&lt;/td&gt;
&lt;td&gt;ICMP requires root privileges&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;curl&lt;/code&gt; or &lt;code&gt;wget&lt;/code&gt; for connectivity tests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are minor trade-offs for the significant security benefit of never running the Docker daemon as root.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Now that you have a secure rootless Docker environment on SLES, you're ready to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pull and run your first containers&lt;/li&gt;
&lt;li&gt;Learn about Docker volumes for persistent data&lt;/li&gt;
&lt;li&gt;Set up multi-container applications with Docker Compose&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prefer a different distro?&lt;/strong&gt; I also wrote the same guide using Ubuntu last week. &lt;/p&gt;

&lt;p&gt;For more deep dives on Docker, check out &lt;strong&gt;"Levelling Up with Docker"&lt;/strong&gt; — 14 chapters of practical guides covering volumes, networking, Compose, production deployments, and more.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found this helpful? Share it with someone who's learning Docker!&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;SEO Metadata:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Title:&lt;/strong&gt; How to Install Docker Rootless on SLES 15/16 (2026 Guide)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta Description:&lt;/strong&gt; Install Docker in rootless mode on SUSE Linux Enterprise Server (SLES) 15 or 16 using openSUSE repositories. No sudo required. Complete step-by-step guide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target Keywords:&lt;/strong&gt; docker rootless sles, docker-stable-rootless-extras, install docker suse linux enterprise, opensuse docker repository sles, docker rootless sles 15, docker rootless sles 16&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Word Count:&lt;/strong&gt; ~1,200&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>sles</category>
      <category>linux</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
