<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged containers</title>
<link>https://coderlegion.com/tag/containers</link>
<description>Powered by Question2Answer</description>
<item>
<title>You patched PHP. The old image is still in your cluster’s future.</title>
<link>https://coderlegion.com/32217/you-patched-php-the-old-image-is-still-in-your-clusters-future</link>
<description>&lt;hr&gt;
&lt;p&gt;title: &quot;You patched PHP. The old image is still in your cluster's future.&quot;&lt;br&gt;
published: false&lt;br&gt;
description: &quot;PHP2Shell (CVE-2026-109618, CVE-2026-109619) as seen from a Kubernetes cluster: how fast the scanners caught up, where they stay blind, and why rebuilding the image is only half of remediation.&quot;&lt;/p&gt;
&lt;h3 id=&quot;tags-kubernetes-security-php-devops&quot;&gt;tags: kubernetes, security, php, devops&lt;/h3&gt;
&lt;p&gt;On 21 October PHP shipped fixes for two critical bugs in the engine itself, CVE-2026-109618 and CVE-2026-109619, both CVSS 9.2, both reachable without authentication, both already being exploited by the time most people read the advisory. The community name is PHP2Shell. Fixed versions are 8.5.12, 8.4.27, 8.3.36 and 8.2.35. CISA and the UK NCSC both published &quot;patch now&quot; notices the same day.&lt;/p&gt;
&lt;p&gt;I am not going to explain the bugs; Wiz, watchTowr and Qualys have done that well. I want to write about what happened in Kubernetes teams in the forty-eight hours after, because I watched it happen and because the second half of it is the part nobody schedules.&lt;/p&gt;
&lt;h3 id=&quot;how-most-teams-found-out&quot;&gt;How most teams found out&lt;/h3&gt;
&lt;p&gt;From the news. Then someone opened a spreadsheet and typed &quot;PHP&quot; into the first column.&lt;/p&gt;
&lt;p&gt;This is not a criticism. It is what a platform team does when the only inventory of what runs in production is the memory of the people who deployed it. The questions that followed were always the same three:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Which clusters run PHP at all?&lt;/li&gt;
&lt;li&gt;Which images, which versions?&lt;/li&gt;
&lt;li&gt;Who gets to say &quot;we are done&quot;?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The third question is the one that comes back in March, when an auditor asks what you knew on 22 October and what you did about it.&lt;/p&gt;
&lt;h3 id=&quot;how-fast-the-scanners-caught-up&quot;&gt;How fast the scanners caught up&lt;/h3&gt;
&lt;p&gt;I measured this on 23 October, two days after the advisory, with Trivy 0.72.0 and whatever vulnerability database it pulled that afternoon. You can repeat every step.&lt;/p&gt;
&lt;p&gt;The database build was stamped 13:13 UTC on 23 October. Both CVEs were in it, with the Debian and Alpine advisories attached:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ grep -a -o CVE-2026-109618 ~/.cache/trivy/db/trivy.db | wc -l
1247
$ grep -a -o CVE-2026-109619 ~/.cache/trivy/db/trivy.db | wc -l
1253
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Debian's security tracker already listed &lt;code&gt;php8.2&lt;/code&gt; as resolved in &lt;code&gt;8.2.35-1~deb12u1&lt;/code&gt; for bookworm and &lt;code&gt;php8.4&lt;/code&gt; in &lt;code&gt;8.4.27-1~deb13u1&lt;/code&gt; for trixie. Alpine's secdb for 3.22 carried both CVEs. A &lt;code&gt;debian:bookworm-slim&lt;/code&gt; image with &lt;code&gt;php8.2-cli&lt;/code&gt; installed from the archive that day came out as 8.2.35, already fixed; a &lt;code&gt;alpine:3.22&lt;/code&gt; image with &lt;code&gt;php84&lt;/code&gt; came out as 8.4.27, also fixed.&lt;/p&gt;
&lt;p&gt;So the honest answer to &quot;did the open-source scanners keep up&quot; is yes, within two days, for PHP installed as a distribution package. That is good news and it is the ordinary case for a lot of internal images.&lt;/p&gt;
&lt;h3 id=&quot;where-the-scanners-stay-blind&quot;&gt;Where the scanners stay blind&lt;/h3&gt;
&lt;p&gt;Now the uncomfortable part.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ trivy image --scanners vuln --severity CRITICAL,HIGH php:8.3-fpm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;126 CRITICAL and HIGH findings. Not one of them about PHP.&lt;/p&gt;
&lt;p&gt;The official &lt;code&gt;php:*&lt;/code&gt; images build PHP from source. There is no &lt;code&gt;php8.3&lt;/code&gt; package in the image for a package scanner to look at, so Trivy, and every scanner that works from the package database, has nothing to match the advisory against. The 126 findings are the Debian base. The thing you actually care about is invisible.&lt;/p&gt;
&lt;p&gt;This is not a Trivy bug. It is what package-based scanning is. But it means that if your PHP workloads run on the official image, or on any image that compiles PHP, then a quiet scanner is not a clean bill. It is silence.&lt;/p&gt;
&lt;p&gt;What you can do about it this week, in order of effort:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ask the binary. &lt;code&gt;docker run --rm &amp;lt;your-image&amp;gt; php -v&lt;/code&gt; tells you the truth in one line. Do it for every PHP image tag you run, not the ones you think you run.&lt;/li&gt;
&lt;li&gt;Rebuild on a base that was pushed after 21 October, pin the digest, and write the digest down somewhere an auditor can read.&lt;/li&gt;
&lt;li&gt;If you can, move to distribution packages for the runtime. It costs you a newer PHP, it buys you a scanner that can see it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;patching-is-half-the-job&quot;&gt;Patching is half the job&lt;/h3&gt;
&lt;p&gt;Here is the part that gets skipped. You rebuild the image, you roll the Deployment, the pods come up on 8.3.36, everyone exhales.&lt;/p&gt;
&lt;p&gt;The old image did not go anywhere. It is still in the registry. It is still in &lt;code&gt;helm history&lt;/code&gt;. It is the image someone will roll back to at 03:00 when the new build has a problem that has nothing to do with PHP. It is in the CI cache, in the staging cluster nobody looks at, in the Job that runs once a month.&lt;/p&gt;
&lt;p&gt;Remediation has two halves:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run the fixed image.&lt;/li&gt;
&lt;li&gt;Make it impossible for the vulnerable image to run again, and keep a record that proves both.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Most teams do the first half and call it done. The second half is what an admission controller is for.&lt;/p&gt;
&lt;h3 id=&quot;what-this-looks-like-with-an-admission-controller-in-the-path&quot;&gt;What this looks like with an admission controller in the path&lt;/h3&gt;
&lt;p&gt;This is where I talk about what we built, so take the next paragraphs for what they are. Attestkeep is an admission controller for Kubernetes that scans, verifies signatures, decides at admission, and keeps the record. Here is exactly what it did, and did not do, for PHP2Shell.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The database refreshes itself.&lt;/strong&gt; The scanner pulls a fresh vulnerability database every six hours. Trivy publishes one every six hours too, so the lag between an advisory landing upstream and the cluster knowing about it is measured in hours, not in someone's calendar.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Every admitted image is rescanned.&lt;/strong&gt; Once a day by default (&lt;code&gt;scan.rescanInterval&lt;/code&gt; in the chart, if you want it shorter), every image the cluster has ever admitted is scanned again against the current database. An image that was clean on Monday and carries a CVE on Thursday shows up as not compliant on Thursday, with the CVE named. You do not open a spreadsheet; the inventory is the admission ledger.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It tells you.&lt;/strong&gt; A scan that finds critical vulnerabilities raises a &lt;code&gt;critical_finding&lt;/code&gt; event, which goes to whichever channel you configured: email, Slack, Teams, a webhook, Jira, GitLab. When a running image flips from &quot;would be admitted&quot; to &quot;would be denied&quot;, that is a second, separate message: &quot;Running image now fails policy&quot;. That is the message an operator actually needs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;New pods with the image are refused.&lt;/strong&gt; If your policy blocks critical findings, the vulnerable image cannot be admitted again. The rollback at 03:00 fails with a reason, the Job that runs monthly fails with a reason, the staging cluster with the same policy refuses it too. The old image is still in the registry. It is no longer in your cluster's future.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Running pods are not killed.&lt;/strong&gt; This is deliberate. Attestkeep does not evict a workload because a database update changed its verdict; that is an operator's decision, with the blast radius in front of them. What it does is make sure the next decision, any next decision, goes the right way, and that the running population is reconciled against the ledger on a schedule so drift is visible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If the exploit lands first, there is a second line.&lt;/strong&gt; PHP2Shell ends in a shell. If Falco is connected, a shell spawned inside a container is a runtime event, and a runtime policy can turn that event into a notification, an issue, or stopping the pod. That is not prevention, it is containment, and it is worth having when the window between advisory and rebuild is days.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Every decision is signed.&lt;/strong&gt; The admission ledger records the image digest, the policy revision that ran, the verdict and the reason. When the auditor asks in March what you knew on 22 October, the answer is a record with a signature, not a recollection.&lt;/p&gt;
&lt;p&gt;And the blind spot above applies to us exactly as it applies to Trivy, because Trivy is the scanner inside. If you run the official &lt;code&gt;php:*&lt;/code&gt; image, Attestkeep's rescan will not name CVE-2026-109618 either. It will show you the digest you admitted, when, and under which policy. The &lt;code&gt;php -v&lt;/code&gt; step is still yours. I would rather tell you that here than have you find it out from a pentest report.&lt;/p&gt;
&lt;h3 id=&quot;the-checklist-tool-agnostic&quot;&gt;The checklist, tool-agnostic&lt;/h3&gt;
&lt;p&gt;For this week, whatever you run:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;List every image whose name or Dockerfile mentions PHP. Not from memory; from the cluster.&lt;/li&gt;
&lt;li&gt;For each, run &lt;code&gt;php -v&lt;/code&gt; inside the image. Trust that over the scanner.&lt;/li&gt;
&lt;li&gt;Rebuild on a post-21-October base, pin digests.&lt;/li&gt;
&lt;li&gt;Add a rule that refuses the old digests at admission, so a rollback cannot resurrect them.&lt;/li&gt;
&lt;li&gt;Write down what you found, what you changed, and when, in a form that survives until the audit.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If step 4 is a policy in an admission controller and step 5 is produced automatically, this is a Tuesday. If step 4 is a Slack message and step 5 is a Confluence page, it is a project, and it will be a project again at the next CVE.&lt;/p&gt;
&lt;h3 id=&quot;closing&quot;&gt;Closing&lt;/h3&gt;
&lt;p&gt;The lesson of PHP2Shell for platform teams is not &quot;patch faster&quot;. Everyone patched. The lesson is that the news is a bad way to learn what is in your cluster, and that a patched Deployment with the old image still admissible is not a remediated cluster.&lt;/p&gt;
&lt;p&gt;Let the cluster do the checking. Keep the record. Make the old image a thing that cannot come back.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Sources: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.cisa.gov/news-events/alerts/2026/10/21/critical-vulnerabilities-php-require-immediate-patching&quot;&gt;CISA alert, 21 October 2026&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.ncsc.gov.uk/news/critical-security-issue-affecting-php&quot;&gt;NCSC advisory&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://watchtowr.com/intelligence/php-cve-2026-109618-faq/&quot;&gt;watchTowr FAQ&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.qualys.com/vulnerabilities-threat-research/2026/10/20/php-announces-critical-security-release-for-two-vulnerabilities&quot;&gt;Qualys&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://cloud.google.com/blog/products/identity-security/responding-to-core-php-vulnerabilities-cve-2026-109618-cve-2026-109619&quot;&gt;Google Cloud response&lt;/a&gt;. Scanner measurements are mine, 23 October 2026, Trivy 0.72.0; the commands are in the text.&lt;/p&gt;
&lt;p&gt;Attestkeep runs inside the cluster it protects; every security feature is in the free edition. &lt;a rel=&quot;nofollow&quot; href=&quot;https://attestkeep.com&quot;&gt;attestkeep.com&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.attestkeep.com&quot;&gt;docs.attestkeep.com&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32217/you-patched-php-the-old-image-is-still-in-your-clusters-future</guid>
<pubDate>Fri, 23 Oct 2026 17:56:15 +0000</pubDate>
</item>
<item>
<title>How and Why I Built My Own bootc-Based Linux Distribution</title>
<link>https://coderlegion.com/27857/how-and-why-i-built-my-own-bootc-based-linux-distribution</link>
<description>&lt;p&gt;What an adventure it has been 🤠&lt;/p&gt;
&lt;p&gt;It all started with yet another attempt to configure NixOS as a replacement for Debian, which I use every day.&lt;/p&gt;
&lt;p&gt;I really like the idea behind NixOS: declarative configuration of the entire system and the ability to reproduce an environment both sound great. Unfortunately, in practice, it turned out to be much harder than I had expected. I find the Nix language completely unintuitive, the documentation can be maddening at times, and conflicting advice on best practices is scattered all over the internet. Or maybe I am just not smart enough to figure it out? Hard to say XD. In any case, after what was probably my third failed attempt to configure NixOS, I started looking for an alternative. That was when I discovered bootc.&lt;/p&gt;
&lt;p&gt;As soon as I started reading the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bootc.dev/bootc/&quot;&gt;bootc documentation&lt;/a&gt;, I was blown away. An entire operating system described in a &lt;code&gt;Containerfile&lt;/code&gt; and built like a regular container image, with updates and rollbacks powered by OSTree. Beautiful. Of course, this is not NixOS with different syntax, and it does not promise exactly the same things. But it gave me what mattered most: I could keep the recipe for my system in a repository and use it to build subsequent versions without manually recreating everything after every installation.&lt;/p&gt;
&lt;p&gt;I quickly got to work configuring my dream distro. I am a minimalist by nature, so the feature-packed images prepared by &lt;a rel=&quot;nofollow&quot; href=&quot;https://universal-blue.org/&quot;&gt;Universal Blue&lt;/a&gt; were not an option. I wanted to decide for myself what went into the system. I chose the official &lt;code&gt;quay.io/fedora/fedora-bootc&lt;/code&gt; image, which provides a Fedora base prepared to run through bootc: the kernel, essential system tools, and the image update mechanism. It is not a ready-made desktop loaded with applications, so I could add everything else myself. Fedora maintains the base, and for each rebuild I can use its current version instead of assembling the system from scratch by hand.&lt;/p&gt;
&lt;p&gt;Of course, I am not crazy enough to switch immediately to a new system before it is ready for my day-to-day work.&lt;/p&gt;
&lt;p&gt;At first, I decided to build and run SpaceOS in a virtual machine. I built the image with Podman, converted it to a &lt;code&gt;qcow2&lt;/code&gt; disk with &lt;code&gt;bootc-image-builder&lt;/code&gt;, and then ran it in QEMU. This let me prepare the basic configuration and check whether the system booted at all. It soon became clear, however, that both building a new VM disk after every change and working inside the VM to run &lt;code&gt;bootc switch&lt;/code&gt; were rather slow and frustrating. A VM also cannot tell me everything about my computer: it is not an easy way to test my Wi-Fi adapter, docking station, or DisplayLink-connected monitors.&lt;/p&gt;
&lt;p&gt;So I decided to install the new system alongside Debian on the physical drive.&lt;/p&gt;
&lt;p&gt;That was where things became a little tricky. I am somewhat paranoid and like to keep my data encrypted with LUKS. At first, I tried installing the system through Anaconda, but my partition layout made the process increasingly unpleasant. I also had the idea of letting Debian and SpaceOS share a single GRUB installation. I managed to get the system to boot, but after rebuilding the SpaceOS image, Debian's GRUB entry did not keep up with the new OSTree deployments. As a result, rollback became inconvenient even though it was supposed to be one of the greatest advantages of the whole exercise.&lt;/p&gt;
&lt;p&gt;In the end, I put the two systems on separate LVM logical volumes inside the same LUKS container. Each system got its own &lt;code&gt;/boot&lt;/code&gt; and &lt;code&gt;/boot/efi&lt;/code&gt; partitions outside the encrypted container and its own UEFI boot entry. This allows bootc to manage its own GRUB installation and the entries for successive system versions, while at startup I choose either Debian or SpaceOS in UEFI. They share data encryption, not the boot mechanism.&lt;/p&gt;
&lt;h3 id=&quot;how-i-installed-it&quot;&gt;How I Installed It&lt;/h3&gt;
&lt;p&gt;If you are mainly interested in the story of the project, feel free to skip this section. What follows describes the layout I used on my machine, not a universal recipe for every drive.&lt;/p&gt;
&lt;p&gt;Partitioning operations are inherently dangerous, so I recommend making a backup first. Unless you enjoy an adrenaline rush and are not afraid of losing files 😉&lt;/p&gt;
&lt;p&gt;My drive is laid out like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;p1  Debian /boot/efi       FAT32
p2  Debian /boot           ext4
p3  shared LUKS container
    └── LVM volume group
        ├── Debian /        ext4
        ├── swap
        └── SpaceOS /       ext4
