<?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>Forem: David Tio</title>
    <description>The latest articles on Forem by David Tio (@davidtio).</description>
    <link>https://forem.com/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>Forem: David Tio</title>
      <link>https://forem.com/davidtio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/davidtio"/>
    <language>en</language>
    <item>
      <title>Boot in Seconds: Cloud Images + cloud-init in Podman</title>
      <dc:creator>David Tio</dc:creator>
      <pubDate>Wed, 15 Apr 2026 02:58:08 +0000</pubDate>
      <link>https://forem.com/davidtio/boot-in-seconds-cloud-images-cloud-init-in-podman-2ki0</link>
      <guid>https://forem.com/davidtio/boot-in-seconds-cloud-images-cloud-init-in-podman-2ki0</guid>
      <description>&lt;h1&gt;
  
  
  Boot in Seconds: Cloud Images + cloud-init
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Quick one-liner:&lt;/strong&gt; Skip the installer entirely — download a pre-built cloud image, seed it with &lt;code&gt;cloud-init&lt;/code&gt;, and boot a fully configured VM in seconds.&lt;/p&gt;




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

&lt;p&gt;Post #3 proved persistence works, but that interactive Alpine install took time. Download ISO, boot, run installer, answer prompts, wait, configure. Repeat that for every new VM and you'll spend more time installing than actually using them.&lt;/p&gt;

&lt;p&gt;Cloud images solve this. They're pre-built disk images with an OS already installed. Ubuntu, Fedora, Debian, Rocky — they all publish ready-to-boot images. You download one, tell &lt;code&gt;cloud-init&lt;/code&gt; your SSH key and username, and boot. No installer, no prompts, no waiting.&lt;/p&gt;

&lt;p&gt;This post swaps the manual install for a cloud image. You'll download an Ubuntu cloud image, create a &lt;code&gt;cloud-init&lt;/code&gt; seed, and boot straight into a configured VM.&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;
&lt;code&gt;~/vm&lt;/code&gt; directory from Post #3&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;genisoimage&lt;/code&gt; installed on your host (provides the &lt;code&gt;mkisofs&lt;/code&gt; command)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📥 Step 1: Download a Cloud Image
&lt;/h2&gt;

&lt;p&gt;Ubuntu publishes cloud images for every release. Grab the latest LTS (24.04 Noble Numbat):&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; ~/vm
&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lh&lt;/span&gt; noble-server-cloudimg-amd64.img
&lt;span class="nt"&gt;-rw-rw-r--&lt;/span&gt; 1 user user 650M Apr  1 12:00 noble-server-cloudimg-amd64.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That 650 MB file is a complete Ubuntu 24.04 installation, ready to boot. No install needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  🗺️ Where to Find Cloud Images
&lt;/h3&gt;

&lt;p&gt;Most major Linux distributions publish cloud images. Here's where to get them:&lt;/p&gt;