p4  SpaceOS /boot          ext4
p5  SpaceOS /boot/efi      FAT32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, I made room at the end of the drive. I booted from a SystemRescue USB drive so I would not have to shrink the filesystem from which Debian was currently running. After unlocking LUKS and activating LVM, I checked Debian's unmounted filesystem and shrank it, then adjusted the sizes of the LVM and LUKS partitions. This part must be adapted to your own layout: order matters, and copying someone else's sector numbers from the internet is a bad idea, even if they belong to a very likable blog author. Before making any changes, I inspected &lt;code&gt;lsblk&lt;/code&gt;, &lt;code&gt;pvs&lt;/code&gt;, &lt;code&gt;vgs&lt;/code&gt;, &lt;code&gt;lvs&lt;/code&gt;, and the GPT partition table. After every operation, I made sure Debian still booted correctly.&lt;/p&gt;
&lt;p&gt;From the space I had freed, I created two small partitions outside LUKS: about 1 GiB for &lt;code&gt;/boot&lt;/code&gt; and about 1 GiB for the SpaceOS EFI partition. I added the remaining space back to the LUKS partition, expanded the LVM physical volume inside it using &lt;code&gt;pvresize&lt;/code&gt;, and created a new logical volume named &lt;code&gt;spaceos&lt;/code&gt;. This gave me around 58 GiB for the system. I formatted that volume and &lt;code&gt;/boot&lt;/code&gt; as &lt;code&gt;ext4&lt;/code&gt;, and the EFI partition as FAT32. This way, the SpaceOS root filesystem is encrypted together with Debian, while both systems have independent boot files.&lt;/p&gt;
&lt;p&gt;After expanding and unlocking the LUKS partition, the part inside LVM looked roughly like this on my machine:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo pvresize /dev/mapper/nvme0n1p3_crypt
sudo lvcreate -L 58G -n spaceos ton618-vg
sudo mkfs.ext4 -L SPACEOS_ROOT /dev/ton618-vg/spaceos
sudo mkfs.ext4 -L SPACEOS_BOOT /dev/nvme0n1p4
sudo mkfs.fat -F32 -n SPACEOS_EFI /dev/nvme0n1p5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These are the device names from my computer. The three formatting commands in particular will erase the contents of the filesystems they target, so check that the partition numbers are correct before using them.&lt;/p&gt;
&lt;p&gt;Next, I mounted the future root filesystem at &lt;code&gt;/mnt/spaceos&lt;/code&gt;, the boot partition at &lt;code&gt;/mnt/spaceos/boot&lt;/code&gt;, and the EFI partition at &lt;code&gt;/mnt/spaceos/boot/efi&lt;/code&gt;. It is worth running &lt;code&gt;findmnt&lt;/code&gt; for all three paths at this point. If any of them points to the wrong filesystem, this is definitely not the time to launch the installer.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo mkdir -p /mnt/spaceos
sudo mount /dev/ton618-vg/spaceos /mnt/spaceos
sudo mkdir -p /mnt/spaceos/boot
sudo mount /dev/nvme0n1p4 /mnt/spaceos/boot
sudo mkdir -p /mnt/spaceos/boot/efi
sudo mount /dev/nvme0n1p5 /mnt/spaceos/boot/efi
findmnt /mnt/spaceos
findmnt /mnt/spaceos/boot
findmnt /mnt/spaceos/boot/efi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I build the image locally with &lt;code&gt;podman build&lt;/code&gt;. I do not publish it to a registry yet, so I installed from the local &lt;code&gt;localhost/spaceos:latest&lt;/code&gt; image by running &lt;code&gt;bootc install to-filesystem&lt;/code&gt; from within it. This method assumes that the filesystems have already been prepared and mounted; bootc should not have to guess how I wanted to partition the drive. I provided the UUIDs for &lt;code&gt;/&lt;/code&gt; and &lt;code&gt;/boot&lt;/code&gt;, and passed the UUID of the LUKS container and the name of the LVM volume as kernel arguments. Without the latter two, the system would not know what to unlock during startup. My script looks like this, except that I have replaced my own identifiers with placeholders for yours:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/usr/bin/env bash
set -euo pipefail

# Use blkid to find the filesystem UUIDs and cryptsetup luksUUID to find
# the LUKS container UUID. Set LVM_LV to the group and volume names.
ROOT_UUID=&quot;ENTER_ROOT_FILESYSTEM_UUID&quot;
BOOT_UUID=&quot;ENTER_BOOT_PARTITION_UUID&quot;
LUKS_UUID=&quot;ENTER_LUKS_CONTAINER_UUID&quot;
LVM_LV=&quot;ENTER_GROUP_NAME/VOLUME_NAME&quot;

for value in &quot;$ROOT_UUID&quot; &quot;$BOOT_UUID&quot; &quot;$LUKS_UUID&quot; &quot;$LVM_LV&quot;; do
  if [[ &quot;$value&quot; == ENTER_* ]]; then
    echo &quot;Fill in the UUIDs and the LVM volume name first.&quot; &amp;gt;&amp;amp;2
    exit 1
  fi
done

sudo podman run --rm --privileged \
  --pid=host \
  --ipc=host \
  --security-opt label=type:unconfined_t \
  -v /dev:/dev \
  -v /var/lib/containers:/var/lib/containers \
  -v /mnt/spaceos:/target \
  localhost/spaceos:latest \
  bootc install to-filesystem \
    --bootloader=grub \
    --root-mount-spec=&quot;UUID=${ROOT_UUID}&quot; \
    --boot-mount-spec=&quot;UUID=${BOOT_UUID}&quot; \
    --karg=&quot;rd.luks.uuid=${LUKS_UUID}&quot; \
    --karg=&quot;rd.lvm.lv=${LVM_LV}&quot; \
    /target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before running it, replace the placeholder values, verify that the filesystems are mounted under &lt;code&gt;/mnt/spaceos&lt;/code&gt;, and make sure the image is available to the instance of Podman launched through &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There was one more catch, this time caused by a bootc bug. With two EFI partitions on the same drive, &lt;code&gt;bootc install to-filesystem&lt;/code&gt; could select the first one, belonging to Debian, even though I had mounted the SpaceOS EFI partition under the target directory. &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/bootc-dev/bootc/issues/1929&quot;&gt;The issue has also been reported in the bootc repository&lt;/a&gt;. For the duration of the installation, I therefore unmounted Debian's EFI partition and temporarily changed its &lt;strong&gt;GPT type&lt;/strong&gt; from “EFI System” to a regular Linux partition. I did not format it or remove any files. After the installation, I restored the correct type and mounted it again. This works around a specific partition-selection problem, but it is also a step where confusing the partition numbers can cause serious trouble.&lt;/p&gt;