&lt;h4&gt;
  
  
  Open Source Distributions
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Distribution&lt;/th&gt;
&lt;th&gt;Cloud Image Repository&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ubuntu&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://cloud-images.ubuntu.com/" rel="noopener noreferrer"&gt;https://cloud-images.ubuntu.com/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Debian&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://cloud.debian.org/images/" rel="noopener noreferrer"&gt;https://cloud.debian.org/images/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fedora&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://download.fedoraproject.org/pub/fedora/linux/releases/" rel="noopener noreferrer"&gt;https://download.fedoraproject.org/pub/fedora/linux/releases/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CentOS Stream&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://cloud.centos.org/centos/" rel="noopener noreferrer"&gt;https://cloud.centos.org/centos/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rocky Linux&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://download.rockylinux.org/pub/rocky/9/images/x86_64/" rel="noopener noreferrer"&gt;https://download.rockylinux.org/pub/rocky/9/images/x86_64/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AlmaLinux&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://repo.almalinux.org/almalinux/9/cloud/x86_64/images/" rel="noopener noreferrer"&gt;https://repo.almalinux.org/almalinux/9/cloud/x86_64/images/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;openSUSE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://download.opensuse.org/tumbleweed/appliances/" rel="noopener noreferrer"&gt;https://download.opensuse.org/tumbleweed/appliances/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Arch Linux&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://geo.mirror.pkgbuild.com/images/" rel="noopener noreferrer"&gt;https://geo.mirror.pkgbuild.com/images/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Enterprise Distributions
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Distribution&lt;/th&gt;
&lt;th&gt;Cloud Image Repository&lt;/th&gt;
&lt;th&gt;Access&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RHEL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://access.redhat.com/downloads/content/rhel" rel="noopener noreferrer"&gt;https://access.redhat.com/downloads/content/rhel&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Subscription (free developer tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SLES&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://download.suse.com/" rel="noopener noreferrer"&gt;https://download.suse.com/&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Account required (free trial available)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Oracle Linux&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://yum.oracle.com/oracle-linux-templates.html" rel="noopener noreferrer"&gt;https://yum.oracle.com/oracle-linux-templates.html&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Amazon Linux&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/linux/al2023/ug/outside-ec2-download.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/linux/al2023/ug/outside-ec2-download.html&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Format tips:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Look for &lt;code&gt;qcow2&lt;/code&gt; format — it's thin-provisioned and works best with QEMU/KVM&lt;/li&gt;
&lt;li&gt;Some sites offer raw images (&lt;code&gt;.img&lt;/code&gt;) — these work too but take more disk space&lt;/li&gt;
&lt;li&gt;Avoid VMDK or VDI formats — those are for VMware and VirtualBox&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Alpine Linux offers cloud images but uses dynamic URLs based on provider, architecture, and firmware options. Visit &lt;a href="https://alpinelinux.org/cloud/" rel="noopener noreferrer"&gt;https://alpinelinux.org/cloud/&lt;/a&gt; to generate the correct download link.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ Step 2: Create a cloud-init Seed
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cloud-init&lt;/code&gt; is the standard for first-boot VM configuration. It runs on the first boot, reads a small YAML file, and sets up users, SSH keys, hostnames, packages — whatever you tell it to.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔑 Get Your SSH Public Key
&lt;/h3&gt;

&lt;p&gt;Cloud images don't have passwords by default — they use SSH key authentication. You'll need a key pair to log in.&lt;/p&gt;

&lt;p&gt;Check if you already have one:&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; ~/.ssh/id_ed25519.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see a file, skip ahead — you're set. If not, generate one. &lt;strong&gt;ed25519&lt;/strong&gt; is the modern standard: faster, smaller, and more secure than RSA:&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;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"your-email@example.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press Enter to accept the default location (&lt;code&gt;~/.ssh/id_ed25519&lt;/code&gt;). Optionally set a passphrase for extra security.&lt;/p&gt;

&lt;p&gt;Then grab your public key:&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; ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...your-key-here... your-email@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the entire line — you'll paste it into the &lt;code&gt;user-data&lt;/code&gt; file below.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔒 Creating a Hashed Password
&lt;/h3&gt;

&lt;p&gt;Cloud images accept passwords in two formats: plain text (risky) or hashed (secure). We'll use a SHA-512 hash. Use &lt;code&gt;read -s&lt;/code&gt; to enter your password without it appearing in shell history, then pipe it to &lt;code&gt;openssl&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;read&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Password: "&lt;/span&gt; PW &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; openssl passwd &lt;span class="nt"&gt;-6&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PW&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;unset &lt;/span&gt;PW
&lt;span class="nv"&gt;$6$randomsalt$hashedpasswordstring&lt;/span&gt;...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-6&lt;/code&gt; flag means SHA-512. The output starts with &lt;code&gt;$6$&lt;/code&gt; — that's the identifier. Copy the entire string.&lt;/p&gt;

&lt;h3&gt;
  
  
  📝 Build the user-data File
&lt;/h3&gt;

&lt;p&gt;Create the &lt;code&gt;user-data&lt;/code&gt; file for Ubuntu:&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; noble
&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; noble/user-data &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;'
#cloud-config
preserve_hostname: false
hostname: kvmpodman
users:
  - name: sysadmin
    groups: sudo
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...your-key-here...
    lock_passwd: false
    passwd: '&lt;/span&gt;&lt;span class="nv"&gt;$6$randomsalt$your&lt;/span&gt;&lt;span class="sh"&gt;-hashed-password...'