&lt;p&gt;Finally, I used &lt;code&gt;efibootmgr -v&lt;/code&gt; to verify that the SpaceOS entry actually pointed to its own EFI partition. Only then did I decide I could breathe again. After my earlier adventures with GRUB, I preferred evidence over a good feeling.&lt;/p&gt;
&lt;h3 id=&quot;developing-the-system-from-within&quot;&gt;Developing the System from Within&lt;/h3&gt;
&lt;p&gt;This was when the fun part of the whole operation began. With SpaceOS correctly installed alongside my main system, I could develop it from within SpaceOS itself. I edit the &lt;code&gt;Containerfile&lt;/code&gt;, build a new image locally, and switch the system to it with &lt;code&gt;bootc switch --transport containers-storage localhost/spaceos:latest&lt;/code&gt;. After a restart, the new deployment boots; if I break something, I can return to the previous one. This is exactly the kind of workflow that made me interested in bootc.&lt;/p&gt;
&lt;p&gt;Installing software and preparing configuration here resembles building a regular container. I currently use Fedora 44 as the base, and some of the main programs included in the system are Sway, Fish, Alacritty, and Neovim. You can find the full and up-to-date list of software in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/SpaceShaman/spaceos&quot;&gt;SpaceOS repository&lt;/a&gt;. The project is still evolving, so its contents may change over time.&lt;/p&gt;
&lt;p&gt;I also came up with the idea of copying the repository to &lt;code&gt;/etc/spaceos&lt;/code&gt; during the build and creating symbolic links to its configuration files. This lets me edit the configuration in one place and immediately see the changes in programs that read it dynamically. Moreover, uncommitted local changes under &lt;code&gt;/etc&lt;/code&gt; do not disappear simply because the system switches to a new image. This does not mean bootc transfers my files into the image, however: Podman uses the current contents of the project directory during the build, while OSTree separately carries local &lt;code&gt;/etc&lt;/code&gt; changes over to the new version during deployment. If I edit something on the running system, I need to remember whether I want to keep it locally only or commit it to the repository as well.&lt;/p&gt;
&lt;p&gt;That distinction matters. &lt;code&gt;/usr&lt;/code&gt; and most of the system come from the image and are replaced with new contents when the version changes. &lt;code&gt;/etc&lt;/code&gt; remains writable by default and persists across updates, with local changes taken into account. Symbolic links to &lt;code&gt;/etc/spaceos&lt;/code&gt; therefore make sense for my experiment, although they require some care: if I modify a file locally, a new version of the same file from the image may not overwrite my edit. &lt;code&gt;/var&lt;/code&gt;, meanwhile, holds data that should survive both system switches and rollbacks. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://bootc.dev/bootc/filesystem.html&quot;&gt;bootc documentation&lt;/a&gt; explains these rules in much greater detail.&lt;/p&gt;
&lt;h3 id=&quot;displaylink-because-of-course-it-couldn-t-be-that-easy&quot;&gt;DisplayLink, Because of Course It Couldn't Be That Easy&lt;/h3&gt;
&lt;p&gt;The DisplayLink drivers for my docking station proved to be one of the bigger problems. Adding an RPM package to the image looked innocent enough. But then I still had to build the EVDI module for the exact kernel included in the image, start the user-space component, and get Sway to cooperate with the additional displays. My first attempts produced the classic “almost works” symptom: the image built successfully, but after login Sway immediately kicked me back to &lt;code&gt;greetd&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;At first, I tried installing an unofficial DisplayLink RPM and building EVDI with DKMS while assembling the image. The module build itself could finish successfully, which unfortunately did not mean that the graphical session would start on the physical machine. On top of that, &lt;code&gt;bootc container lint&lt;/code&gt; warned about files left behind by DKMS and other installation steps. So I began treating the module build and DisplayLink's operation on the hardware as separate problems.&lt;/p&gt;
&lt;p&gt;In the end, I studied how &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ublue-os/akmods&quot;&gt;Universal Blue&lt;/a&gt; uses &lt;code&gt;akmods&lt;/code&gt; and adapted the idea to my own &lt;code&gt;Containerfile&lt;/code&gt;, without switching to one of its ready-made images. In a separate build stage, I install the compilation tools and the &lt;code&gt;kernel-devel&lt;/code&gt; package matching the kernel from &lt;code&gt;fedora-bootc&lt;/code&gt;, build EVDI, verify that the module was actually produced, and copy the resulting package together with the user-space part of DisplayLink into the final image. This means the compilers and the entire build toolchain do not have to remain in the target system.&lt;/p&gt;
&lt;p&gt;I get the packages from the &lt;a rel=&quot;nofollow&quot; href=&quot;https://negativo17.org/&quot;&gt;Fedora Multimedia repository maintained by Negativo17&lt;/a&gt;, also known as Simone Caronni. If you use Fedora and have ever needed something outside its core repositories, you may already have encountered his work. In my case, the &lt;code&gt;displaylink&lt;/code&gt;, &lt;code&gt;libevdi&lt;/code&gt;, and &lt;code&gt;akmod-evdi&lt;/code&gt; packages available there saved me from a great deal of additional manual tinkering. A small nod of appreciation to him, because without people like that, our “it's just one driver” projects would be far less fun.&lt;/p&gt;
&lt;p&gt;The current &lt;code&gt;Containerfile&lt;/code&gt; builds EVDI in a separate stage and installs it in the SpaceOS image. I also had to change the command that launches Sway in &lt;code&gt;greetd&lt;/code&gt;, adding the &lt;code&gt;--unsupported-gpu&lt;/code&gt; flag. I tested the image on the physical computer, and this time DisplayLink works with the docking station while Sway no longer sends me back to &lt;code&gt;greetd&lt;/code&gt;. Finally 😄&lt;/p&gt;
&lt;h3 id=&quot;updates-managed-by-ci-cd&quot;&gt;Updates Managed by CI/CD&lt;/h3&gt;
&lt;p&gt;Once the system became suitable for everyday use, building images locally was no longer enough. It is convenient while working on the configuration, but it is hardly a sensible way to publish subsequent system versions. I also did not want to remember to rebuild SpaceOS manually every time Fedora updated the base image. It was time for GitHub Actions and GHCR.&lt;/p&gt;
&lt;p&gt;An official release now begins with a tag following the &lt;code&gt;v0.1.3&lt;/code&gt; pattern. The workflow validates its format, builds the SpaceOS image, generates &lt;code&gt;/etc/os-release&lt;/code&gt; with the correct version number, and publishes everything to GitHub Container Registry. The immutable version tag remains in the registry permanently, while the &lt;code&gt;stable&lt;/code&gt;, &lt;code&gt;latest&lt;/code&gt;, and &lt;code&gt;auto&lt;/code&gt; tags are moved to the newly released image. This means that someone following the &lt;code&gt;auto&lt;/code&gt; channel receives not only Fedora fixes, but new SpaceOS releases as well.&lt;/p&gt;
&lt;p&gt;Initially, the same tag was also supposed to trigger a build of the Anaconda installation ISO and attach it to a GitHub Release. The build itself worked, but the resulting file exceeded GitHub's 2 GiB limit for a single release asset. I could split an archive into several parts or find separate hosting for files that large, but that would be overengineering for a system built primarily for myself. In the end, the CI/CD pipeline publishes only container images to GHCR. Instead, the repository contains a script for building the ISO locally. It pulls the image from the &lt;code&gt;auto&lt;/code&gt; channel, embeds it in the installer, and generates a SHA-256 checksum. A system installed from this media follows &lt;code&gt;auto&lt;/code&gt; from the start, so it does not have to be switched manually to the update channel after installation.&lt;/p&gt;
&lt;p&gt;A second workflow handles exactly those changes. Once a week, it checks the digest of the &lt;code&gt;fedora-bootc:44&lt;/code&gt; image and compares it with the base used by the most recently published SpaceOS image. If Fedora has changed nothing, the action finishes without building anything. If the digest is new, the workflow checks out the latest release, rebuilds the image, and publishes it under a tag containing the SpaceOS version, the date, and part of the base digest. One example is &lt;code&gt;auto-v0.1.3-20260920-abcdef123456&lt;/code&gt;. The &lt;code&gt;auto&lt;/code&gt; tag is moved to that image, and I retain the five most recent automatic rebuilds in the registry. Images carrying release tags are not subject to this cleanup.&lt;/p&gt;
&lt;p&gt;The remaining question was what to do on the running system. bootc's default automatic update mechanism did not suit the way I work, because I do not want my computer deciding to restart at the least convenient moment. Instead, I added my own systemd timer. Every hour, it checks the update channel and, provided the connection is not metered, the battery is not too low, and the machine is not under heavy load, downloads and stages a new deployment in the background. It does not restart the computer. Waybar tells me that an update is waiting, and the new version starts only the next time I reboot the machine myself.&lt;/p&gt;
&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;
&lt;p&gt;SpaceOS began with the frustration of yet another attempt at NixOS and ended up as a system whose entire recipe I can keep in Git, test like a container image, and update without manually recreating my environment. I had to overcome many problems along the way, but in the end I succeeded. Those very problems ultimately gave me a much better understanding of bootc, OSTree, the process of building a custom operating system, and many other Linux-related topics.&lt;/p&gt;
&lt;p&gt;The project's code is available in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/SpaceShaman/spaceos&quot;&gt;SpaceOS repository on GitHub&lt;/a&gt;. Its README contains a more detailed description of the system itself, the installation process, published tags, update mechanism, included software, and keyboard shortcuts.&lt;/p&gt;
&lt;p&gt;I should emphasize that SpaceOS is a system built specifically for me, my hardware, and the way I work. I would not really recommend installing it directly as your own operating system. Instead, I encourage you to fork the repository and treat it as a starting point for building your own unique distribution. After all, the greatest advantage of this approach is precisely that the system can truly be yours.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27857/how-and-why-i-built-my-own-bootc-based-linux-distribution</guid>
<pubDate>Sun, 20 Sep 2026 20:34:21 +0000</pubDate>
</item>
<item>
<title>One File, Six Services: Running AI Agents With Docker Compose</title>
<link>https://coderlegion.com/25561/one-file-six-services-running-ai-agents-with-docker-compose</link>
<description>&lt;p&gt;Most of us here already reach for Compose the moment a project grows past a single process, and agent work hits that point faster than almost anything else. What follows is the shape that keeps showing up once an agent stops being a script and starts being a system.&lt;/p&gt;
&lt;h3 id=&quot;an-agent-is-never-one-process&quot;&gt;An Agent Is Never One Process&lt;/h3&gt;
&lt;p&gt;A working agent needs a runtime process, a model endpoint, a database for conversation history and agent state, and usually a vector store for retrieval. Add a queue once tasks run in the background, and something for traces once you need to know why a run went sideways. That is six services before anyone has written an interesting tool.&lt;/p&gt;
&lt;p&gt;Running those by hand means remembering startup order, wiring hostnames and ports, and keeping six sets of environment variables in sync. Compose turns all of it into one declarative file with &lt;code&gt;depends_on&lt;/code&gt; and health checks, so &lt;code&gt;docker compose up&lt;/code&gt; produces the same stack on a laptop and on a server, and &lt;code&gt;docker compose down&lt;/code&gt; leaves nothing behind.&lt;/p&gt;
&lt;p&gt;The dependency isolation matters more here than in most stacks. Agent code pins specific versions of Python, PyTorch, and CUDA, and those pins conflict constantly. Two containers can run CUDA 12.4 and CUDA 11.8 side by side because each one carries its own dependency tree.&lt;/p&gt;
&lt;h3 id=&quot;three-patterns-three-cost-profiles&quot;&gt;Three Patterns, Three Cost Profiles&lt;/h3&gt;
&lt;p&gt;The first pattern is API-backed. The agent container calls a hosted model, and the Compose file holds the runtime, a database, and whatever tools the agent needs. Minimal local resources, best available models, and a bill that scales linearly with usage.&lt;/p&gt;
&lt;p&gt;The second runs a model server such as Ollama or vLLM in the same stack. The server holds the model in GPU memory and exposes an OpenAI compatible endpoint on the internal Docker network, so the agent reaches it by service name and configuration stays trivial. Per-call cost goes to zero and nothing leaves your infrastructure, but you own the GPU memory budget, and a large model can want anywhere from 20 to 80 GB of VRAM depending on parameter count and quantization.&lt;/p&gt;
&lt;p&gt;The third is a hybrid that routes cheap high-volume calls to the local server and hard calls out to a frontier model. It is the pattern most teams land on eventually, and it is also the one that makes the routing logic worth testing.&lt;/p&gt;
&lt;h3 id=&quot;the-parts-that-bite-later&quot;&gt;The Parts That Bite Later&lt;/h3&gt;
&lt;p&gt;GPU access needs the device reservation in the Compose file and the container toolkit on the host, and it fails in a way that looks like a slow CPU run rather than an error. State needs named volumes, because a bind mount to a host path is the fastest way to lose a vector index during a rebuild. Service discovery is free inside the Compose network, which is exactly why it is worth being deliberate about which ports you publish to the host at all, since an unauthenticated model server on 0.0.0.0 is a real problem.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.autolearningagents.com/docker-ai-agents/&quot;&gt;This guide to deploying AI agents with Docker Compose&lt;/a&gt; covers each of those in depth, along with the compose file anatomy and the move from a development stack to a production one.&lt;/p&gt;
&lt;h3 id=&quot;where-compose-stops-being-the-answer&quot;&gt;Where Compose Stops Being The Answer&lt;/h3&gt;
&lt;p&gt;Compose is a single-host tool. When you need rolling updates across machines, autoscaling on queue depth, or real multi-tenancy, that is the Kubernetes conversation. Until then, a Compose file is the honest description of your architecture, and it is the artifact a new developer can read in two minutes and run in one command.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25561/one-file-six-services-running-ai-agents-with-docker-compose</guid>
<pubDate>Thu, 27 Aug 2026 23:49:58 +0000</pubDate>
</item>
<item>
<title>Understanding the vectors of container escapes</title>
<link>https://coderlegion.com/24733/understanding-the-vectors-of-container-escapes</link>
<description>&lt;p&gt;Container escapes typically occur when a container gains direct access to host resources due to excessive permissions or shared namespaces. I'm working on a few Github projects to explore these risky misconfigurations in reproducible PoCs. The 3 categories of risk I'm exploring that commonly lead to container escapes are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Dangerous volume mounts&lt;/li&gt;
&lt;li&gt;Linux capabilities&lt;/li&gt;
&lt;li&gt;Shared namespaces&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Dangerous volume mounting&lt;/strong&gt; (like &lt;code&gt;hostPath&lt;/code&gt;) is when a developer mounts a directory from the underlying host filesystems like &lt;code&gt;/&lt;/code&gt;, &lt;code&gt;/var/run/docker.sock&lt;/code&gt; or &lt;code&gt;/etc&lt;/code&gt; directly into a container. The risk here is if a container mounts the host root &lt;code&gt;/&lt;/code&gt; filesystem or sensitive sockets like any sort of container runtime socket, a process inside the container can now interact directly with the host OS or manage container runtimes, effectively bypassing container isolation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Github reference:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ndouglas-edera/hostpath-risk&quot;&gt;https://github.com/ndouglas-edera/hostpath-risk&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Linux capabilities&lt;/strong&gt; (like &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt; and &lt;code&gt;CAP_SYS_PTRACE&lt;/code&gt;) are granular administrative privileges assigned to processes within a container. The risk here is that capabilities like &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt; allow actions such as mounting filesystems, setting network configs, or even interacting with kernel interfaces, ultimately enabling container processes to break out of their Linux namespaces.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Github reference:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ndouglas-edera/CAP_SYS_ADMIN-Escape&quot;&gt;https://github.com/ndouglas-edera/CAP_SYS_ADMIN-Escape&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Shared namespaces&lt;/strong&gt; (like &lt;code&gt;hostPID&lt;/code&gt;, &lt;code&gt;hostNetwork&lt;/code&gt;, and &lt;code&gt;hostIPC&lt;/code&gt;) end up disabling the standard namespace separations between the container and the underlying node. It's ultimately risky because it allows containers to interact with host processes, inter-process communication channels, or raw network interfaces, breaking isolation between workloads on the same node.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Github reference:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ndouglas-edera/hostpid-risk&quot;&gt;https://github.com/ndouglas-edera/hostpid-risk&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-isolated-runtimes-can-mitigate-these-risks&quot;&gt;How isolated runtimes can mitigate these risks&lt;/h3&gt;
&lt;p&gt;The above scripts I provided on Github were purely to automate the demonstration of these existing insecure configurations within Kubernetes. Since standard containerised workloads rely on various shared host Linux kernel features, such as namespaces and cgroups, to enforce boundaries - if a container ends up gaining elevated privileges or access to host resource, it now operates directly on that shared kernel. Secure runtime solutions like Edera Zones, Kata Containers, and gVisor change this threat model by providing secure (MicroVM) sandboxing layers. Instead of sharing the host kernel, workloads run inside lightweight VMs with their own isolated kernel. Similarly, even if a process inside the sandbox achieves &lt;code&gt;privileged: true&lt;/code&gt; or mounts a volume, its actions are constrained to its own guest kernel. Escaping the container only grants access to the isolated microVM environment, preventing access to the host OS, host memory, or adjacent workloads on the node. Finally, direct access to host process IDs or raw host sockets is prevented because the guest kernel maintains an entirely separate execution environment from the host operating system. I'm going to continue fleshing-out these repos to show how these misconfigurations are common place, but also how to address them within a single-node  test sandbox like EderaOn.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24733/understanding-the-vectors-of-container-escapes</guid>
<pubDate>Sun, 16 Aug 2026 20:09:22 +0000</pubDate>
</item>
<item>
<title>Lateral credential harvesting via process namespace sharing</title>
<link>https://coderlegion.com/24641/lateral-credential-harvesting-via-process-namespace-sharing</link>
<description>&lt;p&gt;&lt;strong&gt;Github Source:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ndouglas-edera/hostpid-risk/tree/main&quot;&gt;https://github.com/ndouglas-edera/hostpid-risk/tree/main&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In a multi-tenant or shared Kubernetes node environment, containers are supposed to be isolated environments. However, this demo proves that if a malicious or compromised pod is deployed with host-level privileges, it can spy on neighbouring workloads running on the same node without needing a software vulnerability or zero-day exploit.&lt;/p&gt;
&lt;h3 id=&quot;example-script&quot;&gt;Example script&lt;/h3&gt;
&lt;p&gt;You can download the automation script from Github:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://raw.githubusercontent.com/ndouglas-edera/hostpid-risk/refs/heads/main/hostpid-risk.sh
chmod +x hostpid-risk.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you have a Kubernetes cluster already running (like on &lt;strong&gt;Docker Desktop&lt;/strong&gt; or &lt;strong&gt;kind&lt;/strong&gt;), you can run the below script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./hostpid-risk.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;what-is-the-security-misconfiguration-exactly-&quot;&gt;What is the security misconfiguration exactly?&lt;/h3&gt;
&lt;p&gt;The exploit relies on 2 specific fields in the &lt;code&gt;inspector-pod&lt;/code&gt; YAML manifest working together:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spec:
  hostPID: true            # &amp;lt;--- Misconfiguration #1: Breaks PID isolation
  containers:
  - name: inspector
    securityContext:
      privileged: true     # &amp;lt;--- Misconfiguration #2: Overrides Linux permissions
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;how-it-works-technically&quot;&gt;How it works technically&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Normally, Linux container runtimes place each pod in their own isolated Process ID (&lt;strong&gt;PID&lt;/strong&gt;) namespace. &lt;br&gt;
Setting &lt;code&gt;hostPID: true&lt;/code&gt; actually forces the pod to share the host node’s global PID namespace.&lt;/li&gt;
&lt;li&gt;Suddenly, &lt;code&gt;ps -ef&lt;/code&gt; inside the container displays every process running on the underlying node. &lt;br&gt;
This includes processes belonging to other pods (&lt;strong&gt;target-app&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;This would bypass Linux kernel profiles like &lt;code&gt;AppArmor&lt;/code&gt; and &lt;code&gt;SELinux&lt;/code&gt; - granting full capabilities (like &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;In Linux, the virtual &lt;code&gt;/proc&lt;/code&gt; filesystem exposes process metadata. The file &lt;code&gt;/proc/&amp;lt;PID&amp;gt;/environ&lt;/code&gt; holds the environment variables loaded into memory when that process was spawned. &lt;/li&gt;
&lt;li&gt;Because &lt;strong&gt;inspector-pod&lt;/strong&gt; has host-level permissions, it can read &lt;code&gt;/proc/30629/environ&lt;/code&gt; belonging to &lt;strong&gt;target-app&lt;/strong&gt; before extracting the &lt;code&gt;DB_PASSWORD=SuperSecretPassword123!&lt;/code&gt; creds.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=13729325988490525777&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;strategy-1---pss-in-kubernetes&quot;&gt;Strategy 1 - PSS in Kubernetes&lt;/h3&gt;
&lt;p&gt;Pod Security Standards (or &lt;strong&gt;PSS&lt;/strong&gt;, for short) is a built-in policy Kubernetes framework managed by the Pod Security Admission (&lt;strong&gt;PSA&lt;/strong&gt;) controller. It categorises security into 3 levels:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Privileged&lt;/strong&gt;: Unrestricted (Allowing &lt;code&gt;hostPID: true&lt;/code&gt; &amp;amp; &lt;code&gt;privileged: true&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Baseline&lt;/strong&gt;: Prevents known privilege escalations (Blocking &lt;code&gt;hostPID: true&lt;/code&gt;, host paths, and host networking)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Restricted&lt;/strong&gt;: Hardened best practices (Requires dropping capabilities, non-root users, and more..)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;So, how do PSS policies block the attack?&lt;/strong&gt;&lt;br&gt;
By either applying a &lt;strong&gt;Baseline&lt;/strong&gt; or &lt;strong&gt;Restricted&lt;/strong&gt; policy to the target namespace, Kubernetes will reject &lt;code&gt;inspector-pod&lt;/code&gt; at the API server before it can ever be scheduled. Here's an example of enforcing the &lt;strong&gt;baseline&lt;/strong&gt; profile on the &lt;strong&gt;default&lt;/strong&gt; namespace&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl label --overwrite ns default pod-security.kubernetes.io/enforce=baseline
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If an attacker tries to apply &lt;code&gt;inspector-pod.yaml&lt;/code&gt; now, the Kubernetes API server responds with an immediate error, similar to other admission control technologies like OPA:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Error from server (Forbidden): pods &quot;inspector-pod&quot; is forbidden: 
violates PodSecurity &quot;baseline:latest&quot;: hostPID (spec.hostPID=true), 
privileged (container &quot;inspector&quot; must not set securityContext.privileged=true)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;strategy-2---runtime-isolation&quot;&gt;Strategy 2 - Runtime Isolation&lt;/h3&gt;
&lt;p&gt;While PSS works at the API controlplane-level (by stopping bad YAML from being accepted), the ideal runtime isolation approach should be applied at the Kernel-level.&lt;/p&gt;
&lt;p&gt;For context, &lt;code&gt;hostPID: true&lt;/code&gt; and &lt;code&gt;privileged: true&lt;/code&gt; are commonly enabled purely by accident, oftentimes by developers while troubleshooting nodes, and through legacy Helm charts, or by third-party monitoring and observability agents (like eBPF probes, custom log forwarders, or node-exporters) that require node visibility. And when this happens in a multi-tenant cluster (that's shared across departments or clients), compromising a single pod with host privileges instantly compromises all secrets across all pods on that same node, rendering the entire namespace-level isolation useless.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;So, what if an attacker has cluster-admin privileges or bypasses PSS?&lt;/strong&gt;&lt;br&gt;
Standard Kubernetes containers share a single, common Linux kernel. If namespace boundaries break, everything on that kernel is exposed. In the case of Edera Zones (and microVM-based runtimes like Kata Containers or gVisor, more generally) they implement a defence-in-depth strategy by isolating all your workloads into distinct hypervisor boundaries or independent kernel spaces.&lt;/p&gt;
&lt;p&gt;Under gVisor, Kata Containers, or Edera Zones, even if a pod sets &lt;code&gt;hostPID: true&lt;/code&gt; and &lt;code&gt;privileged: true&lt;/code&gt;, hostPID only shares the PID namespace of the isolated guest kernel / microVM sandbox, not the underlying bare-metal host node. The malicious pod sees only itself and its sandbox. In this way, it can't see processes from neighbouring pods running in parallel sandboxes.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Note: I will continue to update the Github repository to better improve these security automation demos.&lt;/p&gt;
&lt;/blockquote&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24641/lateral-credential-harvesting-via-process-namespace-sharing</guid>
<pubDate>Fri, 14 Aug 2026 16:51:02 +0000</pubDate>
</item>
<item>
<title>Breakouts, Bots, and Bare Metal</title>
<link>https://coderlegion.com/23883/breakouts-bots-and-bare-metal</link>
<description>&lt;p&gt;For years, security teams viewed container breakout attacks as an operational nuisance. The kind of nuisance that was largely confined to misconfigured Kubernetes clusters, over-privileged dev environments, or cloud cryptojacking campaigns. The security industry recently witnessed how fast that assumption has broken down dramatically.&lt;/p&gt;
&lt;p&gt;From OpenAI disclosing that its frontier AI models &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.wired.com/story/openais-hacking-debacle-was-a-human-mistake/&quot;&gt;escaped their evaluation test environment&lt;/a&gt; and compromised external database infrastructure, to Anthropic admitting its Claude models &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.bbc.com/news/articles/cz7dl7w8y7po&quot;&gt;broke free of sandboxes&lt;/a&gt; across three separate target environments, escape-to-host is no longer a problem isolated to cloud-native infrastructure. It is the defining bottleneck of safe AI deployment.&lt;/p&gt;
&lt;p&gt;Understanding why these breakouts keep happening, and why traditional container sandboxing approaches have consistently failed, requires stepping back into MITRE ATT&amp;amp;CK technique &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://attack.mitre.org/techniques/T1611/&quot;&gt;T1611&lt;/a&gt;&lt;/strong&gt;, dissecting past isolation trade-offs, and examining why bare-metal Type-1 hypervisor architecture provides the isolation boundary modern workloads require.&lt;/p&gt;
&lt;h3 id=&quot;what-is-t1611-&quot;&gt;What is T1611?&lt;/h3&gt;
&lt;p&gt;At its core, the escape-to-host technique describes an adversary breaking out of a constrained environment (a container, sandbox, or a VM) to gain unauthorised access to the underlying host kernel, host filesystem, or hypervisor. We also hear the phrase “&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.sysdig.com/learn-cloud-native/how-to-detect-and-prevent-container-escapes&quot;&gt;container escape&lt;/a&gt;&lt;/strong&gt;” being used more specifically in Docker &amp;amp; Kubernetes.&lt;/p&gt;
&lt;p&gt;In modern Linux OS virtualisation, standard containers do not run on isolated hardware. Instead, technologies like Docker and Kubernetes mostly rely on the shared-kernel isolation mechanisms. This includes the below:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/&quot;&gt;namespaces&lt;/a&gt;&lt;/strong&gt; (PID, mount, network) to slice system visibility. &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.kernel.org/admin-guide/cgroup-v1/cgroups.html&quot;&gt;cgroups&lt;/a&gt;&lt;/strong&gt; to limit hardware resource consumption. &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Seccomp&quot;&gt;seccomp&lt;/a&gt;&lt;/strong&gt; &amp;amp; &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://earthly.dev/blog/intro-to-Linux-capabilities/&quot;&gt;capabilities&lt;/a&gt;&lt;/strong&gt; to drop dangerous syscalls.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While these controls partition resources efficiently, the host kernel remains a massive single point of failure. An adversary, or any sort of hyper-capable autonomous model executing untrusted code, can execute an escape via several proven vectors:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Vector &lt;/th&gt;&lt;th&gt;Description&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Privileged containers &lt;/td&gt;&lt;td&gt;Containers running with &lt;code&gt;--privileged&lt;/code&gt; or equivalent flags&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Host bind mounts &lt;/td&gt;&lt;td&gt;Mounting sensitive host paths (like &lt;code&gt;/proc&lt;/code&gt; or &lt;code&gt;/&lt;/code&gt;) can execute direct write operations to host processes or write malicious cron jobs.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Exposed container sockets &lt;/td&gt;&lt;td&gt;Mounting &lt;code&gt;/var/run/docker.sock&lt;/code&gt; into a pod grants full root privileges over the host machine via administrative container commands.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Kernel syscall exploitation &lt;/td&gt;&lt;td&gt;Abusing vulnerable or permissive syscalls (such as &lt;code&gt;unshare&lt;/code&gt;, &lt;code&gt;keyctl&lt;/code&gt;, or &lt;code&gt;copy_file_range&lt;/code&gt;) to trigger privilege escalation bugs in the shared Linux kernel.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Once an attacker breaks the boundary, the host level unlocks lateral movement across every neighbouring pod, secrets held in host memory, and potentially full cluster takeover. Prioritising these container escape vulnerabilities, like CVEs in general, is becoming harder and harder due to the so-called AI-driven &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.centerforcybersecuritypolicy.org/insights-and-research/not-all-mitigations-are-created-equal&quot;&gt;vulnpocolypse&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=593129499724646995&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;An overly-simple tool (hosted on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ndouglas-cloudsmith/Docker-Hub-Scanner/tree/main&quot;&gt;GitHub)&lt;/a&gt; I put together to prioritise CVEs in public DockerHub images&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;when-code-execution-becomes-autonomous&quot;&gt;When code execution becomes autonomous&lt;/h3&gt;
&lt;p&gt;So, why is T1611 suddenly dominating headlines outside the traditional DevOps circles? Isn’t it just another vulnerability that needs to be fixed? Well, yes. But it also differs drastically because of how autonomous AI agents rely heavily on, and will continue to abuse, arbitrary code execution behaviour.&lt;/p&gt;
&lt;p&gt;To solve complex engineering tasks, execute data science workflows, or evaluate security tools, modern AI agents must run dynamic code inside sandboxes. However, when these AI agents encounter unexpected 0 Days or any sort of deployment misconfigs, their single-minded goal priorities turn them into super-powerful, automated pentesters.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;During automated benchmark tests at OpenAI on unreleased models, safety refusals were relaxed to gauge offensive cyber capabilities. The models autonomously chained credentials and unpatched 0-day CVEs inside their testing environment, reached the internet, and managed to compromise live production servers on Hugging Face to pull down testing solutions. &lt;/li&gt;
&lt;li&gt;Likewise, Anthropic’s Claude models participated in CTF-style evaluation scenarios, and also managed to escape their intended test boundaries across 141,000 runs. The models executed unauthenticated SQL injections, created and published malicious packages to public PyPI software repos, and even extracted production database credentials.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These incidents underline a hard security truth that &lt;strong&gt;human error and software misconfigurations will always happen&lt;/strong&gt;. Whether via human mistake, prompt injection, or autonomous goal-seeking, relying on standard OS-level sandboxing means that a single hole in the app-layer or shared kernel cascades into total infrastructure breaches.&lt;/p&gt;
&lt;h3 id=&quot;m1048---application-isolation-and-sandboxing&quot;&gt;M1048 - Application isolation and sandboxing&lt;/h3&gt;
&lt;p&gt;To mitigate &lt;strong&gt;T1611&lt;/strong&gt;, it’s recommended to adopt the MITRE ATT&amp;amp;CK mitigation ID &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://attack.mitre.org/mitigations/M1048/&quot;&gt;M1048&lt;/a&gt;&lt;/strong&gt;. The cloud-native ecosystem has spent a decade or more experimenting with alternative containment boundaries. However, almost every historical attempt forced engineers into a &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://arxiv.org/abs/2501.04580&quot;&gt;goldilocks dilemma&lt;/a&gt;&lt;/strong&gt; whereby we have to make hard decisions about trading off security, speed, and/or DevEx in some cases.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Vector &lt;/th&gt;&lt;th&gt;Description &lt;/th&gt;&lt;th&gt; Description&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;OS Virtualisation &lt;/td&gt;&lt;td&gt;Docker / K8s &lt;/td&gt;&lt;td&gt;While fast and lightweight, sharing the host kernel creates a fragile security boundary with a massive attack surface.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Intercept syscalls &lt;/td&gt;&lt;td&gt;&lt;strong&gt;gVisor&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Re-implementing kernel calls in userland via a Go runtime wrapper introduces heavy syscall overhead and severe performance penalties on bulkier I/O operations.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Type-2 MicroVM &lt;/td&gt;&lt;td&gt;&lt;strong&gt;Kata&lt;/strong&gt; / &lt;strong&gt;F'crackr&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Boot times are slower, cloud providers often restrict nested KVM virtualisation extensions, and the KVM host kernel remains inside your Trusted Computing Base (TCB).&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Unikernel &lt;/td&gt;&lt;td&gt;&lt;strong&gt;IncludeOS&lt;/strong&gt;, &lt;strong&gt;OSV&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Custom compilation requirements introduce major dev friction and break compatibility with standard OCI container images and Kubernetes tooling.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;1. Userspace syscall emulation&lt;/strong&gt;&lt;br&gt;
gVisor is universally celebrated for making sandboxing accessible without forcing changes to how containers are deployed. Developed by Google and battle-tested in platforms like Google Cloud Run, Cloud Functions, and the GKE Sandbox, it provides an elegant way to execute untrusted code or multi-tenant workloads. By intercepting application system calls in userspace using a memory-safe Go kernel called the sentry, gVisor effectively removes direct exposure to the host Linux kernel and eliminates whole classes of privilege escalation attacks. However, because it re-implements the complex Linux API inside a user-space process, it inherently introduces performance friction. Workloads heavy on system calls, network operations, or disk I/O often experience noticeable latency and CPU overhead, while the sandbox still shares the host filesystem under the hood.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Type-2 MicroVMs &amp;amp; KVM-based wrappers&lt;/strong&gt;&lt;br&gt;
Type-2 microVM technologies represent a massive leap forward for multi-tenant isolation, serving as the trusted foundation for cloud-scale infrastructure like AWS Lambda and AWS Fargate. By leveraging KVM (Kernel-based VM), engines like Firecracker and Kata Containers achieve lightning-fast sub-second boot times and minimal memory footprints while granting each workload a dedicated guest kernel and hardware-level isolation. Despite these monumental achievements, their reliance on KVM means the host Linux operating system itself remains directly in the TCB. This means any vulnerability in the host kernel can still compromise the host machine. Additionally, running KVM-backed microVMs inside modern public cloud environments requires nested virtualisation, an extension that remains restricted, cost-prohibitive, or entirely unsupported across many standard cloud node pools.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Unikernels&lt;/strong&gt;&lt;br&gt;
Unikernels offer the theoretical gold standard of minimal attack surfaces and hyper-efficient resource utilisation. By compiling an app and only its exact system dependencies into a single, unified machine image that runs without any traditional OS layer, unikernels deliver near-instantaneous startup times, minuscule memory footprints, and virtually zero bloat for adversaries to exploit. This radically lightweight design has made them a popular research domain for high-performance and single-purpose computing. However, this minimalist paradigm comes at the cost of operational usability. Because unikernels demand customised build pipelines and specialised compilation targets, they inherently break compatibility with standard OCI container images, existing Dockerfile workflows, and native Kubernetes orchestration pipelines.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Hardware-backed Type-1 hypervisor isolation&lt;/strong&gt;&lt;br&gt;
To permanently resolve &lt;strong&gt;T1611&lt;/strong&gt; without sacrificing container performance or developer ergonomics, the shared host kernel must be removed from the TCB altogether. This is the paradigm pioneered by projects and platforms like &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.edera.dev/overview/&quot;&gt;Edera&lt;/a&gt;&lt;/strong&gt;, which pair an optimised VMM with a minimal Type-1 (bare-metal) Xen microkernel hypervisor.&lt;/p&gt;
&lt;p&gt;Unlike Type-2 hypervisors that run on top of millions of lines of standard Linux C code, a paravirtualised Type-1 design relies on a microkernel core written or wrapped in memory-safe languages like Rust. This eliminates entire classes of hypervisor memory-corruption vulnerabilities (such as buffer overflows and use-after-free bugs) while reducing the ring-0 code footprint by up to 90%. By using paravirtualisation hypercalls instead of heavy CPU hardware emulation, guest OS containers run with hypervisor awareness. This delivers near-native CPU performance without requiring nested virtualisation extensions, allowing it to run on standard cloud infrastructure. Crucially, modern implementations act as drop-in replacements for standard Kubernetes &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kubernetes.io/docs/concepts/containers/cri/&quot;&gt;CRI&lt;/a&gt;&lt;/strong&gt;s, preserving existing OCI container images, Helm charts, and &lt;code&gt;kubectl&lt;/code&gt; workflows while giving every pod hardware-enforced boundaries.&lt;/p&gt;
&lt;p&gt;And wherever there is a security breakthrough, there will always be some slight drawbacks. While runtime performance of the hardware-backed type-1 hypervisor isolation matches native containers within a few % points, spinning up a fresh hypervisor zone introduces a small startup penalty (somewhere below 1s). This is unnoticeable for long-running AI agents or database microservices, but may require optimisation for ultra-short-lived, serverless functions. Also, moving host nodes to bare-metal hypervisors alters node provisioning compared to standard Linux worker nodes running basic &lt;code&gt;containerd&lt;/code&gt;. This is a noticeable operational paradigm shift for DevOps teams.&lt;/p&gt;
&lt;h3 id=&quot;closing-the-door-on-sandbox-escapes&quot;&gt;Closing the door on sandbox escapes&lt;/h3&gt;
&lt;p&gt;As autonomous AI agents and multi-tenant cloud workloads scale, the boundary between untrusted application code and trusted host management will continue to blur. Relying on software-only or userspace Linux kernel controls to block sophisticated privilege escalation attempts is a losing battle. While all of the approaches mentioned above are fantastic for preventing escapes, there is always going to be some kind of drawback (either performance or compatibility) when we opt for a secure-by-default approach. I hope I made a clear case for pairing modern Type-1 bare-metal hypervisor isolation with the DevEx of native containers to neutralise MITRE ATT&amp;amp;CK’s technique ID &lt;strong&gt;T1611&lt;/strong&gt; at the architectural level. When another frontier lab’s agent or an actual malicious actor decides to break out of a container runtime or sandbox environment, they won't land on the host OS, they should ultimately land in an isolated hypervisor zone with nowhere left to go.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23883/breakouts-bots-and-bare-metal</guid>
<pubDate>Fri, 31 Jul 2026 14:24:01 +0000</pubDate>
</item>
<item>
<title>From Dockerfile to Docker Hub: A Complete Beginner's Guide to Docker </title>
<link>https://coderlegion.com/20281/from-dockerfile-to-docker-hub-a-complete-beginners-guide-to-docker</link>
<description>&lt;p&gt;Containerization has become a fundamental skill for DevOps Engineers, Cloud Engineers, and Software Developers.&lt;/p&gt;
&lt;p&gt;However, many beginners learn Docker commands without understanding the complete workflow behind them.&lt;/p&gt;
&lt;p&gt;Questions like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What exactly is a Docker Image?&lt;/li&gt;
&lt;li&gt;How does a Dockerfile become a running Container?&lt;/li&gt;
&lt;li&gt;Why do we push images to Docker Hub?&lt;/li&gt;
&lt;li&gt;How do organizations deploy the same application everywhere?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;are common when starting out.&lt;/p&gt;
&lt;p&gt;In this blog, we'll build a simple Node.js application, containerize it using Docker, run it locally, and finally push the image to Docker Hub.&lt;/p&gt;
&lt;p&gt;By the end of this guide, you'll understand the complete Docker workflow used in real-world DevOps environments.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;docker-workflow-overview&quot;&gt;Docker Workflow Overview&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    A[Application Code] --&amp;gt; B[Dockerfile]
    B --&amp;gt; C[Docker Build]
    C --&amp;gt; D[Docker Image]
    D --&amp;gt; E[Docker Run]
    E --&amp;gt; F[Docker Container]
    D --&amp;gt; G[Docker Push]
    G --&amp;gt; H[Docker Hub]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the same workflow followed by developers and DevOps teams worldwide.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-are-we-building-&quot;&gt;What Are We Building?&lt;/h2&gt;
&lt;p&gt;We'll create a simple Node.js application that returns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A welcome message&lt;/li&gt;
&lt;li&gt;Environment information&lt;/li&gt;
&lt;li&gt;Container hostname&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This helps us verify that our application is actually running inside a Docker container.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;project-structure&quot;&gt;Project Structure&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;.
├── Dockerfile
├── app.js
└── package.json
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-1-create-the-node-js-application&quot;&gt;Step 1: Create the Node.js Application&lt;/h2&gt;
&lt;h3 id=&quot;app-js&quot;&gt;app.js&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const express = require(&quot;express&quot;);
const app = express();

const PORT = process.env.PORT || 3000;
const ENV = process.env.ENV_VALUE || &quot;No env set&quot;;
const HOSTNAME = process.env.HOSTNAME || require(&quot;os&quot;).hostname();

app.get(&quot;/&quot;, (req, res) =&amp;gt; {
  res.json({
    message: &quot;Hello from Simple App (Node)&quot;,
    env: ENV,
    container: HOSTNAME
  });
});

app.listen(PORT, () =&amp;gt;
  console.log(`Node Hello listening on ${PORT}`)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;package-json&quot;&gt;package.json&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;name&quot;: &quot;hello-app-node&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;main&quot;: &quot;app.js&quot;,
  &quot;dependencies&quot;: {
    &quot;express&quot;: &quot;^5.2.1&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;application-architecture&quot;&gt;Application Architecture&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    User --&amp;gt; Browser
    Browser --&amp;gt; Container
    Container --&amp;gt; NodeJS
    NodeJS --&amp;gt; JSONResponse
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Whenever a user accesses the application, the request reaches the containerized Node.js application, which returns a JSON response.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-2-create-the-dockerfile&quot;&gt;Step 2: Create the Dockerfile&lt;/h2&gt;
&lt;p&gt;A Dockerfile is simply a blueprint that tells Docker how to build an image.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM node:24-slim

WORKDIR /app

COPY package*.json ./

RUN npm install --production

COPY . .

EXPOSE 3000

CMD [&quot;node&quot;, &quot;app.js&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;understanding-the-dockerfile&quot;&gt;Understanding the Dockerfile&lt;/h3&gt;
&lt;h4 id=&quot;base-image&quot;&gt;Base Image&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM node:24-slim
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We start with an official lightweight Node.js image from Docker Hub.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;working-directory&quot;&gt;Working Directory&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;WORKDIR /app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Creates and switches to the &lt;code&gt;/app&lt;/code&gt; directory inside the container.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;copy-dependency-files&quot;&gt;Copy Dependency Files&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;COPY package*.json ./
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Copies package files first.&lt;/p&gt;
&lt;p&gt;This is considered a Docker best practice because Docker can cache dependency layers and speed up future builds.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;install-dependencies&quot;&gt;Install Dependencies&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;RUN npm install --production
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Installs only production dependencies.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;copy-application-source-code&quot;&gt;Copy Application Source Code&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;COPY . .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Copies the entire project into the container.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;expose-application-port&quot;&gt;Expose Application Port&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;EXPOSE 3000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Documents that the application listens on port 3000.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;start-the-application&quot;&gt;Start the Application&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;CMD [&quot;node&quot;, &quot;app.js&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Runs the Node.js application when the container starts.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-happens-during-docker-build-&quot;&gt;What Happens During Docker Build?&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TD
    A[Dockerfile]
    --&amp;gt; B[Pull Node Base Image]
    --&amp;gt; C[Create Working Directory]
    --&amp;gt; D[Copy package.json]
    --&amp;gt; E[Install Dependencies]
    --&amp;gt; F[Copy Application Code]
    --&amp;gt; G[Create Image Layers]
    --&amp;gt; H[Docker Image Ready]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Docker executes each instruction sequentially and creates image layers.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-3-build-the-docker-image&quot;&gt;Step 3: Build the Docker Image&lt;/h2&gt;
&lt;p&gt;Run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker build -t madhavnaks/node-app .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let's understand this command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Builds an image from a Dockerfile.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-t
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Assigns a tag to the image.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;madhavnaks/node-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Image name.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Current directory containing the Dockerfile.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;After a successful build you'll see output similar to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;=&amp;gt; naming to docker.io/madhavnaks/node-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Docker has now created an image locally on your machine.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-4-verify-the-image&quot;&gt;Step 4: Verify the Image&lt;/h2&gt;
&lt;p&gt;To see locally available images:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker images
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;REPOSITORY                TAG       IMAGE ID       SIZE
madhavnaks/node-app       latest    42c936686000   234MB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, the image exists but is not running.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;docker-image-vs-docker-container&quot;&gt;Docker Image vs Docker Container&lt;/h2&gt;
&lt;p&gt;Many beginners confuse these two concepts.&lt;/p&gt;
&lt;p&gt;Think of it like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docker Image = Blueprint&lt;/li&gt;
&lt;li&gt;Docker Container = Running Application&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    A[Docker Image]
    --&amp;gt; B[Container 1]

    A --&amp;gt; C[Container 2]

    A --&amp;gt; D[Container 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A single image can create multiple containers.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-5-run-a-container&quot;&gt;Step 5: Run a Container&lt;/h2&gt;
&lt;p&gt;Now let's start our application.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run -d --name node-app -p 3001:3000 madhavnaks/node-app
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;understanding-the-command&quot;&gt;Understanding the Command&lt;/h3&gt;
&lt;h4 id=&quot;detached-mode&quot;&gt;Detached Mode&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Runs the container in the background.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;container-name&quot;&gt;Container Name&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;--name node-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Assigns a friendly name to the container.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;port-mapping&quot;&gt;Port Mapping&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-p 3001:3000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Maps:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Host Port      → Container Port
3001           → 3000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The application listens on port 3000 inside the container but is accessible on port 3001 from your machine.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-6-verify-the-container&quot;&gt;Step 6: Verify the Container&lt;/h2&gt;
&lt;p&gt;Check running containers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;CONTAINER ID   IMAGE                 STATUS
10265698c978   madhavnaks/node-app   Up
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-7-access-the-application&quot;&gt;Step 7: Access the Application&lt;/h2&gt;
&lt;p&gt;Open:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;http://localhost:3001
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected response:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;message&quot;: &quot;Hello from Simple App (Node)&quot;,
  &quot;env&quot;: &quot;No env set&quot;,
  &quot;container&quot;: &quot;10265698c978&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the hostname.&lt;/p&gt;
&lt;p&gt;The hostname corresponds to the container ID, proving the application is running inside Docker.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-8-login-to-docker-hub&quot;&gt;Step 8: Login to Docker Hub&lt;/h2&gt;
&lt;p&gt;Before pushing images, authenticate with Docker Hub.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker login
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Login Succeeded
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Docker is now authorized to push images to your repository.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-push-images-to-docker-hub-&quot;&gt;Why Push Images to Docker Hub?&lt;/h2&gt;
&lt;p&gt;Docker Hub acts as a centralized registry.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    A[Developer Machine]
    --&amp;gt;|Push| B[Docker Hub]

    B --&amp;gt;|Pull| C[Server]

    B --&amp;gt;|Pull| D[Kubernetes Cluster]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once an image is pushed, it can be pulled and run from anywhere.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-9-push-the-image&quot;&gt;Step 9: Push the Image&lt;/h2&gt;
&lt;p&gt;Push the image to Docker Hub:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker push madhavnaks/node-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;latest: digest: sha256:d1081ec542f521e100bd886943c13ab7568c663b56e36200e6d846163975f979
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This confirms that the image has been successfully uploaded.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-10-verify-on-docker-hub&quot;&gt;Step 10: Verify on Docker Hub&lt;/h2&gt;
&lt;p&gt;Visit your Docker Hub account.&lt;/p&gt;
&lt;p&gt;You should now see:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;madhavnaks/node-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;listed under repositories.&lt;/p&gt;
&lt;p&gt;You can even share the image with others.&lt;/p&gt;
&lt;p&gt;Anyone can now run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker pull madhavnaks/node-app

docker run -p 3001:3000 madhavnaks/node-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;without needing your source code.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;real-world-devops-workflow&quot;&gt;Real-World DevOps Workflow&lt;/h2&gt;
&lt;p&gt;In production environments, the workflow usually looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    A[Developer Writes Code]
    --&amp;gt; B[GitHub]

    B --&amp;gt; C[CI/CD Pipeline]

    C --&amp;gt; D[Docker Build]

    D --&amp;gt; E[Docker Hub]

    E --&amp;gt; F[Kubernetes Cluster]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is exactly how modern DevOps teams package and deploy applications.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;key-takeaways&quot;&gt;Key Takeaways&lt;/h2&gt;
&lt;p&gt;✅ Dockerfile defines how an image should be built&lt;/p&gt;
&lt;p&gt;✅ Docker Build creates an image&lt;/p&gt;
&lt;p&gt;✅ Docker Run creates a container&lt;/p&gt;
&lt;p&gt;✅ Docker Hub stores and distributes images&lt;/p&gt;
&lt;p&gt;✅ One image can create multiple containers&lt;/p&gt;
&lt;p&gt;✅ Containerization ensures consistency across environments&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this guide, we covered the complete Docker workflow from source code to Docker Hub.&lt;/p&gt;
&lt;p&gt;We learned how to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a Dockerfile&lt;/li&gt;
&lt;li&gt;Build a Docker image&lt;/li&gt;
&lt;li&gt;Run a container&lt;/li&gt;
&lt;li&gt;Verify application execution&lt;/li&gt;
&lt;li&gt;Push images to Docker Hub&lt;/li&gt;
&lt;li&gt;Understand how images move through real-world DevOps pipelines&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Mastering these Docker fundamentals is the first step toward advanced topics like Docker Compose, Kubernetes, Helm, CI/CD pipelines, and cloud-native deployments.&lt;/p&gt;
&lt;p&gt;If you're learning DevOps, try containerizing applications in different languages such as Python, Java, Go, or .NET to strengthen your understanding of Docker.&lt;/p&gt;
&lt;p&gt;Happy Containerizing! &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20281/from-dockerfile-to-docker-hub-a-complete-beginners-guide-to-docker</guid>
<pubDate>Thu, 11 Jun 2026 10:31:28 +0000</pubDate>
</item>
<item>
<title>Running Astro in a preview container</title>
<link>https://coderlegion.com/19983/running-astro-in-a-preview-container</link>
<description>&lt;p&gt;If you've ever worked on a collection of different Node apps, you've likely encountered version conflicts. Everyone wants a different version of Node or PNPM, and your new job is trying to align them all, or managing versions daily.&lt;/p&gt;
&lt;p&gt;That's when open-source hero &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/kostiskapelonis/&quot;&gt;Kostis Kapelonis&lt;/a&gt; said, &quot;Why don't we run the preview in a container?&quot; In fact, he didn't just say this; he also submitted a PR. I told you he's an open-source hero.&lt;/p&gt;
&lt;p&gt;The PR added a &lt;code&gt;Dockerfile&lt;/code&gt; and a &lt;code&gt;docker-compose.yaml&lt;/code&gt; file to the project, which let you spin up the preview site using:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once Kostis had done all the hard work, I added a small enhancement to make Astro's live preview work when you change files. That meant you could start the container and keep working while all your changes are instantly visible in the preview. That keeps the developer inner loop nice and tight.&lt;/p&gt;
&lt;p&gt;If you want to do the same, here's how to make it happen. Once again, I added a very small cherry to Kostis' wonderfully fluffy cake, so send your adoration his way.&lt;/p&gt;
&lt;h3 id=&quot;add-a-docker-compose-file&quot;&gt;Add a Docker compose file&lt;/h3&gt;
&lt;p&gt;Here's the &lt;code&gt;docker-compose.yml&lt;/code&gt; file for your Astro project. It goes in the root directory.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  astro:
    build: .
    ports:
 - &quot;3000:3000&quot;
    volumes:
 - .:/app
 - /app/node_modules
    environment:
 - NODE_ENV=development
 - HOST=0.0.0.0
    stdin_open: true
    tty: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This maps the volumes, with a special case for node_modules. It exposes Astro's port 3000 inside the container to port 3000 on your machine so you can open it in your browser.&lt;/p&gt;
&lt;h3 id=&quot;the-docker-file&quot;&gt;The Docker file&lt;/h3&gt;
&lt;p&gt;Here's the &lt;code&gt;Dockerfile&lt;/code&gt;, which also goes in your project's root directory.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;# Use Node 20 as the base image
FROM node:20-slim

# Install pnpm globally
ENV PNPM_HOME=&quot;/pnpm&quot;
ENV PATH=&quot;$PNPM_HOME:$PATH&quot;
RUN corepack enable

# Set the working directory
WORKDIR /app

# Copy package files
COPY package.json pnpm-lock.yaml* ./

# Install dependencies
RUN pnpm install

# Copy the rest of the source code
COPY . .

# Expose the default Astro port
EXPOSE 3000

# Start the dev server
CMD [&quot;pnpm&quot;, &quot;compose:dev&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There's an optimization here around the package files, which is why they get their own copy command. There's an extra command in the &lt;code&gt;package.json&lt;/code&gt; file that we call here, too. It's a variation of the &lt;code&gt;dev&lt;/code&gt; script we use, but switches out the Astro run with a slight variation (the addition of the &lt;code&gt;--host&lt;/code&gt; flag) lets just show the important bits in this code snippet:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;scripts&quot;: {     
    &quot;compose:dev&quot;: &quot;npm-run-all --parallel dev:img dev:dictionary compose:dev:astro dev:watch&quot;,
    &quot;compose:dev:astro&quot;: &quot;astro dev --host&quot;,
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-vite-config-change&quot;&gt;The Vite config change&lt;/h3&gt;
&lt;p&gt;The final change is the one that makes the live refresh to work. This goes in your &lt;code&gt;astro.config.mjs&lt;/code&gt; file, and I popped it right after the existing &lt;code&gt;server&lt;/code&gt; config.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;server: {
    port: 3000
}
vite: {
    server: {
        watch: {
            usePolling: true,
        },
    },
},
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;spinning-it-up&quot;&gt;Spinning it up&lt;/h3&gt;
&lt;p&gt;The first time I ran this, I started things up with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose up --build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can then stop the container with&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you haven't changed the container, you can start it with the faster:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose up
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19983/running-astro-in-a-preview-container</guid>
<pubDate>Mon, 08 Jun 2026 16:35:17 +0000</pubDate>
</item>
<item>
<title>Moving from Dockerfile to apko for Declarative Images</title>
<link>https://coderlegion.com/17802/moving-from-dockerfile-to-apko-for-declarative-images</link>
<description>&lt;p&gt;Instead of a &lt;code&gt;Dockerfile&lt;/code&gt;, we write YAML-based files for &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chainguard-dev/apko&quot;&gt;apko&lt;/a&gt;&lt;/strong&gt;. This file tells &lt;code&gt;apko&lt;/code&gt; exactly which repositories to trust and which packages to materialise into the image.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir apko-demo &amp;amp;&amp;amp; cd apko-demo &amp;amp;&amp;amp; cat &amp;lt;&amp;lt;EOF &amp;gt; wolfi.yaml
contents:
  repositories:
    - https://packages.wolfi.dev/os
  keyring:
    - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub
  packages:
    - wolfi-base
    - python-3.11
    - py3-pip

entrypoint:
  command: /usr/bin/python3

archs:
  - x86_64
  - aarch64
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;why-this-is-different-from-a-dockerfile-&quot;&gt;Why this is different from a Dockerfile:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;repositories&lt;/code&gt; &amp;amp; &lt;code&gt;keyring&lt;/code&gt;: You are explicitly defining the trust chain.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages&lt;/code&gt;: These are pulled directly from Wolfi. There is no &lt;code&gt;apt-get update&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;archs&lt;/code&gt;: You can tell apko to build for both Intel and Apple Silicon at the same time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can simply run the below command to build the container image. &lt;br&gt;
You don't even need the Docker daemon running for this part.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apko build wolfi.yaml my-wolfi-python:latest output.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=16932139997674696109&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;So, what just happened? Well, &lt;code&gt;apko&lt;/code&gt; resolved the dependency tree for &lt;code&gt;Python 3.11&lt;/code&gt;. It downloaded the &lt;code&gt;.apk&lt;/code&gt; files directly. It then proceeded to unpack them into a filesystem and wrapped them in an OCI-compliant &lt;code&gt;.tar&lt;/code&gt; file. It then finished the process by generating an SBOM automatically in the same folder. You can then load and run the image with Docker, even though we never used Docker to build it, we can use Docker to run it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker load &amp;lt; output.tar
docker run --rm my-wolfi-python:latest-arm64 --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=10670442386965896980&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;single-layer-images-are-really-really-cool&quot;&gt;Single-layer images are really, really cool&lt;/h3&gt;
&lt;p&gt;In a standard Dockerfile, every &lt;code&gt;RUN&lt;/code&gt;, &lt;code&gt;COPY&lt;/code&gt;, and &lt;code&gt;ADD&lt;/code&gt; creates a new layer on disk. In &lt;strong&gt;apko&lt;/strong&gt;, everything is flattened into a single, optimised layer. This makes pulling the image across a network much faster because the container runtime only has to fetch and extract one single blob.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker history my-wolfi-python:latest-arm64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=6164810776283107478&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Look in your folder for a file ending in &lt;code&gt;.sbom.json&lt;/code&gt;. Open it up. You should have a complete, machine-readable list of every single library inside your image, generated for free.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jq . sbom-aarch64.spdx.json
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;SPDXID&quot;: &quot;SPDXRef-DOCUMENT&quot;,
  &quot;name&quot;: &quot;sbom-sha256:4df02c70193790be7e47ca9943cdce69959308bf13e5b095dc401cb0ae9d352f&quot;,
  &quot;spdxVersion&quot;: &quot;SPDX-2.3&quot;,
  &quot;creationInfo&quot;: {
    &quot;created&quot;: &quot;2026-05-13T20:49:35Z&quot;,
    &quot;creators&quot;: [
      &quot;Tool: apko (1.2.12)&quot;,
      &quot;Organization: Chainguard, Inc&quot;
    ],
    &quot;licenseListVersion&quot;: &quot;3.27&quot;
  },
  &quot;dataLicense&quot;: &quot;CC0-1.0&quot;,
  &quot;documentNamespace&quot;: &quot;https://spdx.org/spdxdocs/apko/&quot;,
  &quot;documentDescribes&quot;: [
    &quot;SPDXRef-Package-Image-sha256-7b4c80050663e9a70af6841bbf136a9e4af2d4f3338c1c2a462c8def09ef6cbf&quot;
  ],
  &quot;packages&quot;: [
    {
      &quot;SPDXID&quot;: &quot;SPDXRef-Package-Image-sha256-7b4c80050663e9a70af6841bbf136a9e4af2d4f3338c1c2a462c8def09ef6cbf&quot;,
      &quot;name&quot;: &quot;sha256:7b4c80050663e9a70af6841bbf136a9e4af2d4f3338c1c2a462c8def09ef6cbf&quot;,
      &quot;versionInfo&quot;: &quot;sha256:7b4c80050663e9a70af6841bbf136a9e4af2d4f3338c1c2a462c8def09ef6cbf&quot;,
      &quot;filesAnalyzed&quot;: false,
      &quot;description&quot;: &quot;apko container image&quot;,
      &quot;downloadLocation&quot;: &quot;NOASSERTION&quot;,
      &quot;supplier&quot;: &quot;Organization: Wolfi&quot;,
      &quot;primaryPackagePurpose&quot;: &quot;CONTAINER&quot;,
      &quot;checksums&quot;: [
        {
          &quot;algorithm&quot;: &quot;SHA256&quot;,
          &quot;checksumValue&quot;: &quot;7b4c80050663e9a70af6841bbf136a9e4af2d4f3338c1c2a462c8def09ef6cbf&quot;
        }
      ],
      &quot;externalRefs&quot;: [
        {
          &quot;referenceCategory&quot;: &quot;PACKAGE-MANAGER&quot;,
          &quot;referenceLocator&quot;: &quot;pkg:oci/my-wolfi-python@sha256:7b4c80050663e9a70af6841bbf136a9e4af2d4f3338c1c2a462c8def09ef6cbf?arch=arm64&amp;amp;mediaType=application%2Fvnd.oci.image.manifest.v1%2Bjson&amp;amp;os=linux&quot;,
          &quot;referenceType&quot;: &quot;purl&quot;
        }
      ]
    },
    {
      &quot;SPDXID&quot;: &quot;SPDXRef-Package-ImageLayer-sha256-4df02c70193790be7e47ca9943cdce69959308bf13e5b095dc401cb0ae9d352f&quot;,
      &quot;name&quot;: &quot;sha256:4df02c70193790be7e47ca9943cdce69959308bf13e5b095dc401cb0ae9d352f&quot;,
      &quot;versionInfo&quot;: &quot;20230201&quot;,
      &quot;filesAnalyzed&quot;: false,
      &quot;description&quot;: &quot;apko operating system layer&quot;,
      &quot;downloadLocation&quot;: &quot;NOASSERTION&quot;,
      &quot;supplier&quot;: &quot;Organization: Wolfi&quot;,
      &quot;primaryPackagePurpose&quot;: &quot;CONTAINER&quot;,
      &quot;externalRefs&quot;: [
        {
          &quot;referenceCategory&quot;: &quot;PACKAGE-MANAGER&quot;,
          &quot;referenceLocator&quot;: &quot;pkg:oci/my-wolfi-python@sha256:4df02c70193790be7e47ca9943cdce69959308bf13e5b095dc401cb0ae9d352f?arch=arm64&amp;amp;mediaType=application%2Fvnd.oci.image.layer.v1.tar%2Bgzip&amp;amp;os=linux&quot;,
          &quot;referenceType&quot;: &quot;purl&quot;
        }
      ]
    },
    {
      &quot;SPDXID&quot;: &quot;SPDXRef-OperatingSystem-wolfi&quot;,
      &quot;name&quot;: &quot;wolfi&quot;,
      &quot;versionInfo&quot;: &quot;20230201&quot;,
      &quot;filesAnalyzed&quot;: false,
      &quot;description&quot;: &quot;Operating System&quot;,
      &quot;downloadLocation&quot;: &quot;NOASSERTION&quot;,
      &quot;supplier&quot;: &quot;Organization: Wolfi&quot;,
      &quot;primaryPackagePurpose&quot;: &quot;OPERATING_SYSTEM&quot;
    },
    {
      &quot;SPDXID&quot;: &quot;SPDXRef-Package-apk-wolfi-baselayout-20230201-r29&quot;,
      &quot;name&quot;: &quot;wolfi-baselayout&quot;,
      &quot;versionInfo&quot;: &quot;20230201-r29&quot;,
      &quot;filesAnalyzed&quot;: false,
      &quot;licenseConcluded&quot;: &quot;NOASSERTION&quot;,
      &quot;licenseDeclared&quot;: &quot;MIT&quot;,
      &quot;downloadLocation&quot;: &quot;NOASSERTION&quot;,
      &quot;originator&quot;: &quot;Organization: Wolfi&quot;,
      &quot;supplier&quot;: &quot;Organization: Wolfi&quot;,
      &quot;copyrightText&quot;: &quot;NOASSERTION&quot;,
      &quot;primaryPackagePurpose&quot;: &quot;APPLICATION&quot;,
      &quot;externalRefs&quot;: [
        {
          &quot;referenceCategory&quot;: &quot;PACKAGE-MANAGER&quot;,
          &quot;referenceLocator&quot;: &quot;pkg:apk/wolfi/wolfi-baselayout@20230201-r29?arch=aarch64&amp;amp;distro=wolfi&quot;,
          &quot;referenceType&quot;: &quot;purl&quot;
        }
      ]
    },
    {
      &quot;SPDXID&quot;: &quot;SPDXRef-Package-Melange-wolfi-baselayout.yaml-35e7b4713442869116936fae3fe8cf7c69bc9054&quot;,
      &quot;name&quot;: &quot;wolfi-baselayout.yaml&quot;,
      &quot;versionInfo&quot;: &quot;35e7b4713442869116936fae3fe8cf7c69bc9054&quot;,
      &quot;filesAnalyzed&quot;: false,
      &quot;licenseConcluded&quot;: &quot;NOASSERTION&quot;,
      &quot;licenseDeclared&quot;: &quot;Apache-2.0&quot;,
      &quot;downloadLocation&quot;: &quot;NOASSERTION&quot;,
      &quot;originator&quot;: &quot;Organization: Wolfi&quot;,
      &quot;supplier&quot;: &quot;Organization: Wolfi&quot;,
      &quot;primaryPackagePurpose&quot;: &quot;INSTALL&quot;,
      &quot;externalRefs&quot;: [
        {
          &quot;referenceCategory&quot;: &quot;PACKAGE-MANAGER&quot;,
          &quot;referenceLocator&quot;: &quot;pkg:github/chainguard-dev/stereo@35e7b4713442869116936fae3fe8cf7c69bc9054#wolfi-baselayout.yaml&quot;,
          &quot;referenceType&quot;: &quot;purl&quot;
        }
      ]
    },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above SBOM contains a smoking gun that proves exactly why the Wolfi ecosystem is more secure than standard Docker builds. While a normal SBOM (like one generated by scanning a Debian image) just says, &quot;&lt;em&gt;I think I found a package called X&lt;/em&gt;&quot;, this Wolfi SBOM includes a specific record that links the final binary back to the original source code instructions. Look at this exact package entry:&lt;/p&gt;
&lt;h3 id=&quot;melange-is-the-secret-sauce&quot;&gt;Melange is the secret sauce&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&quot;SPDXID&quot;: &quot;SPDXRef-Package-Melange-wolfi-baselayout.yaml-35e7b4713442869116936fae3fe8cf7c69bc9054&quot;,
&quot;name&quot;: &quot;wolfi-baselayout.yaml&quot;,
&quot;primaryPackagePurpose&quot;: &quot;INSTALL&quot;,
&quot;referenceLocator&quot;: &quot;pkg:github/chainguard-dev/stereo@35e7b4713442869116936fae3fe8cf7c69bc9054#wolfi-baselayout.yaml&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This provides direct software provenance. Most SBOMs stop at the package-level. This SBOM goes a level deeper. It tells you exactly which Melange YAML file was used to build that specific &lt;code&gt;.apk&lt;/code&gt;. It’s not just baselayout, it’s basically showing &quot;baselayout as defined in this specific Git commit on GitHub&quot;.&lt;/p&gt;
&lt;p&gt;In this scenario there’s no guesswork. Most SBOM tools (like Syft or Trivy) have to scan a finished image and guess what’s inside. Because &lt;code&gt;apko&lt;/code&gt; builds the image by assembling packages it already knows about, this SBOM is authoritative. It was created during the build, not after. You can also see a reference to &lt;code&gt;chainguard-dev/stereo&lt;/code&gt;. This is an internal-facing look at how Wolfi packages are organised. It shows that even the most basic parts of your OS (the baselayout) are treated like verifiable source code.&lt;/p&gt;
&lt;p&gt;You’ll also notice the &lt;code&gt;&quot;licenseConcluded&quot;: &quot;NOASSERTION&quot;&lt;/code&gt; field in some spots. This isn't an error; it's actually a sign of a clean room build. It means &lt;code&gt;apko&lt;/code&gt; is strictly reporting what was declared in the package metadata rather than trying to guess the license by regex-scanning files, which is how many security tools end up with false positives.&lt;/p&gt;
&lt;p&gt;In short, this SBOM provides a clear chain of custody from your running container all the way back to a specific line of YAML in a GitHub repo. A standard &lt;code&gt;Dockerfile&lt;/code&gt; can almost never provide that level of transparency - and that’s why I’m moving over to &lt;code&gt;apko&lt;/code&gt; going forward.&lt;/p&gt;
&lt;h3 id=&quot;cleanup&quot;&gt;Cleanup&lt;/h3&gt;
&lt;p&gt;As always, I leave a little cleanup script at the end of the exercise:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rm -r apko-demo
docker stop $(docker ps -q)
docker rm $(docker ps -aq)
docker rmi $(docker images -q)
docker images
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/17802/moving-from-dockerfile-to-apko-for-declarative-images</guid>
<pubDate>Thu, 14 May 2026 09:50:03 +0000</pubDate>
</item>
<item>
<title>Why I’m building my Images using Wolfi</title>
<link>https://coderlegion.com/17729/why-im-building-my-images-using-wolfi</link>
<description>&lt;p&gt;I’m working in software supply chain security, with a specific focus on secure Kubernetes deployments. In a DevOps capacity, I’m focused specifically on ensuring that my container images have as few software vulnerabilities as possible. I believe &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/wolfi-dev&quot;&gt;Wolfi&lt;/a&gt;&lt;/strong&gt; is the best free and open source way to achieve this. This blog post should hopefully demonstrate the value proposition of the Wolfi project.&lt;/p&gt;
&lt;h3 id=&quot;getting-started-with-wolfi&quot;&gt;Getting started with Wolfi&lt;/h3&gt;
&lt;p&gt;This exercise assumes you already have the Docker runtime installed in order to launch your first Wolfi instance. If docker is already installed, simply open your terminal and pull the base Wolfi image from &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://edu.chainguard.dev/open-source/wolfi/overview/&quot;&gt;Chainguard&lt;/a&gt;&lt;/strong&gt; (the creators of Wolfi). This will drop you straight into a Wolfi shell: &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -it cgr.dev/chainguard/wolfi-base
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, docker downloaded the minimal Wolfi image and started an interactive session. You are now inside the &lt;strong&gt;Wolfi OS&lt;/strong&gt; container. Wolfi uses the &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://wiki.alpinelinux.org/wiki/Alpine_Package_Keeper&quot;&gt;apk&lt;/a&gt;&lt;/strong&gt; package manager (same as Alpine Linux), but it uses &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/wolfi-dev/os/blob/main/glibc.yaml&quot;&gt;glibc&lt;/a&gt;&lt;/strong&gt; for better compatibility with modern software. So, within your wolfi shell session, you can simply run the command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apk update
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You'll often hear that wolfi is &quot;&lt;strong&gt;distroless-adjacent&lt;/strong&gt;&quot;, or similar phrasing. In short, this means that the distro starts almost empty. If you want to use it for Python development specifically, as an example, you could run the below commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apk add python-3.11
python3 --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=12112540440976633695&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;If you want to leave the container, simply run the &lt;code&gt;exit&lt;/code&gt; command. Because of the &lt;code&gt;--rm&lt;/code&gt; flag (if you use it), the container will disappear, keeping your Macbook clean. While this process can be achieved manually by shelling into running containers, in reality, this is now how we would use Wolfi for Projects. Instead, the real power of Wolfi on a Mac is building ultra-secure, tiny images for your own apps, and to be created using a &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.docker.com/get-started/docker-concepts/building-images/writing-a-dockerfile/#explanation&quot;&gt;Dockerfile&lt;/a&gt;&lt;/strong&gt; like the example below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir wolfi-app &amp;amp;&amp;amp; cd wolfi-app &amp;amp;&amp;amp; cat &amp;lt;&amp;lt;EOF &amp;gt; Dockerfile
FROM cgr.dev/chainguard/wolfi-base
# Install only what you need
RUN apk add nodejs-20
WORKDIR /app
COPY . .
CMD [&quot;node&quot;, &quot;index.js&quot;]
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This Dockerfile has a line that says &lt;code&gt;COPY . .&lt;/code&gt; and &lt;code&gt;CMD [&quot;node&quot;, &quot;index.js&quot;]&lt;/code&gt;. Yet, we haven’t created those files yet. This Dockerfile expects a JavaScript file to exist, so let's create a quick one in order to prevent the build from failing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;console.log('Hello from Wolfi running on my Mac!');&quot; &amp;gt; index.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, tell Docker to read your Dockerfile and package it up into an image named &lt;code&gt;my-wolfi-app&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker build -t my-wolfi-app .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=12008982084100392931&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;-t&lt;/code&gt; command tags / names your image so you don't have to remember a random container ID number. Similarly, the (&lt;code&gt;.&lt;/code&gt;) simply tells Docker to look in the current folder for the Dockerfile. Once the build is created, you can run your custom Wolfi container:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run my-wolfi-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5218096406605721811&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;So what have we actually done so far? We got a boring helloworld style message from JavScript. Why does that matter? Well, by creating that &lt;code&gt;Dockerfile&lt;/code&gt;, we have created a &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.alexhyett.com/newsletter/thoughts-on-reproducible-environments/&quot;&gt;&lt;strong&gt;reproducible environment&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;From a &lt;strong&gt;security&lt;/strong&gt; standpoint, unlike a standard Mac environment, this container only has &lt;code&gt;nodejs&lt;/code&gt; and the bare essentials required to run that helloworld-style application. We’re here to reduce the software dependency bloat, ultimately reducing the overall software supply chain risk surface.&lt;/li&gt;
&lt;li&gt;From a &lt;strong&gt;portability&lt;/strong&gt; perspective, you could send that &lt;code&gt;Dockerfile&lt;/code&gt; and &lt;code&gt;index.js&lt;/code&gt; to a friend, and they would run the exact same environment with the exact same version of &lt;code&gt;Node&lt;/code&gt;, regardless of what they have installed on their machine. This needs to work globally, not just on my machine.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you, like myself, are on any of the &lt;strong&gt;M1&lt;/strong&gt;, &lt;strong&gt;M2&lt;/strong&gt;, or &lt;strong&gt;M3&lt;/strong&gt; Apple Silicon chips, Wolfi is fantastic because it has native &lt;code&gt;aarch64&lt;/code&gt; support. This means your containers will run at native speeds without needing Rosetta emulation.&lt;/p&gt;
&lt;h3 id=&quot;wait-why-use-this-over-ubuntu-or-alpine-&quot;&gt;Wait, why use this over Ubuntu or Alpine?&lt;/h3&gt;
&lt;p&gt;As we mentioned from a security standpoint, most standard distros often carry bloat (extra libraries and dependencies) that hackers can exploit. Wolfi is stripped down to the bare essentials and is updated almost daily to fix vulnerabilities (CVEs) faster than almost any other OS. Saying that, when we built our generic &lt;strong&gt;Node.js&lt;/strong&gt; web app, we introduced a lot of software dependencies into the build process. The container ended up being &lt;strong&gt;192 MB&lt;/strong&gt; in total size.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker images | grep my-wolfi-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=16512756977018092263&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;comparing-wolfi-build-sizes-versus-standard-distros&quot;&gt;Comparing Wolfi build sizes versus standard distros&lt;/h3&gt;
&lt;p&gt;Comparing image sizes is one of the best ways to show off why developers are flocking to Wolfi. It’s significantly leaner than traditional, thick distributions like &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://hub.docker.com/_/ubuntu/&quot;&gt;Ubuntu&lt;/a&gt;&lt;/strong&gt; (39.6 MB), but maintains the compatibility that sometimes breaks in &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://hub.docker.com/_/alpine&quot;&gt;Alpine&lt;/a&gt;&lt;/strong&gt; (3.6 MB).&lt;/p&gt;
&lt;p&gt;I’m running all the steps below on my Macbook to pull a few standard images. First of all, Wolfi is a distroless / &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.chainguard.dev/unchained/introducing-wolfi-the-first-linux-un-distro-designed-for-securing-the-software-supply-chain&quot;&gt;(un)distro&lt;/a&gt;&lt;/strong&gt; setup, so Wolfi will work the same on Mac as it will work on any other underlying distribution. So let’s use Docker to download the images and use the docker images command to compare them.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull ubuntu:latest
docker pull debian:stable-slim
docker pull alpine:latest
docker images | grep -E &quot;wolfi|ubuntu|debian|alpine&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=14237599569964637555&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;What you’ll likely see:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Debian Slim&lt;/strong&gt;: 141 MB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ubuntu&lt;/strong&gt;: 180 MB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wolfi&lt;/strong&gt; (&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://hub.docker.com/r/chainguard/wolfi-base&quot;&gt;base&lt;/a&gt;&lt;/strong&gt;): 21.9 MB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Alpine&lt;/strong&gt;: 13.6MB&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The argument here is that Wolfi is nearly as small as Alpine (the gold standard for tiny images), but unlike Alpine, it uses &lt;code&gt;glibc&lt;/code&gt;, meaning your real world apps won't break due to missing library links.&lt;/p&gt;
&lt;h3 id=&quot;understand-whether-a-container-is-built-using-wolfi&quot;&gt;Understand whether a container is built using Wolfi&lt;/h3&gt;
&lt;p&gt;Since Wolfi doesn't have a flashy desktop wallpaper, you have to look at the system identity files and the unique way it handles libraries. By using &lt;code&gt;docker run&lt;/code&gt; instead of &lt;code&gt;exec&lt;/code&gt;. This creates a container instance from your image, runs the command, and then shuts down.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run --rm my-wolfi-app cat /etc/os-release
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Checking &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.freedesktop.org/software/systemd/man/latest/os-release.html&quot;&gt;os-release&lt;/a&gt;&lt;/strong&gt; is likely the most direct proof of wolfi being used to build the container image. Every Linux distribution has a file that describes what it is. The &lt;code&gt;/etc/os-release&lt;/code&gt; path is a standard config file in Linux systems (introduced by systemd) that contains operating system identification data, such as the name, version, and ID of the distribution. It is used for identifying the system in scripts, ensuring compatibility, and providing vendor-independent information.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=12631332412038694556&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ID=wolfi&lt;/li&gt;
&lt;li&gt;NAME=&quot;Wolfi&quot;&lt;/li&gt;
&lt;li&gt;PRETTY_NAME=&quot;Wolfi&quot;&lt;/li&gt;
&lt;li&gt;VERSION_ID=&quot;20230201&quot;&lt;/li&gt;
&lt;li&gt;HOME_URL=&quot;&lt;a rel=&quot;nofollow&quot; href=&quot;https://wolfi.dev&quot;&gt;https://wolfi.dev&lt;/a&gt;&quot;&lt;/li&gt;
&lt;li&gt;BUG_REPORT_URL=&quot;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/wolfi-dev/os/issues&quot;&gt;https://github.com/wolfi-dev/os/issues&lt;/a&gt;&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the official birth certificate of &lt;strong&gt;WolfiOS&lt;/strong&gt;. You can also check the apk package manager to show your friends where the software actually comes from.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run --rm my-wolfi-app cat /etc/apk/repositories
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=9878792876002365342&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;If you want to get really technical, like-for-like comparison of container images to show your friends how many extra (and in some cases unnecessary) tools are sitting inside the general purpose image that a hacker could use - versus Wolfi - just run the below commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run --rm python:latest ls /usr/bin/ | wc -l
docker run --rm --entrypoint sh cgr.dev/chainguard/python:latest-dev -c &quot;ls /usr/bin/&quot; | wc -l
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=14296167763467635036&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The below numbers perfectly illustrate why &lt;strong&gt;hardened&lt;/strong&gt;, &lt;strong&gt;minimal images&lt;/strong&gt; like those from Chainguard’s Wolfi are gaining so much traction. The delta we're seeing (655 vs. 326) represents the &lt;strong&gt;attack surface&lt;/strong&gt;. In the official Python image, you have over 300 additional binaries (things like &lt;code&gt;sed&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, and various networking tools) that aren't strictly necessary for your Python code to execute, but provide more living off the land (&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.crowdstrike.com/en-us/cybersecurity-101/cyberattacks/living-off-the-land-attack/&quot;&gt;LOTL-style&lt;/a&gt;&lt;/strong&gt;) opportunities for an attacker.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=6633481679747430199&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;so-why-are-the-numbers-so-different-&quot;&gt;So, why are the numbers so different?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;official&lt;/strong&gt; &lt;code&gt;python:latest&lt;/code&gt; (&lt;strong&gt;655 files&lt;/strong&gt;) image is based on &lt;code&gt;Debian&lt;/code&gt;. It includes a full suite of GNU Coreutils, multiple shells, and legacy system utilities. It’s designed for convenience so that almost any script you write &quot;&lt;strong&gt;just works&lt;/strong&gt;&quot; because all the standard Linux tools are there.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chainguard’s&lt;/strong&gt; &lt;code&gt;python:latest-dev&lt;/code&gt; (&lt;strong&gt;326 files&lt;/strong&gt;) is alternatively based on &lt;strong&gt;Wolfi&lt;/strong&gt;. Even in the &lt;code&gt;-dev&lt;/code&gt; version (which intentionally includes more tools than the production version), they have stripped out almost everything that isn't required to build or run common Python applications.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The hidden reality here is that if developers were to run that same count on the &lt;strong&gt;non-dev&lt;/strong&gt; Chainguard image (the production version), the number would drop even more drastically - likely down to &lt;strong&gt;&lt;em&gt;under 10 files.&lt;/em&gt;&lt;/strong&gt; There is essentially only python and the bare-minimum shared libraries (&lt;code&gt;libc&lt;/code&gt;, and stuff like that) the basic packages needed to keep the Python interpreter alive in the Chainguard production version.&lt;/p&gt;
&lt;p&gt;In a production Chainguard Python image:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There is no &lt;code&gt;ls&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;There is no &lt;code&gt;sh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;There is no &lt;code&gt;pip&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Where the developer experience gains are clear for the official python image, since it includes everything so nothing breaks in &lt;strong&gt;Day 2 operations&lt;/strong&gt;, the free-to-use Chainguard dev images provide the hardened development environment required for my builds so it only includes what's needed to build and test the web application. Going further with the Chainguard licensed images, which were never the focus of this blog post, developers receive maximum security, since it includes no shell, no tools, and no unnecessary binaries. You can do additional testing by using cli tools like &lt;code&gt;pip&lt;/code&gt; to list python-specific packages in each image you test.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run --rm python:latest pip list
docker run --rm cgr.dev/chainguard/python:latest-dev -m pip list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I hope you found this blog useful.&lt;/p&gt;
&lt;h3 id=&quot;cleanup-1&quot;&gt;Cleanup&lt;/h3&gt;
&lt;p&gt;If you want to delete all the locally stored images that aren't currently being used by a container, use the &lt;code&gt;prune&lt;/code&gt; command or just remove the images manually with &lt;code&gt;rmi&lt;/code&gt;. I’m lazy, so I’m going to just do a nuclear cleanup on everything created during this exercise:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker stop $(docker ps -q)
docker rm $(docker ps -aq)
docker rmi $(docker images -q)
docker images
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/17729/why-im-building-my-images-using-wolfi</guid>
<pubDate>Wed, 13 May 2026 15:02:58 +0000</pubDate>
</item>
<item>
<title>Infrastructure as Code: Deploying a Financial Ecosystem with Docker Compose</title>
<link>https://coderlegion.com/16328/infrastructure-as-code-deploying-a-financial-ecosystem-with-docker-compose</link>
<description>&lt;p&gt;Hey folks!&lt;/p&gt;
&lt;p&gt;In the &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/rvneto/building-a-microservices-ecosystem-stock-brokerage-simulator-my-broker-b3-45bj&quot;&gt;previous post&lt;/a&gt;, I introduced the overview of &lt;strong&gt;My Broker B3&lt;/strong&gt;. Today we'll talk about the foundation that supports the entire ecosystem: the &lt;strong&gt;infrastructure&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For a microservices system of this complexity, manually configuring each database, message broker and monitoring tool would be unmanageable. The solution was to use &lt;strong&gt;Docker Compose&lt;/strong&gt; to create a reproducible local environment that faithfully replicates the needs of a distributed system.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-the-infrastructure-map&quot;&gt;️ The Infrastructure Map&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;docker-compose.yml&lt;/code&gt; orchestrates &lt;strong&gt;12 containers&lt;/strong&gt; organized in 5 layers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;┌─────────────────────────────────────────────┐
│           RELATIONAL LAYER (SQL)            │
│  identity-db  wallet-db  order-db  asset-db │
│     MySQL        MySQL    MySQL     MySQL   │
│                  b3-core-db (PostgreSQL)    │
├─────────────────────────────────────────────┤
│              NOSQL LAYER                    │
│           broker-mongodb (Mongo 6.0)        │
├─────────────────────────────────────────────┤
│              CACHE LAYER                    │
│    broker-asset-cache    b3-market-cache    │
│         Redis Alpine          Redis Alpine  │
├─────────────────────────────────────────────┤
│             MESSAGING LAYER                 │
│    kafka (KRaft)        rabbitmq (AMQP)     │
├─────────────────────────────────────────────┤
│           OBSERVABILITY LAYER               │
│        prometheus            grafana        │
└─────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-design-decision-domain-isolation&quot;&gt;️ Design Decision: Domain Isolation&lt;/h2&gt;
&lt;p&gt;The most important infrastructure decision was &lt;strong&gt;data isolation by domain&lt;/strong&gt;. Instead of a monolithic database, each microservice has its own instance:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# Each service has its own database — failures are isolated
broker-identity-db:
  image: mysql:8.0
  ports:
    - '3306:3306'  # identity

broker-wallet-db:
  image: mysql:8.0
  ports:
    - '3307:3306'  # wallet

broker-order-db:
  image: mysql:8.0
  ports:
    - '3308:3306'  # orders

broker-asset-db:
  image: mysql:8.0
  ports:
    - '3309:3306'  # assets

b3-core-db:
  image: postgres:15
  ports:
    - '5432:5432'  # B3 core
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why does this matter?&lt;/strong&gt; If the orders database has a problem, the identity service keeps running. Failures stay isolated in their domain.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;cache-isolated-instances-by-context&quot;&gt;Cache: Isolated Instances by Context&lt;/h2&gt;
&lt;p&gt;Two Redis instances with distinct purposes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;broker-asset-cache:
  image: redis:7.0-alpine
  ports:
    - '${REDIS_BROKER_ASSET_PORT}:6379'  # prices for the broker

b3-market-cache:
  image: redis:7.0-alpine
  ports:
    - '${REDIS_B3_MARKET_PORT}:6379'    # prices for the matching engine
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;b3-market-sync-api&lt;/code&gt; writes to &lt;code&gt;b3-market-cache&lt;/code&gt; and the &lt;code&gt;b3-matching-engine-api&lt;/code&gt; reads from it to decide whether to execute or reject an order. The separation ensures that broker-side problems don't affect the B3 side and vice versa.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;messaging-two-brokers-two-purposes&quot;&gt;Messaging: Two Brokers, Two Purposes&lt;/h2&gt;
&lt;h4 id=&quot;apache-kafka-internal-event-bus&quot;&gt;Apache Kafka — Internal Event Bus&lt;/h4&gt;
&lt;p&gt;Configured in modern &lt;strong&gt;KRaft&lt;/strong&gt; mode (no Zookeeper), lighter and more stable for local development:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;kafka:
  image: apache/kafka:3.7.0
  environment:
    KAFKA_PROCESS_ROLES: broker,controller
    KAFKA_LISTENERS: EXTERNAL://0.0.0.0:9092,INTERNAL://0.0.0.0:29092,CONTROLLER://0.0.0.0:9093
    KAFKA_ADVERTISED_LISTENERS: EXTERNAL://localhost:9092,INTERNAL://finance-kafka:29092
    KAFKA_CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Used for the ecosystem's internal topics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;assets-market-data-v1&lt;/code&gt; — asset quotes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;order-events-v1&lt;/code&gt; — order lifecycle events&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;rabbitmq-broker-b3-integration&quot;&gt;RabbitMQ — Broker ↔ B3 Integration&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;rabbitmq:
  image: rabbitmq:3-management
  ports:
    - '5672:5672'    # AMQP
    - '15672:15672'  # Management UI
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Used exclusively for communication between the broker and the B3 simulator:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mq-broker-to-b3&lt;/code&gt; — orders sent to the matching engine&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mq-b3-to-broker&lt;/code&gt; — execution feedback&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why two messaging systems?&lt;/strong&gt; Kafka is ideal for event streams with replay and multiple consumers. RabbitMQ is ideal for work queues with point-to-point delivery guarantees. Each where it makes the most sense.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;observability-from-day-one&quot;&gt;Observability from Day One&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;prometheus:
  image: prom/prometheus:latest
  volumes:
    - ./config/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
  ports:
    - '9090:9090'

grafana:
  image: grafana/grafana:latest
  ports:
    - '3000:3000'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All Spring Boot microservices export metrics via &lt;code&gt;/actuator/health&lt;/code&gt; and &lt;code&gt;/actuator/prometheus&lt;/code&gt;. Prometheus collects, Grafana visualizes. From the very first service that starts, we already have observability.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;security-no-hardcoded-secrets&quot;&gt;Security: No Hardcoded Secrets&lt;/h2&gt;
&lt;p&gt;All sensitive configuration lives in a &lt;code&gt;.env&lt;/code&gt; file (ignored by Git):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# docker-compose.yml — references only
broker-identity-db:
  environment:
    MYSQL_DATABASE: ${BROKER_IDENTITY_DB_NAME}
    MYSQL_USER: ${BROKER_DB_USER}
    MYSQL_PASSWORD: ${BROKER_DB_PASS}
    MYSQL_ROOT_PASSWORD: ${BROKER_DB_ROOT_PASS}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A &lt;code&gt;.env.example&lt;/code&gt; is committed as a template, but actual values never enter the repository.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;how-to-run&quot;&gt;How to Run&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cp .env.example .env
# fill in the variables in .env

docker-compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A single command brings up all 12 containers. The development environment is identical for anyone who clones the repository.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-s-next-&quot;&gt;What's Next?&lt;/h2&gt;
&lt;p&gt;With the infrastructure running, the next post shows the first microservice: &lt;code&gt;broker-market-data-api&lt;/code&gt; — a Python service that fetches real quotes from Brapi, persists them in MongoDB and publishes to Kafka.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;about-the-series&quot;&gt;About the Series&lt;/h2&gt;
&lt;p&gt;⬅️ Previous Post: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/rvneto/building-a-microservices-ecosystem-stock-brokerage-simulator-my-broker-b3-45bj&quot;&gt;Building a Microservices Ecosystem&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;➡️ Next Post: Market Data Integrator: Consuming Real-Time Data with Python, MongoDB and Kafka&lt;/p&gt;
&lt;p&gt; Series Index: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/rvneto/series-roadmap-building-a-stock-brokerage-simulator-with-microservices-kgh&quot;&gt;Series Roadmap&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/roberto-de-vargas/&quot;&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rvneto/trading-docker-infra&quot;&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16328/infrastructure-as-code-deploying-a-financial-ecosystem-with-docker-compose</guid>
<pubDate>Fri, 01 May 2026 13:51:55 +0000</pubDate>
</item>
<item>
<title>Building Production-Grade Microservices on AWS ECS Fargate with GitLab CI/CD Automation</title>
<link>https://coderlegion.com/14226/building-production-grade-microservices-on-aws-ecs-fargate-with-gitlab-ci-cd-automation</link>
<description>&lt;p&gt;&lt;strong&gt;1. Introduction&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Nowadays, applications are moving towards microservices because of their scalability, rollback deployment, and faster development. &lt;br&gt;
But with the growing number of services, deployment pipelines have become:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Slow (like updating all services on every commit)&lt;/li&gt;
&lt;li&gt;Hard to manage (multiple pipelines for a single service)&lt;/li&gt;
&lt;li&gt;This was causing problems (manual deployments, Docker images being inconsistent, etc.).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I combined &lt;strong&gt;AWS Elastic Container Service (ECS) Fargate&lt;/strong&gt; and &lt;strong&gt;Amazon ECR,&lt;/strong&gt; combined with &lt;strong&gt;GitLab CI/CD&lt;/strong&gt;, which made the pipeline deploy only the service which was changed.&lt;/p&gt;
&lt;p&gt;In this guide, I have created an actual production-ready microservices deployment system with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ECS Fargate&lt;/strong&gt; for serverless containers&lt;/li&gt;
&lt;li&gt;Five microservices:&lt;br&gt;
  &lt;strong&gt;billing&lt;/strong&gt;, &lt;strong&gt;client_app_api&lt;/strong&gt;, &lt;strong&gt;driver_api&lt;/strong&gt;, &lt;strong&gt;odmu_auth&lt;/strong&gt;, &lt;strong&gt;odmu_contractual&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Amazon ECR&lt;/strong&gt; as an image registry for containers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ALB (Application Load Balancer)&lt;/strong&gt; with multiple target groups&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitLab CI/CD&lt;/strong&gt; with folder based triggers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Terraform&lt;/strong&gt; for infrastructure as code&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CloudWatch&lt;/strong&gt; for monitoring&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By the end, you’ll have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Independent deployments per microservice&lt;/li&gt;
&lt;li&gt;Folder-based Automated CI/CD&lt;/li&gt;
&lt;li&gt;Production grade ECS services&lt;/li&gt;
&lt;li&gt;Scalable, secure, monitored architecture&lt;/li&gt;
&lt;li&gt;No manual SSH or PM2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The Real Problem&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When I was previously working with ECS, every change in code triggered a full redeployment of all services, although the change was in one service folder. It caused unnecessary downtime and more deployment times, and rollback was risky. I also faced Docker image drift in various environments.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I found a solution&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Only the service that changes is rebuilt and deployed&lt;/li&gt;
&lt;li&gt;Each service remains fully isolated&lt;/li&gt;
&lt;li&gt;Deployments are repeatable and predictable&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-architecture-&quot;&gt;2. ARCHITECTURE:&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/86xjnix6lbdywv3jhqxm.png&quot; alt=&quot;Image description&quot;&gt; &lt;br&gt;
&lt;strong&gt;Key Components&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Component &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;ECS Fargate &lt;/td&gt;&lt;td&gt;Serverless containers (no EC2 management)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;ECR &lt;/td&gt;&lt;td&gt;Private container registry&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;ALB &lt;/td&gt;&lt;td&gt;Routes incoming traffic to the right microservice&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CloudWatch &lt;/td&gt;&lt;td&gt;Logs and metrics&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Terraform &lt;/td&gt;&lt;td&gt;Infrastructure provisioning&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;GitLab CI/CD &lt;/td&gt;&lt;td&gt;Automated container build and deployment&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Why &lt;strong&gt;ECS Fargate&lt;/strong&gt;?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No EC2 provisioning or patching&lt;/li&gt;
&lt;li&gt;Auto-scaling built-in&lt;/li&gt;
&lt;li&gt;Secure &lt;/li&gt;
&lt;li&gt;Suits for microservices&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;3-microservices-structure&quot;&gt;3. Microservices Structure&lt;/h3&gt;
&lt;p&gt;Repo Structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;services/
 ├── billing/
 ├── client_app_api/
 ├── driver_api/
 ├── odmu_auth/
 └── odmu_contractual/
.gitlab-ci.yml
terraform/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each microservice has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Its own Dockerfile&lt;/li&gt;
&lt;li&gt;Its own business logic&lt;/li&gt;
&lt;li&gt;Its own task definition&lt;/li&gt;
&lt;li&gt;Its own ECS service&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;4-dockerizing-the-microservices&quot;&gt;4. Dockerizing the Microservices&lt;/h3&gt;
&lt;p&gt;Example Dockerfile for client_app_api:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-docker&quot;&gt;FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
EXPOSE 3000

CMD [&quot;node&quot;, &quot;server.js&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I used different ports for each microservice:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Service              Port
billing              3001
client_app_api       3000
driver_api           3002
odmu_auth            3003
odmu_contractual     3004
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;5-creating-ecr-repositories&quot;&gt;5. Creating ECR Repositories&lt;/h3&gt;
&lt;p&gt;Account ID: 745812456855&lt;br&gt;
Region: us-east-1&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;aws ecr create-repository --repository-name billing
aws ecr create-repository --repository-name client_app_api
aws ecr create-repository --repository-name driver_api
aws ecr create-repository --repository-name odmu_auth
aws ecr create-repository --repository-name odmu_contractual


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Build and Push (example: billing)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;docker build -t billing ./services/billing

docker tag billing:latest \
  745812456855.dkr.ecr.us-east-1.amazonaws.com/billing:latest

aws ecr get-login-password --region us-east-1 \
  | docker login --username AWS \
  --password-stdin 745812456855.dkr.ecr.us-east-1.amazonaws.com

docker push \
  745812456855.dkr.ecr.us-east-1.amazonaws.com/billing:latest

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;6-ecs-fargate-task-definition-terraform-&quot;&gt;6. ECS Fargate: Task Definition (Terraform)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Example for the billing microservice:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-terraform&quot;&gt;resource &quot;aws_ecs_task_definition&quot; &quot;billing&quot; {
  family                   = &quot;billing-task&quot;
  network_mode             = &quot;awsvpc&quot;
  requires_compatibilities = [&quot;FARGATE&quot;]
  cpu                      = &quot;512&quot;
  memory                   = &quot;1024&quot;

  execution_role_arn = &quot;arn:aws:iam::745812456855:role/ecsTaskExecutionRole&quot;

  container_definitions = jsonencode([
    {
      name  = &quot;billing&quot;
      image = &quot;745812456855.dkr.ecr.us-east-1.amazonaws.com/billing:latest&quot;

      portMappings = [{
        containerPort = 3001
        protocol      = &quot;tcp&quot;
      }]

      essential = true
    }
  ])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similar type for all microservices.&lt;/p&gt;
&lt;h3 id=&quot;7-application-load-balancer-routing&quot;&gt;7. Application Load Balancer Routing&lt;/h3&gt;
&lt;p&gt;Each microservice has a separate target group:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Service           &lt;/th&gt;&lt;th&gt;Target Group Name &lt;/th&gt;&lt;th&gt;Path Pattern&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;billing           &lt;/td&gt;&lt;td&gt;tg-billing        &lt;/td&gt;&lt;td&gt;/billing/*&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;client_app_api    &lt;/td&gt;&lt;td&gt;tg-client         &lt;/td&gt;&lt;td&gt;/client/*&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;driver_api        &lt;/td&gt;&lt;td&gt;tg-driver         &lt;/td&gt;&lt;td&gt;/driver/*&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;odmu_auth         &lt;/td&gt;&lt;td&gt;tg-auth           &lt;/td&gt;&lt;td&gt;/auth/*&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;odmu_contractual  &lt;/td&gt;&lt;td&gt;tg-contract       &lt;/td&gt;&lt;td&gt;/contract/*&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;security-considerations&quot;&gt;Security Considerations&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Security was a first-class concern in this design&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each ECS task runs with VPC networking. Each ECS task has ENI-level network isolation.&lt;/li&gt;
&lt;li&gt;Containers cannot be accessed via SSH; therefore, all deployments take place through CI/CD.&lt;/li&gt;
&lt;li&gt;IAM task execution roles are created following the principle of least privilege.&lt;/li&gt;
&lt;li&gt;All container images are stored in private ECR repositories on Amazon.&lt;/li&gt;
&lt;li&gt;The ALB serves as the only public access point to ECS, and all services remain in a private subnet inside the VPC.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Terraform example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-terraform&quot;&gt;resource &quot;aws_lb_target_group&quot; &quot;billing&quot; {
  name        = &quot;tg-billing&quot;
  port        = 3001
  protocol    = &quot;HTTP&quot;
  target_type = &quot;ip&quot;
  vpc_id      = var.vpc_id
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;GitLab CI/CD: Folder-Based Deployments&lt;br&gt;
This is the most important section.&lt;br&gt;
Instead of running one giant pipeline or manually using SSH/PM2, I configured the GitLab yml file to only deploy the service that changed.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;stages:
  - build
  - deploy

# ----------------------------
# Billing Microservice
# ----------------------------
build_billing:
  stage: build
  only:
    changes:
      - services/billing/**
  script:
    - docker build -t billing ./services/billing
    - docker tag billing:latest 745812456855.dkr.ecr.us-east-1.amazonaws.com/billing:latest
    - aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 745812456855.dkr.ecr.us-east-1.amazonaws.com
    - docker push 745812456855.dkr.ecr.us-east-1.amazonaws.com/billing:latest

deploy_billing:
  stage: deploy
  only:
    changes:
      - services/billing/**
  script:
    - aws ecs update-service \
        --cluster production-cluster \
        --service billing-service \
        --force-new-deployment

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;9-monitoring-amp-observability-cloudwatch-&quot;&gt;9. Monitoring &amp;amp; Observability (CloudWatch)&lt;/h3&gt;
&lt;p&gt;Logs appear under:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;/ecs/billing
/ecs/client_app_api
/ecs/driver_api
/ecs/odmu_auth
/ecs/odmu_contractual
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create log groups:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;aws logs create-log-group --log-group-name /ecs/billing
aws logs put-retention-policy --log-group-name /ecs/billing --retention-in-days 7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CPU Alarm:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;aws cloudwatch put-metric-alarm \
--alarm-name BillingCPUHigh \
--metric-name CPUUtilization \
--namespace AWS/ECS \
--statistic Average \
--period 300 \
--threshold 80 \
--comparison-operator GreaterThanThreshold \
--dimensions Name=ServiceName,Value=billing-service \
--evaluation-periods 2 \
--alarm-actions arn:aws:sns:us-east-1:745812456855:NotifyTeam

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;effects-and-outcomes-of-implementing-new-architecture&quot;&gt;Effects and Outcomes of Implementing New Architecture&lt;/h4&gt;
&lt;p&gt;By deploying this architecture I was able to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reduce deployment time, since only those services that had their code changed must be rebuilt.&lt;/li&gt;
&lt;li&gt;Allow teams to deploy their microservices independently of one another.&lt;/li&gt;
&lt;li&gt;No need to deploy via SSH using PM2.&lt;/li&gt;
&lt;li&gt;Create a more predictable and safer rollback process.&lt;/li&gt;
&lt;li&gt;Provide for reuse of the same Terraform modules in all target environments.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With the increasing number of services, it became clear that this architecture can be scaled successfully.&lt;/p&gt;
&lt;h3 id=&quot;10-lessons-learned-&quot;&gt;10. Lessons Learned:&lt;/h3&gt;
&lt;p&gt;Here are some of the lessons I learned from building this system:&lt;br&gt;
✔Keep microservices fully isolated&lt;br&gt;
The Docker images, task definitions and ALB routes need to be decoupled.&lt;br&gt;
✔ Use folder-based CI/CD&lt;br&gt;
It reduces deployment times so much.&lt;br&gt;
✔ Choose ECR instead of using public registries&lt;br&gt;
It works well with ECS.&lt;br&gt;
✔ Allocate the right CPU &amp;amp; Memory&lt;br&gt;
Under-provisioning leads to throttling.&lt;br&gt;
Over-provisioning wastes money.&lt;br&gt;
✔ Terraform modules save weeks&lt;br&gt;
When multiple environment setups are in question, reusability is the key.&lt;br&gt;
✔ With this CloudWatch setu you get 80% of your monitoring&lt;br&gt;
Later, add Prometheus or Grafana Cloud.&lt;/p&gt;
&lt;h3 id=&quot;11-conclusion&quot;&gt;11. Conclusion&lt;/h3&gt;
&lt;p&gt;Containers are the foundation of modern infrastructure, and AWS ECS Fargate enables you to run microservices without managing servers. Mixed with GitLab CI/CD and Terraform, you have a scalable, automated, production-grade workflow.&lt;br&gt;
This architecture ensures:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fast, independent deployments&lt;/li&gt;
&lt;li&gt;Secure and isolated workloads&lt;/li&gt;
&lt;li&gt;ALB with Fargate for high availability&lt;/li&gt;
&lt;li&gt;Complete observability via CloudWatch&lt;/li&gt;
&lt;li&gt;Infrastructure is defined as code&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you are deploying microservices on AWS and experiencing slow or high risk deployments, this might be a good working solution validated across real-world environments with the increasing number of services.&lt;/p&gt;
&lt;p&gt;If you enjoyed this post, please share it on your favourite social network and get in touch if I can help with anything. Feel free to reach out if you’d like to chat about AWS, DevOps, or container architectures.&lt;/p&gt;
&lt;h3 id=&quot;connect-with-me&quot;&gt;Connect with me&lt;/h3&gt;
&lt;p&gt;• Portfolio: &lt;a rel=&quot;nofollow&quot; href=&quot;https://khanalsamir.com&quot;&gt;https://khanalsamir.com&lt;/a&gt;&lt;br&gt;
• GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Shawmeer&quot;&gt;https://github.com/Shawmeer&lt;/a&gt;&lt;br&gt;
• LinkedIn: &lt;a rel=&quot;nofollow&quot; href=&quot;https://linkedin.com/in/samir-khanal&quot;&gt;https://linkedin.com/in/samir-khanal&lt;/a&gt;  &lt;/p&gt;
&lt;p&gt;If you're working on similar DevOps or AWS projects, feel free to connect. I regularly share practical cloud and CI/CD content.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14226/building-production-grade-microservices-on-aws-ecs-fargate-with-gitlab-ci-cd-automation</guid>
<pubDate>Wed, 08 Apr 2026 11:03:05 +0000</pubDate>
</item>
<item>
<title>From image to HTTPS endpoint in one step with ECS Express Mode</title>
<link>https://coderlegion.com/13189/from-image-to-https-endpoint-in-one-step-with-ecs-express-mode</link>
<description>&lt;h2 id=&quot;amazon-ecs-from-ec2-managed-to-express-mode&quot;&gt;Amazon ECS: From EC2 Managed to Express Mode&lt;/h2&gt;
&lt;p&gt;Amazon ECS has evolved significantly over the years—from managing EC2 container instances manually, to the introduction of Fargate for serverless containers, making infrastructure management seamless. For developers or anyone new to containers, the priority is deploying applications at pace without complexity.&lt;/p&gt;
&lt;p&gt;Recently, AWS introduced ECS Managed Instances to simplify launching workloads further. Now the big leap is &lt;strong&gt;ECS Express Mode&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-ecs-express-mode-&quot;&gt;What is ECS Express Mode?&lt;/h3&gt;
&lt;p&gt;Express Mode lets you deploy containerized applications in a single step. Upload your container image from Amazon ECR, optionally specify basic resources like vCPU and memory, or skip resource input to use defaults. Express Mode automatically provisions all necessary AWS infrastructure for you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Application Load Balancer (ALB)  &lt;/li&gt;
&lt;li&gt;CloudWatch Logs  &lt;/li&gt;
&lt;li&gt;Networking &amp;amp; VPC configuration  &lt;/li&gt;
&lt;li&gt;Auto scaling  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This takes away the heavy lifting and infrastructure setup that often slows down deployment.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-ecs-express-mode-matters&quot;&gt;Why ECS Express Mode Matters&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rapid Deployment:&lt;/strong&gt; From image to HTTPS endpoint instantly, bypassing manual setup.  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simplified Configurations:&lt;/strong&gt; Minimal inputs needed. AWS sets up production-ready infrastructure.  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built for Developers:&lt;/strong&gt; Especially friendly for those new to containers or needing speedy iterations.  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fully Managed:&lt;/strong&gt; Auto scaling, logging, monitoring, and security come out of the box.  &lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-i-used-ecs-express-mode&quot;&gt;How I Used ECS Express Mode&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5anjukuoiw9hex506mgh.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Provide the container image URI from ECR.  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rk0l130we7j2axz0ekuz.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create or confirm the required IAM roles (task execution and infrastructure). The console can generate these automatically. &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fh0f87qoxknuous8tw4c.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Launch the deployment and get an auto-managed HTTPS endpoint with ALB in minutes.  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1a4qckynv416m5x2f78f.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fqgs0lrbged9wuba7923.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We can what are resources created for us in the &lt;strong&gt;timeline view&lt;br&gt;
&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wb95gint6vyg6jpm9fn2.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We can see what are resources created for us in &lt;strong&gt;table view&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8057ua9dyqh5wy2lwsjk.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;we can also update the express service setup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ozear94m06nqo68jbh28.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;By Default our deployment stratgey set to canary &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/15tpb553iao9btxrckbu.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check your website is up or not &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tjhzahw4hhqwpf5ii0oz.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It is created everything for me , it is so cool.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2bv9ojeisuypdbottcsi.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Just one click , kaboom everything can be easily destroyed .&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wwx03d0dvuvy1s4jhh64.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;lets-now-explore-the-additional-configuration-option&quot;&gt;Lets now explore the additional configuration option&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;We can select where this express mode task needs to be scheduled like in which cluster , you can take care about &lt;strong&gt;containerPort&lt;/strong&gt; and &lt;strong&gt;health check&lt;/strong&gt;.&lt;br&gt;
You can see there is env variables options where you can pass you variables from your &lt;strong&gt;secrets manager or ssm parameter store&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xxr4ui8l7042wfl023ja.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We can pass our Docker CMD commands here . In compute options we can able to set how many vCpus we need and memory . Task scaling etc&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b57315y7gjmo3il8l1n9.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cy67j63irp2ilhktyt2r.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;we can customize our networking like if you have already available VPC, SG etc . if we dont enable , express mode creates them for us by default&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gk053m8ynxpnhgs7lgie.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;we name our cloudwatchlog group name and prefix , if not it will generated automatically&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pocv1g4c6ifymnu7ltwn.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;lets deploy it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/320plremb25y6qo5wwxw.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;iam-roles-needed&quot;&gt;IAM Roles Needed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Task Execution Role:&lt;/strong&gt; Allows ECS to pull images, write logs, and retrieve secrets.  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Infrastructure Role:&lt;/strong&gt; Lets ECS manage ALB, networking, and scaling.  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AWS can auto-create these roles with recommended permissions, easing onboarding for new users.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;ending-note&quot;&gt;Ending Note&lt;/h3&gt;
&lt;p&gt;ECS Express Mode represents a huge win for developers and teams who want to focus on building applications, not wrestling with infrastructure. It delivers container deployments at full pace—bringing production-ready, scalable applications live with minimal setup and maximum developer productivity. &lt;/p&gt;
&lt;p&gt;Whether you're prototyping or launching at scale, Express Mode simplifies the journey from container image to a fully managed HTTPS endpoint. This is the future of container deployment on AWS ECS—fast, secure, and developer-friendly.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/aquavis12/ecs-ecr-demo-2025-expressmode&quot;&gt;Github Url : https://github.com/aquavis12/ecs-ecr-demo-2025-expressmode&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Deploy highly available, scalable containerized applications with simplified configurations. ECS Express Mode takes care of the rest.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13189/from-image-to-https-endpoint-in-one-step-with-ecs-express-mode</guid>
<pubDate>Thu, 19 Mar 2026 11:29:16 +0000</pubDate>
</item>
<item>
<title>Leveraging Laravel with Docker for Streamlined Development</title>
<link>https://coderlegion.com/12501/leveraging-laravel-with-docker-for-streamlined-development</link>
<description>&lt;p&gt;Explore how to integrate Docker into your Laravel development workflow for enhanced efficiency and consistency.&lt;/p&gt;
&lt;p&gt;Setting up a development environment can be challenging. Using Docker with Laravel can simplify this process remarkably. Here’s how to get started:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Install Docker&lt;/strong&gt;: Ensure you have Docker installed on your machine. Follow the &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.docker.com/get-docker/&quot;&gt;official Docker documentation&lt;/a&gt; for your operating system.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a Dockerfile&lt;/strong&gt;: In your Laravel project, create a &lt;code&gt;Dockerfile&lt;/code&gt; to define your application environment. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM php:8.0-fpm
WORKDIR /var/www
COPY . .
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y libpng-dev libjpeg-dev libfreetype6-dev &amp;amp;&amp;amp; docker-php-ext-configure gd --with-freetype --with-jpeg &amp;amp;&amp;amp; docker-php-ext-install gd
CMD php artisan serve --host=0.0.0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example sets up PHP with GD support, essential for image processing in Laravel applications.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a docker-compose.yml file&lt;/strong&gt;: This file will manage your multi-container application. For Laravel, it might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &quot;3.8&quot;
services:
  app:
    build:
      context: .
    volumes:
      - .:/var/www
    ports:
      - &quot;8000:8000&quot;
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: laravel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above example sets up a MySQL database alongside your Laravel app.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run the containers&lt;/strong&gt;: Execute the following command in your terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker-compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command will bring up your Laravel application and your database as defined in the &lt;code&gt;docker-compose.yml&lt;/code&gt; file.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Access your application&lt;/strong&gt;: You can now access your Laravel application running on &lt;code&gt;http://localhost:8000&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;: By using Docker, you ensure a consistent development environment across different setups, leading to fewer deployment headaches. It’s a game-changer for collaborative projects where each developer may have different local setups.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Take Action&lt;/strong&gt;: Try integrating Docker into your Laravel project today and notice how it changes your workflow for the better.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/12501/leveraging-laravel-with-docker-for-streamlined-development</guid>
<pubDate>Mon, 09 Mar 2026 19:44:13 +0000</pubDate>
</item>
</channel>
</rss>