runcmd:
  - echo "cloud-init completed" &amp;gt; /home/sysadmin/.cloud-init-done
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the SSH key with your own (&lt;code&gt;cat ~/.ssh/id_ed25519.pub&lt;/code&gt;) and the &lt;code&gt;passwd&lt;/code&gt; hash with the one you generated above.&lt;/p&gt;

&lt;p&gt;This seed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sets the hostname to &lt;code&gt;kvmpodman&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Creates a user named &lt;code&gt;sysadmin&lt;/code&gt; with sudo access&lt;/li&gt;
&lt;li&gt;Enables password login (for console testing)&lt;/li&gt;
&lt;li&gt;Leaves a marker file when cloud-init finishes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📄 Create meta-data
&lt;/h3&gt;

&lt;p&gt;Create an empty &lt;code&gt;meta-data&lt;/code&gt; file (required, but can be empty):&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;touch &lt;/span&gt;noble/meta-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💿 Step 3: Build the cloud-init ISO
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cloud-init&lt;/code&gt; reads its config from a CD-ROM attached to the VM. Use &lt;code&gt;mkisofs&lt;/code&gt; to create a small ISO containing your seed files. Run this on your host, from &lt;code&gt;~/vm&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;mkisofs &lt;span class="nt"&gt;-J&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="nt"&gt;-input-charset&lt;/span&gt; utf-8 &lt;span class="nt"&gt;-V&lt;/span&gt; cidata &lt;span class="nt"&gt;-o&lt;/span&gt; cloud-init-noble.iso noble/user-data noble/meta-data
&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;Total translation table size: 0
Total rockridge attributes bytes: 331
Total directory bytes: 0
Path table size(bytes): 10
Max brk space used 0
182 extents written (0 MB)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-J&lt;/code&gt; flag enables Joliet extensions, &lt;code&gt;-R&lt;/code&gt; adds Rock Ridge (Unix file permissions — this fixes the warning), and &lt;code&gt;-V cidata&lt;/code&gt; sets the volume label to &lt;code&gt;cidata&lt;/code&gt; — which is what &lt;code&gt;cloud-init&lt;/code&gt; scans for on boot.&lt;/p&gt;

&lt;p&gt;That small ISO contains your entire first-boot configuration.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Step 4: Boot the Cloud Image
&lt;/h2&gt;

&lt;p&gt;Attach both the cloud image disk and the cloud-init ISO. The cloud image boots as normal, &lt;code&gt;cloud-init&lt;/code&gt; detects the CD-ROM, and applies your seed on first boot:&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; 1024 &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/noble-server-cloudimg-amd64.img,format&lt;span class="o"&gt;=&lt;/span&gt;qcow2 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-cdrom&lt;/span&gt; /vm/cloud-init-noble.iso &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-boot&lt;/span&gt; c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The VM boots. Wait about 15-30 seconds for &lt;code&gt;cloud-init&lt;/code&gt; to run. You'll see output 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;cloud-init 25.3-0ubuntu1~24.04.1 running 'modules:config' at Sat, 12 Apr 2026 12:34:56 +0000
cloud-init 25.3-0ubuntu1~24.04.1 running 'modules:final' at Sat, 12 Apr 2026 12:34:58 +0000
cloud-init 25.3-0ubuntu1~24.04.1 finished at Sat, 12 Apr 2026 12:35:02 +0000
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you see &lt;code&gt;finished&lt;/code&gt;, the VM is ready. Log in with the username and password you set:&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;kvmpodman login: sysadmin
Password:
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then shut down cleanly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysadmin@kvmpodman:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;poweroff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container will exit automatically when the VM powers off, and the disk image persists.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Step 5: Verify cloud-init Ran
&lt;/h2&gt;

&lt;p&gt;Boot again, this time without the cloud-init ISO (it's only needed on first boot):&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; 1024 &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/noble-server-cloudimg-amd64.img,format&lt;span class="o"&gt;=&lt;/span&gt;qcow2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log in with &lt;code&gt;sysadmin&lt;/code&gt; and the password you set. Then verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysadmin@kvmpodman:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;su -
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; password &lt;span class="k"&gt;for &lt;/span&gt;sysadmin: 
root@kvmpodman:~#
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the disk layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@kvmpodman:~# lsblk
NAME    MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
sda       8:0    0  3.5G  0 disk 
├─sda1    8:1    0  2.5G  0 part /
├─sda14   8:14   0    4M  0 part 
├─sda15   8:15   0  106M  0 part /boot/efi
└─sda16 259:0    0  913M  0 part /boot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cloud image is thin-provisioned — the disk is 3.5 GB total, with a 2.5 GB root partition, 913 MB for &lt;code&gt;/boot&lt;/code&gt;, and 106 MB for EFI. The 4 MB &lt;code&gt;sda14&lt;/code&gt; is a BIOS boot partition (GRUB uses it on non-EFI boots). No swap is configured by default.&lt;/p&gt;

&lt;p&gt;Memory-wise, this VM was given 1 GB (&lt;code&gt;-m 1024&lt;/code&gt;), and Ubuntu reports about 961 MB available after kernel reservations.&lt;/p&gt;

&lt;p&gt;Confirm &lt;code&gt;cloud-init&lt;/code&gt; ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@kvmpodman:~# cloud-init status
status: &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then 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;root@kvmpodman:~# poweroff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container exits automatically when the VM shuts down, and the disk image persists.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Step 6: Mini Shootout — SLES 16 and Amazon Linux 2023
&lt;/h2&gt;

&lt;p&gt;Cloud images skip the installer entirely, and &lt;code&gt;cloud-init&lt;/code&gt; automates the rest of the setup. A working VM boots in seconds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Time to Working VM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Manual install (Post #3)&lt;/td&gt;
&lt;td&gt;5-10 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud image + cloud-init (IDE/SATA)&lt;/td&gt;
&lt;td&gt;~25s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud image + cloud-init (VirtIO)&lt;/td&gt;
&lt;td&gt;~10s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ubuntu is the easy path. With a working VM in about 10 seconds, we have plenty of time to spin up different distributions and see how they compare. Let's try two I find interesting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SLES 16&lt;/strong&gt; — enterprise Linux that rarely gets covered in tutorials. Most blog posts stop at RHEL or CentOS. SLES is what shops with a SUSE subscription actually run, and it's almost nowhere to be found in container or KVM content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Amazon Linux 2023&lt;/strong&gt; — I know it exists, but I've never used it outside an EC2 instance. It's built exclusively for AWS, so booting it as a native KVM VM on my own hardware is something I've been curious about.&lt;/p&gt;

&lt;p&gt;We'll download both, build cloud-init seeds for each, and boot with VirtIO.&lt;/p&gt;

&lt;h3&gt;
  
  
  📥 Download the Images
&lt;/h3&gt;

&lt;p&gt;Head to the links in the table above and grab the qcow2 images for your distro:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SLES 16&lt;/strong&gt;: &lt;a href="https://download.suse.com/" rel="noopener noreferrer"&gt;SUSE Download Center&lt;/a&gt; — requires a free SUSE account. Look for &lt;code&gt;SLES-16.0-Minimal-VM.x86_64-Cloud-GM.qcow2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Linux 2023&lt;/strong&gt;: &lt;a href="https://docs.aws.amazon.com/linux/al2023/ug/outside-ec2-download.html" rel="noopener noreferrer"&gt;AWS Documentation&lt;/a&gt; — free, no account needed. Look for &lt;code&gt;al2023-kvm-2023.10.20260325.0-kernel-6.1-x86_64.xfs.gpt.qcow2&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drop both files into &lt;code&gt;~/vm/&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ⚙️ Build cloud-init Seeds
&lt;/h3&gt;

&lt;p&gt;Create a directory for each distro:&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; sles16 al2023
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;SLES 16&lt;/strong&gt; — uses &lt;code&gt;wheel&lt;/code&gt; group for sudo. SLES comments out &lt;code&gt;%wheel&lt;/code&gt; in &lt;code&gt;/etc/sudoers&lt;/code&gt; by default, so we need to uncomment 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;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; sles16/user-data &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;'
#cloud-config
preserve_hostname: false
hostname: kvmpodman
users:
  - name: sysadmin
    groups: wheel
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...your-key-here...
    lock_passwd: false
    passwd: '&lt;/span&gt;&lt;span class="nv"&gt;$6$randomsalt$your&lt;/span&gt;&lt;span class="sh"&gt;-hashed-password...'
runcmd:
  - sed -i 's/^#&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sh"&gt;*%wheel&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sh"&gt;*ALL=(ALL)&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sh"&gt;*ALL&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sh"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;$/&lt;/span&gt;&lt;span class="sh"&gt;%wheel ALL=(ALL) ALL/' /etc/sudoers
  - echo "cloud-init completed" &amp;gt; /home/sysadmin/.cloud-init-done
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;touch &lt;/span&gt;sles16/meta-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Amazon Linux 2023&lt;/strong&gt; — also uses &lt;code&gt;wheel&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; al2023/user-data &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;'
#cloud-config
preserve_hostname: false
hostname: kvmpodman
users:
  - name: sysadmin
    groups: wheel
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...your-key-here...
    lock_passwd: false
    passwd: '&lt;/span&gt;&lt;span class="nv"&gt;$6$randomsalt$your&lt;/span&gt;&lt;span class="sh"&gt;-hashed-password...'
runcmd:
  - echo "cloud-init completed" &amp;gt; /home/sysadmin/.cloud-init-done
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;touch &lt;/span&gt;al2023/meta-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SLES seed uses &lt;code&gt;wheel&lt;/code&gt; and uncommented &lt;code&gt;%wheel&lt;/code&gt; in &lt;code&gt;/etc/sudoers&lt;/code&gt;. Amazon Linux also uses &lt;code&gt;wheel&lt;/code&gt; but doesn't need the sed hack. The &lt;code&gt;runcmd&lt;/code&gt; just leaves a marker file.&lt;/p&gt;

&lt;p&gt;Build the ISOs on your host, from &lt;code&gt;~/vm&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;mkisofs &lt;span class="nt"&gt;-J&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="nt"&gt;-input-charset&lt;/span&gt; utf-8 &lt;span class="nt"&gt;-V&lt;/span&gt; cidata &lt;span class="nt"&gt;-o&lt;/span&gt; cloud-init-sles.iso sles16/user-data sles16/meta-data
&lt;span class="nv"&gt;$ &lt;/span&gt;mkisofs &lt;span class="nt"&gt;-J&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="nt"&gt;-input-charset&lt;/span&gt; utf-8 &lt;span class="nt"&gt;-V&lt;/span&gt; cidata &lt;span class="nt"&gt;-o&lt;/span&gt; cloud-init-al2023.iso al2023/user-data al2023/meta-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🚀 Boot
&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;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; 1024 &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/SLES-16.0-Minimal-VM.x86_64-Cloud-GM.qcow2,format&lt;span class="o"&gt;=&lt;/span&gt;qcow2,if&lt;span class="o"&gt;=&lt;/span&gt;virtio &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-cdrom&lt;/span&gt; /vm/cloud-init-sles.iso &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-boot&lt;/span&gt; c
&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;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; 1024 &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/al2023-kvm-2023.10.20260325.0-kernel-6.1-x86_64.xfs.gpt.qcow2,format&lt;span class="o"&gt;=&lt;/span&gt;qcow2,if&lt;span class="o"&gt;=&lt;/span&gt;virtio &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-cdrom&lt;/span&gt; /vm/cloud-init-al2023.iso &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-boot&lt;/span&gt; c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first boot with the cloud-init ISO configures the VM. After that, drop the &lt;code&gt;-cdrom&lt;/code&gt; flag — it's only needed once:&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; 1024 &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/SLES-16.0-Minimal-VM.x86_64-Cloud-GM.qcow2,format&lt;span class="o"&gt;=&lt;/span&gt;qcow2,if&lt;span class="o"&gt;=&lt;/span&gt;virtio
&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;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; 1024 &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/al2023-kvm-2023.10.20260325.0-kernel-6.1-x86_64.xfs.gpt.qcow2,format&lt;span class="o"&gt;=&lt;/span&gt;qcow2,if&lt;span class="o"&gt;=&lt;/span&gt;virtio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  📊 Results
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Distribution&lt;/th&gt;
&lt;th&gt;Disk Size&lt;/th&gt;
&lt;th&gt;Boot Time&lt;/th&gt;
&lt;th&gt;Root Usage&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ubuntu 24.04&lt;/td&gt;
&lt;td&gt;3.5 GB&lt;/td&gt;
&lt;td&gt;~10s&lt;/td&gt;
&lt;td&gt;72%&lt;/td&gt;
&lt;td&gt;Lightweight server, ext4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLES 16&lt;/td&gt;
&lt;td&gt;1.3 GB&lt;/td&gt;
&lt;td&gt;~10s&lt;/td&gt;
&lt;td&gt;97%&lt;/td&gt;
&lt;td&gt;Minimal install, xfs, very tight on disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon Linux 2023&lt;/td&gt;
&lt;td&gt;25 GB&lt;/td&gt;
&lt;td&gt;~20s&lt;/td&gt;
&lt;td&gt;7%&lt;/td&gt;
&lt;td&gt;Cloud-optimized, xfs, plenty of room&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  🦎 SLES 16 — What's Different
&lt;/h3&gt;

&lt;p&gt;SLES boots fine with its own cloud-init seed. The &lt;code&gt;wheel&lt;/code&gt; group works for sudo. Boot time:&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;real    0m9.855s
user    0m0.085s
sys     0m0.055s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;About 10 seconds — same ballpark as Ubuntu. QEMU defaults to the right boot device when there's only one disk, so &lt;code&gt;-boot c&lt;/code&gt; isn't needed for subsequent boots.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysadmin@kvmpodman:~&amp;gt; cloud-init &lt;span class="nt"&gt;--version&lt;/span&gt;
/usr/bin/cloud-init 25.1.3-160000.1.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The disk layout tells a different story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysadmin@kvmpodman:~&amp;gt; lsblk
NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
sr0     11:0    1  364K  0 rom  
vda    253:0    0  1.3G  0 disk 
├─vda1 253:1    0    2M  0 part 
├─vda2 253:2    0  512M  0 part /boot/efi
└─vda3 253:3    0  806M  0 part /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three partitions instead of four — no separate &lt;code&gt;/boot&lt;/code&gt;, just EFI and root. The 2 MB &lt;code&gt;vda1&lt;/code&gt; is the BIOS boot partition. The root filesystem is xfs, not ext4.&lt;/p&gt;

&lt;p&gt;And the disk is already 97% full:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysadmin@kvmpodman:~&amp;gt; &lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-Th&lt;/span&gt;
Filesystem     Type      Size  Used Avail Use% Mounted on
/dev/vda3      xfs       742M  716M   27M  97% /
/dev/vda2      vfat      512M  3.9M  508M   1% /boot/efi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;27 MB of free space on root is tight. Installing a single package with &lt;code&gt;zypper&lt;/code&gt; will likely fill the remaining space. Ubuntu gave you 688 MB free on a 3.5 GB image. SLES is a true minimal image — but that means almost no headroom.&lt;/p&gt;

&lt;p&gt;Memory-wise, both Ubuntu and SLES report about 960 MB from the 1 GB allocation — no surprise there.&lt;/p&gt;

&lt;h3&gt;
  
  
  ☁️ Amazon Linux 2023 — What's Different
&lt;/h3&gt;

&lt;p&gt;Amazon Linux takes a completely different approach. It ships a 25 GB image with only 7% used:&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="o"&gt;[&lt;/span&gt;sysadmin@localhost ~]&lt;span class="nv"&gt;$ &lt;/span&gt;lsblk
NAME     MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
sr0       11:0    1  364K  0 rom  
vda      252:0    0   25G  0 disk 
├─vda1   252:1    0   25G  0 part /
├─vda127 259:0    0    1M  0 part 
└─vda128 259:1    0   10M  0 part /boot/efi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three partitions — a single massive 25 GB root partition, a 1 MB BIOS boot partition, and a tiny 10 MB EFI partition. No separate &lt;code&gt;/boot&lt;/code&gt;. The filesystem is xfs, same as SLES.&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="o"&gt;[&lt;/span&gt;sysadmin@localhost ~]&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;
Filesystem     Type      Size  Used Avail Use% Mounted on
/dev/vda1      xfs        25G  1.7G   24G   7% /
/dev/vda128    vfat       10M  1.3M  8.7M  13% /boot/efi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;24 GB of free space out of the box. This image is built for production use, not minimal testing. You can install packages, run services, and never worry about disk space.&lt;/p&gt;

&lt;p&gt;Memory usage is similar — 964 MB total, 317 MB used:&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="o"&gt;[&lt;/span&gt;sysadmin@localhost ~]&lt;span class="nv"&gt;$ &lt;/span&gt;free &lt;span class="nt"&gt;-m&lt;/span&gt;
               total        used        free      shared  buff/cache   available
Mem:             964         317         433           2         213         508
Swap:              0           0           0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cloud-init version:&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="o"&gt;[&lt;/span&gt;sysadmin@localhost ~]&lt;span class="nv"&gt;$ &lt;/span&gt;cloud-init &lt;span class="nt"&gt;--version&lt;/span&gt;
/usr/bin/cloud-init 22.2.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One oddity: the hostname shows as &lt;code&gt;localhost&lt;/code&gt; instead of &lt;code&gt;kvmpodman&lt;/code&gt;. Amazon Linux ships with cloud-init 22.2.2, which is significantly older than Ubuntu's 25.3 or SLES's 25.1. The &lt;code&gt;preserve_hostname: false&lt;/code&gt; directive may not be honored properly in this older version, or Amazon's own hostname logic overrides it during boot. Fix it 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="o"&gt;[&lt;/span&gt;sysadmin@localhost ~]&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;hostnamectl set-hostname kvmpodman
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boot time:&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;real    0m19.998s
user    0m0.061s
sys     0m0.089s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;About 20 seconds — the slowest of the three. Ubuntu and SLES both hit ~10 seconds. Amazon's extra startup time likely comes from its larger disk image, older cloud-init version, and the init services it brings up by default. Still, 20 seconds is nothing for a full production-grade Linux install.&lt;/p&gt;

&lt;p&gt;The real kicker: this is an image designed exclusively for AWS EC2, and it boots natively as a KVM VM inside a Podman container. No AWS APIs, no special tooling — just qcow2 and VirtIO. It works.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔍 Distro Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Ubuntu 24.04&lt;/th&gt;
&lt;th&gt;SLES 16&lt;/th&gt;
&lt;th&gt;Amazon Linux 2023&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Disk size&lt;/td&gt;
&lt;td&gt;3.5 GB&lt;/td&gt;
&lt;td&gt;1.3 GB&lt;/td&gt;
&lt;td&gt;25 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Root usage&lt;/td&gt;
&lt;td&gt;72%&lt;/td&gt;
&lt;td&gt;97%&lt;/td&gt;
&lt;td&gt;7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filesystem&lt;/td&gt;
&lt;td&gt;ext4&lt;/td&gt;
&lt;td&gt;xfs&lt;/td&gt;
&lt;td&gt;xfs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boot time&lt;/td&gt;
&lt;td&gt;~10s&lt;/td&gt;
&lt;td&gt;~10s&lt;/td&gt;
&lt;td&gt;~20s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cloud-init&lt;/td&gt;
&lt;td&gt;25.3&lt;/td&gt;
&lt;td&gt;25.1&lt;/td&gt;
&lt;td&gt;22.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partitions&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swap&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  ⚠️ Distro-Specific Gotchas
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;Uses &lt;code&gt;zypper&lt;/code&gt; instead of &lt;code&gt;apt&lt;/code&gt;/&lt;code&gt;dnf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Root filesystem is xfs, not ext4&lt;/li&gt;
&lt;li&gt;Disk is extremely tight (97% used out of the box) — not ideal for installing additional packages&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;wheel&lt;/code&gt; group works for sudo access&lt;/li&gt;
&lt;li&gt;Older images may have cloud-init issues — grab the latest from the SUSE download center&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Amazon Linux 2023:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses &lt;code&gt;dnf&lt;/code&gt; like RHEL/CentOS&lt;/li&gt;
&lt;li&gt;Comes with cloud-init pre-installed (it's designed for AWS)&lt;/li&gt;
&lt;li&gt;May try to reach AWS metadata services on boot — harmless but adds a few seconds&lt;/li&gt;
&lt;li&gt;Default shell for root is &lt;code&gt;bash&lt;/code&gt;, but the &lt;code&gt;ec2-user&lt;/code&gt; default varies&lt;/li&gt;
&lt;/ul&gt;




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

&lt;ul&gt;
&lt;li&gt;✅ Ubuntu cloud image downloaded and ready to boot&lt;/li&gt;
&lt;li&gt;✅ &lt;code&gt;cloud-init&lt;/code&gt; seed with user, SSH key, and hashed password&lt;/li&gt;
&lt;li&gt;✅ VM boots in under 10 seconds with VirtIO, fully configured&lt;/li&gt;
&lt;li&gt;✅ No manual installer, no interactive prompts&lt;/li&gt;
&lt;li&gt;✅ SLES 16 and Amazon Linux 2023 running side by side&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;You can boot into the VM, but you're stuck in the console. Typing commands in the QEMU terminal is clunky — no copy/paste, no multiple windows, no real terminal features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Post #5:&lt;/strong&gt; We'll set up SSH networking so you can connect from your host terminal, use your favorite SSH client, and treat this VM like a real remote server.&lt;/p&gt;




&lt;p&gt;This guide is &lt;strong&gt;Part 4&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;Part 3:&lt;/strong&gt; &lt;a href="//KVM-03-PERSISTENT-DISK.md"&gt;Persistent VMs in Podman: Install Alpine to a qcow2 Disk Image&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Coming up in Part 5:&lt;/strong&gt; SSH Into Your Podman VM — Container Networking for KVM&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=Boot%20KVM%20VMs%20in%20seconds%20with%20cloud%20images!&amp;amp;url=https://blog.dtio.app/2026/04/boot-in-seconds-cloud-images-cloud-init.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>kvm</category>
      <category>podman</category>
      <category>cloudinit</category>
      <category>tutorial</category>
    </item>
    <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://forem.com/davidtio/docker-compose-explained-one-file-one-container-2026-38m6</link>
      <guid>https://forem.com/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://forem.com/davidtio/building-a-blog-platform-with-docker-2-tailwind-css-3be9</link>
      <guid>https://forem.com/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://forem.com/davidtio/docker-networking-explained-connect-your-containers-2026-2j83</link>
      <guid>https://forem.com/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://forem.com/davidtio/podman-on-sles-16-installation-storage-and-first-rootless-container-2026-guide-2mki</link>
      <guid>https://forem.com/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://forem.com/davidtio/persistent-vms-in-podman-install-alpine-to-a-qcow2-disk-image-go6</link>
      <guid>https://forem.com/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://forem.com/davidtio/kvm-acceleration-in-a-rootless-podman-container-before-and-after-hf4</link>
      <guid>https://forem.com/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://forem.com/davidtio/building-a-blog-platform-with-docker-1-flask-setup-5h10</link>
      <guid>https://forem.com/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://forem.com/davidtio/sles-16-add-a-dvd-as-a-local-zypper-repository-no-subscription-needed-fpp</link>
      <guid>https://forem.com/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://forem.com/davidtio/build-a-kvm-ready-container-image-from-scratch-2c6g</link>
      <guid>https://forem.com/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://forem.com/davidtio/docker-environment-management-images-logs-and-cleanup-2026-guide-2hba</link>
      <guid>https://forem.com/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://forem.com/davidtio/run-your-first-docker-container-2026-step-by-step-14ab</link>
      <guid>https://forem.com/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>
  </channel>
</rss>
