<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: MICHAEL ABAJEMITO</title>
    <description>The latest articles on DEV Community by MICHAEL ABAJEMITO (@mikegbenga).</description>
    <link>https://dev.to/mikegbenga</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3829516%2Face5eedb-4ef6-40d1-a0a1-645d03fff85f.jpg</url>
      <title>DEV Community: MICHAEL ABAJEMITO</title>
      <link>https://dev.to/mikegbenga</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mikegbenga"/>
    <language>en</language>
    <item>
      <title>Jenkins Remoting: Builds on Remote Agents, Two CPU Architectures, Isolated Nodes (CodeAlpha DevOps Internship, Task 2)</title>
      <dc:creator>MICHAEL ABAJEMITO</dc:creator>
      <pubDate>Mon, 09 Nov 2026 16:22:09 +0000</pubDate>
      <link>https://dev.to/mikegbenga/jenkins-remoting-builds-on-remote-agents-two-cpu-architectures-isolated-nodes-codealpha-devops-14i8</link>
      <guid>https://dev.to/mikegbenga/jenkins-remoting-builds-on-remote-agents-two-cpu-architectures-isolated-nodes-codealpha-devops-14i8</guid>
      <description>&lt;p&gt;Last project from my CodeAlpha DevOps internship. The task was to set up Jenkins Remoting, which is how Jenkins runs builds on other machines (agents), spread the work across them, run jobs on different CPU architectures, and use node isolation for security.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/mikegbenga/CodeAlpha_JenkinsRemoting" rel="noopener noreferrer"&gt;github.com/mikegbenga/CodeAlpha_JenkinsRemoting&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;The main Jenkins server (the &lt;strong&gt;controller&lt;/strong&gt;) never runs a build itself. Everything goes to agents. If a build runs on the controller, it can read everything in &lt;code&gt;JENKINS_HOME&lt;/code&gt;, including every credential, so keeping builds off it is one of the easiest security wins.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0x1nct303z7aw7rh4axp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0x1nct303z7aw7rh4axp.png" alt="Layout: the controller in the middle, two WebSocket agents (amd64 and arm64), an SSH agent on its own network, and a real remote machine" width="800" height="346"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;How it connects&lt;/th&gt;
&lt;th&gt;CPU&lt;/th&gt;
&lt;th&gt;Network&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;agent-linux-amd64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Agent dials in over WebSocket&lt;/td&gt;
&lt;td&gt;x86_64&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ci-inbound&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;agent-linux-arm64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Agent dials in over WebSocket&lt;/td&gt;
&lt;td&gt;aarch64&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ci-inbound&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;agent-ssh-secure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Controller connects over SSH&lt;/td&gt;
&lt;td&gt;x86_64&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ci-secure&lt;/code&gt; (no internet)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;remote-vm-01&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Real machine, dials in over WebSocket&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;anywhere it can reach the controller&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How Jenkins Remoting works (short version)
&lt;/h2&gt;

&lt;p&gt;Remoting is the library behind the controller–agent connection. It opens a two-way channel, over WebSocket, TCP or an SSH session. Over that channel the controller sends the agent bits of work to run (&lt;code&gt;sh&lt;/code&gt;, &lt;code&gt;checkout&lt;/code&gt;, file operations), the agent streams the console output and files back, and the agent downloads any Java classes it needs from the controller.&lt;/p&gt;

&lt;p&gt;The agent side is &lt;code&gt;agent.jar&lt;/code&gt;. It has to match the controller's version, so the installer always downloads it from &lt;code&gt;&amp;lt;JENKINS_URL&amp;gt;/jnlpJars/agent.jar&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For agents that dial in (inbound agents):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The node is defined on the controller, and Jenkins generates a &lt;strong&gt;secret&lt;/strong&gt; for it.&lt;/li&gt;
&lt;li&gt;The agent starts with &lt;code&gt;-url&lt;/code&gt;, &lt;code&gt;-name&lt;/code&gt;, &lt;code&gt;-secret&lt;/code&gt; and &lt;code&gt;-webSocket&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It connects to the controller's normal web port, and the controller checks the secret.&lt;/li&gt;
&lt;li&gt;The node goes online. If the connection drops, the agent reconnects by itself.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The nice thing about WebSocket is that the agent only needs to reach the controller's web port. The agent machine doesn't need any open ports, so it works fine behind NAT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything is configured as code
&lt;/h2&gt;

&lt;p&gt;I didn't click anything in the Jenkins UI. The controller image installs the plugins, and &lt;strong&gt;Jenkins Configuration as Code (JCasC)&lt;/strong&gt; sets up security, nodes and credentials when it starts. The demo jobs are created by &lt;strong&gt;Job DSL&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; jenkins/jenkins:lts-jdk17&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; JAVA_OPTS="-Djenkins.install.runSetupWizard=false" \&lt;/span&gt;
    CASC_JENKINS_CONFIG=/usr/share/jenkins/casc
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; plugins.txt /usr/share/jenkins/ref/plugins.txt&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;jenkins-plugin-cli &lt;span class="nt"&gt;--plugin-file&lt;/span&gt; /usr/share/jenkins/ref/plugins.txt
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; casc/      /usr/share/jenkins/casc/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pipelines/ /usr/share/jenkins/pipelines/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important parts of &lt;code&gt;casc/jenkins.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jenkins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;numExecutors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;          &lt;span class="c1"&gt;# the controller never builds&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EXCLUSIVE&lt;/span&gt;
  &lt;span class="na"&gt;remotingSecurity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;          &lt;span class="c1"&gt;# agents can't read or write controller files&lt;/span&gt;
  &lt;span class="na"&gt;securityRealm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;local&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;allowsSignup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${JENKINS_ADMIN_ID}"&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${JENKINS_ADMIN_PASSWORD}"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${JENKINS_DEV_ID:-developer}"&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${JENKINS_DEV_PASSWORD}"&lt;/span&gt;
  &lt;span class="na"&gt;authorizationStrategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;globalMatrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;entries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${JENKINS_ADMIN_ID}"&lt;/span&gt;
            &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Overall/Administer"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${JENKINS_DEV_ID:-developer}"&lt;/span&gt;
            &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Overall/Read"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Job/Read"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Job/Build"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Job/Cancel"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;nodes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;permanent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-linux-arm64"&lt;/span&gt;
        &lt;span class="na"&gt;labelString&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;linux&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;arm64&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;aarch64&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inbound&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;docker"&lt;/span&gt;
        &lt;span class="na"&gt;remoteFS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/jenkins/agent"&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EXCLUSIVE&lt;/span&gt;    &lt;span class="c1"&gt;# only jobs that ask for these labels&lt;/span&gt;
        &lt;span class="na"&gt;launcher&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;inbound&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;webSocket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;permanent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-ssh-secure"&lt;/span&gt;
        &lt;span class="na"&gt;labelString&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;linux&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;amd64&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssh&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secure-zone"&lt;/span&gt;
        &lt;span class="na"&gt;launcher&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ssh&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-ssh"&lt;/span&gt;
            &lt;span class="na"&gt;credentialsId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-ssh-key"&lt;/span&gt;
            &lt;span class="na"&gt;javaPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/opt/java/openjdk/bin/java"&lt;/span&gt;
            &lt;span class="na"&gt;sshHostKeyVerificationStrategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;manuallyTrustedKeyVerificationStrategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;requireInitialManualTrust&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the SSH key the controller uses to start the SSH agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;domainCredentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;basicSSHUserPrivateKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SYSTEM&lt;/span&gt;        &lt;span class="c1"&gt;# Jenkins can use it, pipelines can't&lt;/span&gt;
              &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-ssh-key"&lt;/span&gt;
              &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jenkins"&lt;/span&gt;
              &lt;span class="na"&gt;privateKeySource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;directEntry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;privateKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${readFile:/run/secrets/agent_ssh_key}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Things I had to figure out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The SSH agent image puts Java at &lt;code&gt;/opt/java/openjdk&lt;/code&gt;, and an SSH session doesn't load the normal &lt;code&gt;PATH&lt;/code&gt;. Without &lt;code&gt;javaPath&lt;/code&gt; the agent fails with &lt;code&gt;java: not found&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The inbound agent secret can't be set in advance. Jenkins generates it from the node name and the controller's key, so the setup script reads it from the controller after Jenkins starts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Network isolation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jenkins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ci-inbound&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;ci-secure&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# only the controller is on both&lt;/span&gt;

  &lt;span class="na"&gt;agent-amd64&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jenkins/inbound-agent:latest-jdk17&lt;/span&gt;
    &lt;span class="na"&gt;cap_drop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ALL&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;security_opt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;no-new-privileges&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1g&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pids&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;512&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ci-inbound&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;agent-ssh&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jenkins/ssh-agent:latest-jdk17&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ci-secure&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ci-inbound&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
  &lt;span class="na"&gt;ci-secure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
    &lt;span class="na"&gt;internal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;    &lt;span class="c1"&gt;# no route to the internet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agents in different zones can't even resolve each other's names, and the secure zone has no internet access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running on two CPU architectures
&lt;/h2&gt;

&lt;p&gt;The arm64 agent is the same image with &lt;code&gt;platform: linux/arm64&lt;/code&gt;. On an ARM machine (Apple Silicon, Raspberry Pi, AWS Graviton) it runs natively. On an x86 machine you enable emulation once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--privileged&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; tonistiigi/binfmt &lt;span class="nt"&gt;--install&lt;/span&gt; arm64
docker compose &lt;span class="nt"&gt;--profile&lt;/span&gt; arm64 up &lt;span class="nt"&gt;-d&lt;/span&gt; agent-arm64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Emulated builds are slower (roughly 5–10×), but it's enough to show the same job running on both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo jobs
&lt;/h2&gt;

&lt;p&gt;Job DSL creates four pipelines in a folder:&lt;/p&gt;

&lt;h3&gt;
  
  
  01: Multi-architecture build
&lt;/h3&gt;

&lt;p&gt;The same Java program gets compiled and run on three agents at once. Each one prints &lt;code&gt;os.arch&lt;/code&gt;, so you can see where it actually ran. Then the results are gathered on one agent and archived.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Build on every architecture'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;parallel&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'x86_64 (inbound WebSocket)'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="s1"&gt;'linux &amp;amp;&amp;amp; amd64 &amp;amp;&amp;amp; inbound'&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;buildAndRun&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'amd64'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'arm64 (inbound WebSocket)'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="s1"&gt;'linux &amp;amp;&amp;amp; arm64'&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;buildAndRun&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'arm64'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'x86_64 (SSH, secure zone)'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="s1"&gt;'ssh &amp;amp;&amp;amp; secure-zone'&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;buildAndRun&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'amd64-ssh'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;stash&lt;/code&gt; and &lt;code&gt;unstash&lt;/code&gt; move the built files between agents. That traffic goes through the Remoting channel too.&lt;/p&gt;

&lt;h3&gt;
  
  
  02: Load distribution
&lt;/h3&gt;

&lt;p&gt;The job splits work into N shards (6 by default). Each shard asks for any agent with the &lt;code&gt;linux&lt;/code&gt; label, and Jenkins spreads them across whatever's free. At the end it prints a table of which node ran which shard.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;shards&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
    &lt;span class="n"&gt;branches&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"shard-${n}"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AGENT_LABEL&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s2"&gt;"echo shard-${n} on \$NODE_NAME (\$(uname -m))"&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;parallel&lt;/span&gt; &lt;span class="n"&gt;branches&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  03: Node isolation audit
&lt;/h3&gt;

&lt;p&gt;This job runs on every agent and &lt;strong&gt;fails&lt;/strong&gt; if something isn't isolated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The build runs as non-root&lt;/li&gt;
&lt;li&gt;The controller's &lt;code&gt;/var/jenkins_home&lt;/code&gt; isn't visible&lt;/li&gt;
&lt;li&gt;The controller's SSH key isn't on the agent&lt;/li&gt;
&lt;li&gt;No effective Linux capabilities&lt;/li&gt;
&lt;li&gt;Agents in one zone can't resolve agents in the other zone&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  04: Remote machine build
&lt;/h3&gt;

&lt;p&gt;This runs on &lt;code&gt;remote-vm-01&lt;/code&gt;, a real machine outside Docker. It prints the host's OS, CPU, memory and Java version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting a real remote machine
&lt;/h2&gt;

&lt;p&gt;On the controller host, get the node's secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/fetch-agent-secrets.sh remote-vm-01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the remote Linux machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;&lt;span class="nv"&gt;JENKINS_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://&amp;lt;controller-ip&amp;gt;:8080 &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nv"&gt;AGENT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;remote-vm-01 &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nv"&gt;AGENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;secret&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
     ./remote-node/install-agent.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer creates a &lt;code&gt;jenkins-agent&lt;/code&gt; user with no login shell, downloads &lt;code&gt;agent.jar&lt;/code&gt; from the controller, saves the secret in a file readable only by that user (&lt;code&gt;-secret @file&lt;/code&gt;, so it never shows up in &lt;code&gt;ps&lt;/code&gt;), and runs it as a locked-down systemd service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;jenkins-agent&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/java -jar /etc/jenkins-agent/agent.jar &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s"&gt;-url ${JENKINS_URL} -name ${AGENT_NAME} &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s"&gt;-secret @/etc/jenkins-agent/secret -workDir ${WORKDIR} -webSocket&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="py"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;ProtectSystem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;
&lt;span class="py"&gt;ProtectHome&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;ReadWritePaths&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/lib/jenkins-agent&lt;/span&gt;
&lt;span class="py"&gt;CapabilityBoundingSet&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="py"&gt;MemoryMax&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2G&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also &lt;code&gt;install-agent.ps1&lt;/code&gt; for Windows, which runs the agent as a scheduled task at startup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/setup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The setup script:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creates an SSH key pair and a &lt;code&gt;.env&lt;/code&gt; with random passwords&lt;/li&gt;
&lt;li&gt;Builds and starts the controller and the SSH agent&lt;/li&gt;
&lt;li&gt;Waits for Jenkins to be healthy, then reads the inbound agent secrets&lt;/li&gt;
&lt;li&gt;Starts the amd64 agent, and the arm64 one if emulation is available&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then &lt;code&gt;./scripts/status.sh&lt;/code&gt; lists every node with its state, executors, architecture and labels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Controller has 0 executors&lt;/td&gt;
&lt;td&gt;Builds can't read controller secrets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent → controller access control&lt;/td&gt;
&lt;td&gt;A compromised agent can't touch controller files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exclusive nodes + labels&lt;/td&gt;
&lt;td&gt;Sensitive jobs only run in &lt;code&gt;secure-zone&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Separate networks, &lt;code&gt;ci-secure&lt;/code&gt; has no internet&lt;/td&gt;
&lt;td&gt;Zones can't reach each other&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-root agents, &lt;code&gt;cap_drop: ALL&lt;/code&gt;, resource limits&lt;/td&gt;
&lt;td&gt;Limits damage from a bad build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets from files, not the command line&lt;/td&gt;
&lt;td&gt;Not visible in &lt;code&gt;ps&lt;/code&gt; or logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SYSTEM-scoped SSH key&lt;/td&gt;
&lt;td&gt;Pipelines can't print it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Matrix authorization, no signup&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;developer&lt;/code&gt; can only read, build and cancel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sandboxed pipeline scripts&lt;/td&gt;
&lt;td&gt;Groovy runs under Script Security&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inbound agent: &lt;code&gt;Incorrect secret&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The controller volume was reset; run &lt;code&gt;fetch-agent-secrets.sh&lt;/code&gt; again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent tries &lt;code&gt;tcpSlaveAgentListener&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;JENKINS_WEB_SOCKET=true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Unknown launcher inbound&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Older Jenkins; rename &lt;code&gt;inbound:&lt;/code&gt; to &lt;code&gt;jnlp:&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSH agent: &lt;code&gt;Host key verification failed&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The container was recreated; trust the new host key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;arm64: &lt;code&gt;exec format error&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Install binfmt (command above)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job stuck "waiting for executor"&lt;/td&gt;
&lt;td&gt;No online agent has that label; check &lt;code&gt;status.sh&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keeping builds off the controller is easy and makes a big difference.&lt;/li&gt;
&lt;li&gt;WebSocket agents make connecting remote machines simple, even behind NAT.&lt;/li&gt;
&lt;li&gt;Labels are what make spreading the load practical.&lt;/li&gt;
&lt;li&gt;Having the whole setup in code means I can tear it down and rebuild it in a few minutes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the end of this series. Thanks for reading, and thanks to &lt;a href="https://www.codealpha.tech" rel="noopener noreferrer"&gt;CodeAlpha&lt;/a&gt; for the internship.&lt;/p&gt;

</description>
      <category>jenkins</category>
      <category>devops</category>
      <category>docker</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Running a Web Server in Docker the Right Way (CodeAlpha DevOps Internship, Task 4)</title>
      <dc:creator>MICHAEL ABAJEMITO</dc:creator>
      <pubDate>Mon, 09 Nov 2026 16:22:02 +0000</pubDate>
      <link>https://dev.to/mikegbenga/running-a-web-server-in-docker-the-right-way-codealpha-devops-internship-task-4-h2</link>
      <guid>https://dev.to/mikegbenga/running-a-web-server-in-docker-the-right-way-codealpha-devops-internship-task-4-h2</guid>
      <description>&lt;p&gt;Second project from my CodeAlpha DevOps internship. The task was to deploy and manage a web server inside Docker, learn the container lifecycle, and monitor the container's health.&lt;/p&gt;

&lt;p&gt;Getting NGINX running in Docker is one command. So I tried to set it up the way you'd want it in production: not running as root, locked down, with a proper health check, and able to recover on its own if it crashes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/mikegbenga/CodeAlpha_DockerWebServer" rel="noopener noreferrer"&gt;github.com/mikegbenga/CodeAlpha_DockerWebServer&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl0wsis6cakhmra577lpw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl0wsis6cakhmra577lpw.png" alt="Diagram: browser to the NGINX container on port 8080, Docker health check probing /healthz, exporter and Prometheus for monitoring" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An NGINX container based on &lt;code&gt;nginx-unprivileged&lt;/code&gt;, running as user 101&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;/healthz&lt;/code&gt; endpoint that Docker checks every 15 seconds&lt;/li&gt;
&lt;li&gt;A read-only filesystem, no Linux capabilities, and CPU and memory limits&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;manage.sh&lt;/code&gt;, a script for the whole container lifecycle, with tests and a crash test&lt;/li&gt;
&lt;li&gt;Optional monitoring with the NGINX Prometheus exporter and Prometheus&lt;/li&gt;
&lt;li&gt;GitHub Actions that lint, build, run, test and scan the image on every push&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the page it serves. The status line at the bottom calls &lt;code&gt;/healthz&lt;/code&gt; from the browser and shows the request ID that NGINX adds to every response.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzh8snrwju8eese3e96xj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzh8snrwju8eese3e96xj.png" alt="The site served from the container, showing a live health status line" width="799" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dockerfile
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; nginxinc/nginx-unprivileged:1.27-alpine&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; 0&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/nginx/conf.d/default.conf
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=nginx:nginx nginx/nginx.conf      /etc/nginx/nginx.conf&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=nginx:nginx nginx/conf.d/         /etc/nginx/conf.d/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=nginx:nginx site/                 /usr/share/nginx/html/&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; 101&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;
&lt;span class="k"&gt;STOPSIGNAL&lt;/span&gt;&lt;span class="s"&gt; SIGQUIT&lt;/span&gt;

&lt;span class="k"&gt;HEALTHCHECK&lt;/span&gt;&lt;span class="s"&gt; --interval=15s --timeout=3s --start-period=5s --retries=3 \&lt;/span&gt;
  CMD ["wget", "-q", "--spider", "http://127.0.0.1:8080/healthz"]

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["nginx", "-g", "daemon off;"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each part is for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;nginx-unprivileged&lt;/code&gt;&lt;/strong&gt; listens on 8080 and runs as a normal user. A non-root user can't bind to ports below 1024, which is why it's 8080 and not 80.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RUN nginx -t&lt;/code&gt;&lt;/strong&gt; checks the config during the build. A typo fails the build instead of the deploy. This saved me more than once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;STOPSIGNAL SIGQUIT&lt;/code&gt;&lt;/strong&gt; makes &lt;code&gt;docker stop&lt;/code&gt; shut NGINX down gracefully, so requests in progress get to finish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HEALTHCHECK&lt;/code&gt;&lt;/strong&gt; is what makes &lt;code&gt;docker ps&lt;/code&gt; show &lt;code&gt;(healthy)&lt;/code&gt; or &lt;code&gt;(unhealthy)&lt;/code&gt; instead of just "Up".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Running it safely (docker-compose.yml)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;codealpha/webserver:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wget"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-q"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--spider"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:8080/healthz"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="na"&gt;read_only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;tmpfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/tmp:size=16m&lt;/span&gt;
    &lt;span class="na"&gt;cap_drop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ALL&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;security_opt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;no-new-privileges:true&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.50"&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;128M&lt;/span&gt;
    &lt;span class="na"&gt;logging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;json-file&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;max-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10m"&lt;/span&gt;
        &lt;span class="na"&gt;max-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;read_only: true&lt;/code&gt;: nothing in the container can be changed, even if someone gets in.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tmpfs: /tmp&lt;/code&gt;: NGINX still needs somewhere to write temp files, so I pointed all its temp paths at &lt;code&gt;/tmp&lt;/code&gt; in &lt;code&gt;nginx.conf&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cap_drop: [ALL]&lt;/code&gt; and &lt;code&gt;no-new-privileges&lt;/code&gt;: the process gets no special Linux permissions and can't gain any.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;deploy.resources.limits&lt;/code&gt;: one container can't eat the whole server.&lt;/li&gt;
&lt;li&gt;Log rotation, so &lt;code&gt;docker logs&lt;/code&gt; can't fill the disk.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The NGINX config
&lt;/h2&gt;

&lt;p&gt;Main settings in &lt;code&gt;nginx.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;pid&lt;/span&gt; &lt;span class="n"&gt;/tmp/nginx.pid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;client_body_temp_path&lt;/span&gt; &lt;span class="n"&gt;/tmp/client_temp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;proxy_temp_path&lt;/span&gt;       &lt;span class="n"&gt;/tmp/proxy_temp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;log_format&lt;/span&gt; &lt;span class="s"&gt;json&lt;/span&gt; &lt;span class="s"&gt;escape=json&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kn"&gt;"time":"&lt;/span&gt;&lt;span class="nv"&gt;$time_iso8601&lt;/span&gt;&lt;span class="s"&gt;","request_id":"&lt;/span&gt;&lt;span class="nv"&gt;$request_id&lt;/span&gt;&lt;span class="s"&gt;",'&lt;/span&gt;
                            &lt;span class="s"&gt;'"method":"&lt;/span&gt;&lt;span class="nv"&gt;$request_method&lt;/span&gt;&lt;span class="s"&gt;","uri":"&lt;/span&gt;&lt;span class="nv"&gt;$request_uri&lt;/span&gt;&lt;span class="s"&gt;",'&lt;/span&gt;
                            &lt;span class="s"&gt;'"status":&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s"&gt;,"request_time":&lt;/span&gt;&lt;span class="nv"&gt;$request_time&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="n"&gt;/dev/stdout&lt;/span&gt; &lt;span class="s"&gt;json&lt;/span&gt; &lt;span class="s"&gt;if=&lt;/span&gt;&lt;span class="nv"&gt;$loggable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;# health checks are not logged&lt;/span&gt;

&lt;span class="kn"&gt;server_tokens&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;gzip&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;limit_req_zone&lt;/span&gt; &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt; &lt;span class="s"&gt;zone=perip:10m&lt;/span&gt; &lt;span class="s"&gt;rate=20r/s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the server block in &lt;code&gt;conf.d/site.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt;        &lt;span class="s"&gt;"DENY"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Request-ID&lt;/span&gt;           &lt;span class="nv"&gt;$request_id&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/healthz&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;default_type&lt;/span&gt; &lt;span class="nc"&gt;application/json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kn"&gt;"status":"ok","server":"nginx"&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/nginx_status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;stub_status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;allow&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="s"&gt;.0.0/8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;allow&lt;/span&gt; &lt;span class="mf"&gt;172.16&lt;/span&gt;&lt;span class="s"&gt;.0.0/12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;allow&lt;/span&gt; &lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="s"&gt;.0.0/16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;deny&lt;/span&gt;  &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;/\.&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# .git, .env and other hidden files&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also a custom 404 page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff5g9oi9fpnb258lzl5ik.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff5g9oi9fpnb258lzl5ik.png" alt="Custom 404 page" width="799" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One thing that tripped me up: I originally had &lt;code&gt;listen [::]:8080;&lt;/code&gt; as well. On a machine with IPv6 turned off, NGINX refused to start with &lt;code&gt;Address family not supported by protocol&lt;/code&gt;. I removed that line.&lt;/p&gt;

&lt;p&gt;Another one: in NGINX, if a &lt;code&gt;location&lt;/code&gt; block has its own &lt;code&gt;add_header&lt;/code&gt;, it &lt;strong&gt;replaces&lt;/strong&gt; the headers from the server block instead of adding to them. So the security headers have to be repeated in those locations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking it works
&lt;/h2&gt;

&lt;p&gt;I ran the config on a real NGINX and checked it with curl:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu6li6k8g688hb2mvn9jx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu6li6k8g688hb2mvn9jx.png" alt="curl checks: 200 for pages, 404 for missing pages and .env, health JSON, security headers, gzip" width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The lifecycle script
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;scripts/manage.sh&lt;/code&gt; wraps the Docker commands so the whole lifecycle is one script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/manage.sh build       &lt;span class="c"&gt;# build the image&lt;/span&gt;
./scripts/manage.sh run         &lt;span class="c"&gt;# docker run with all the hardening flags, then wait until healthy&lt;/span&gt;
./scripts/manage.sh status      &lt;span class="c"&gt;# status, restart count, OOMKilled, exit code&lt;/span&gt;
./scripts/manage.sh health      &lt;span class="c"&gt;# health state + last 5 probe results&lt;/span&gt;
./scripts/manage.sh logs        &lt;span class="c"&gt;# follow logs&lt;/span&gt;
./scripts/manage.sh stats       &lt;span class="c"&gt;# CPU and memory against the limits&lt;/span&gt;
./scripts/manage.sh stop        &lt;span class="c"&gt;# graceful stop&lt;/span&gt;
./scripts/manage.sh &lt;span class="nb"&gt;test&lt;/span&gt;        &lt;span class="c"&gt;# 8 functional + security checks&lt;/span&gt;
./scripts/manage.sh crash-test  &lt;span class="c"&gt;# kill the server and check it comes back&lt;/span&gt;
./scripts/manage.sh clean       &lt;span class="c"&gt;# remove container and image&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;test&lt;/code&gt; command checks the pages, the health endpoint, the 404 page, that &lt;code&gt;.env&lt;/code&gt; is blocked, the security headers, that the root filesystem is read-only, and that it's not running as root.&lt;/p&gt;

&lt;h3&gt;
  
  
  The crash test
&lt;/h3&gt;

&lt;p&gt;This was the most interesting part for me. My first idea was &lt;code&gt;docker exec … kill -9 1&lt;/code&gt;. It didn't work, because PID 1 inside a container ignores &lt;code&gt;SIGKILL&lt;/code&gt; sent from inside its own namespace. Asking NGINX itself to stop works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;codealpha-web nginx &lt;span class="nt"&gt;-s&lt;/span&gt; stop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container exits, the &lt;code&gt;unless-stopped&lt;/code&gt; restart policy starts it again, and the restart count goes up. Then the script waits until it's healthy again.&lt;/p&gt;

&lt;p&gt;One more thing worth knowing: Docker on its own &lt;strong&gt;does not restart a container just because it's unhealthy&lt;/strong&gt;. It only restarts containers that exit. That's why there's also &lt;code&gt;scripts/health-monitor.sh&lt;/code&gt;, which restarts the container after 3 failed checks in a row. Swarm or Kubernetes would do this for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Container lifecycle cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker build -t codealpha/webserver .&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Layers are cached; &lt;code&gt;nginx -t&lt;/code&gt; runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker run -d --name codealpha-web -p 8080:8080 …&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Created and started&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker inspect -f '{{.State.Health.Status}}' codealpha-web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;starting&lt;/code&gt; → &lt;code&gt;healthy&lt;/code&gt; / &lt;code&gt;unhealthy&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pause&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker pause&lt;/code&gt; / &lt;code&gt;docker unpause&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Processes frozen, port still bound&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker stop -t 10 codealpha-web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SIGQUIT&lt;/code&gt;, then &lt;code&gt;SIGKILL&lt;/code&gt; after 10 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restart&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker restart codealpha-web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stop + start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker rm -f codealpha-web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Container gone; the image stays&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Monitoring
&lt;/h2&gt;



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

&lt;/div&gt;



&lt;p&gt;This also starts the &lt;code&gt;nginx-prometheus-exporter&lt;/code&gt;, which reads &lt;code&gt;/nginx_status&lt;/code&gt;, and Prometheus on port 9090. Prometheus has two alert rules: &lt;code&gt;NginxDown&lt;/code&gt; and too many active connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Every push runs &lt;code&gt;.github/workflows/docker-ci.yml&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F71cgwj2rue36tclx7k2o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F71cgwj2rue36tclx7k2o.png" alt="GitHub Actions run: lint, ShellCheck, build, run, checks, crash test, Trivy scan, all passing" width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To be honest, the first two runs failed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I'd pinned &lt;code&gt;aquasecurity/trivy-action@0.28.0&lt;/code&gt;, which doesn't exist. The releases use a &lt;code&gt;v&lt;/code&gt; prefix, so &lt;code&gt;v0.36.0&lt;/code&gt; works.&lt;/li&gt;
&lt;li&gt;ShellCheck complained about &lt;code&gt;A &amp;amp;&amp;amp; B || C&lt;/code&gt; (SC2015), because if B fails, C runs even though A succeeded. I changed those lines to proper &lt;code&gt;if/else&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Third run passed in 1 minute 9 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;port is already allocated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Something else is on 8080&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HOST_PORT=8081 ./scripts/manage.sh run&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Read-only file system&lt;/code&gt; error&lt;/td&gt;
&lt;td&gt;A temp path isn't on the tmpfs&lt;/td&gt;
&lt;td&gt;Keep the &lt;code&gt;*_temp_path /tmp/...&lt;/code&gt; lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bind() to 0.0.0.0:80 failed (13: Permission denied)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Non-root can't use port 80&lt;/td&gt;
&lt;td&gt;Listen on 8080, map &lt;code&gt;-p 80:8080&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stuck on &lt;code&gt;starting&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The probe can't reach &lt;code&gt;/healthz&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker exec codealpha-web wget -qO- http://127.0.0.1:8080/healthz&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OOMKilled: true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hit the memory limit&lt;/td&gt;
&lt;td&gt;Raise the limit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A health check tells you a lot more than "the container is running".&lt;/li&gt;
&lt;li&gt;Most of the hardening is just a few lines of config.&lt;/li&gt;
&lt;li&gt;Checking the config during the build catches mistakes early.&lt;/li&gt;
&lt;li&gt;You don't really know self-healing works until you crash it on purpose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next in the series: Jenkins Remoting, running builds on other machines.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://www.codealpha.tech" rel="noopener noreferrer"&gt;CodeAlpha&lt;/a&gt; for the internship.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>nginx</category>
      <category>devops</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building a CI/CD Pipeline on Azure (CodeAlpha DevOps Internship, Task 1)</title>
      <dc:creator>MICHAEL ABAJEMITO</dc:creator>
      <pubDate>Mon, 09 Nov 2026 16:21:17 +0000</pubDate>
      <link>https://dev.to/mikegbenga/building-a-cicd-pipeline-on-azure-codealpha-devops-internship-task-1-28c7</link>
      <guid>https://dev.to/mikegbenga/building-a-cicd-pipeline-on-azure-codealpha-devops-internship-task-1-28c7</guid>
      <description>&lt;p&gt;This is the first project from my DevOps internship at CodeAlpha. The task was to build a CI/CD pipeline with Azure tools: Azure Pipelines for the automation, Azure Container Registry (ACR) to store the images, and Azure App Service to run the app.&lt;/p&gt;

&lt;p&gt;I'm writing this down partly so I remember how I did it, and partly in case it helps someone else doing the same thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/mikegbenga/CodeAlpha_AzureCICD" rel="noopener noreferrer"&gt;github.com/mikegbenga/CodeAlpha_AzureCICD&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the pipeline does
&lt;/h2&gt;

&lt;p&gt;Every time I push to &lt;code&gt;main&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The tests run.&lt;/li&gt;
&lt;li&gt;The app gets built into a Docker image.&lt;/li&gt;
&lt;li&gt;The image is pushed to Azure Container Registry, tagged with the build number.&lt;/li&gt;
&lt;li&gt;App Service is updated to run that exact image.&lt;/li&gt;
&lt;li&gt;A smoke test calls the live &lt;code&gt;/health&lt;/code&gt; endpoint until it reports the new build number.&lt;/li&gt;
&lt;li&gt;If that never happens, the pipeline puts the previous image back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pull requests only run the tests and the build. Nothing gets pushed or deployed from a PR.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg418n5vo5tj1o47szw8x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg418n5vo5tj1o47szw8x.png" alt="Architecture diagram: GitHub triggers Azure Pipelines, which pushes to ACR; App Service pulls with a managed identity; App Insights monitors" width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The app
&lt;/h2&gt;

&lt;p&gt;I kept the app small on purpose, because the pipeline is the point here. It's a Node.js server with no dependencies and three routes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;What it returns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A page that shows which build is running&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{"status":"healthy","version":"…"}&lt;/code&gt;, used by App Service and the smoke test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/api/info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Build number, environment, hostname, uptime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is it running on my machine. Locally the version shows &lt;code&gt;dev&lt;/code&gt; and the build ID shows &lt;code&gt;local&lt;/code&gt;. In Azure those get filled in by the pipeline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftjqug6g7m30t05615am8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftjqug6g7m30t05615am8.png" alt="The app's landing page showing the running build details" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also logs every request as one line of JSON, which makes the logs easy to search later in Log Analytics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"t"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-11-09T16:13:19.119Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"/health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;0.41&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tests
&lt;/h3&gt;

&lt;p&gt;There are six tests using Node's built-in test runner, so there's nothing to install. They check the health endpoint, the API, the security headers, a 404, that path traversal is blocked, and that &lt;code&gt;POST&lt;/code&gt; is rejected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ztnycposaqcswteuscc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ztnycposaqcswteuscc.png" alt="Six tests passing locally" width="800" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the pipeline they run with a JUnit reporter so the results show up in the &lt;strong&gt;Tests&lt;/strong&gt; tab in Azure DevOps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"test:ci"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node --test --test-reporter=spec --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=test-results.xml test/*.test.js"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Dockerfile
&lt;/h2&gt;

&lt;p&gt;Two stages. The first one runs the tests, so a broken commit can't even produce an image. The second one is the small image that actually runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:22-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /src&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; app/package.json ./&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; app/src ./src&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; app/public ./public&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; app/test ./test&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;test&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:22-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runtime&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; APP_VERSION=dev&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; BUILD_ID=local&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NODE_ENV=production PORT=3000 APP_VERSION=${APP_VERSION} BUILD_ID=${BUILD_ID}&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=node:node app/package.json ./&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=node:node app/src ./src&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=node:node app/public ./public&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; 1000&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;HEALTHCHECK&lt;/span&gt;&lt;span class="s"&gt; --interval=30s --timeout=3s --start-period=10s --retries=3 \&lt;/span&gt;
  CMD ["wget", "-q", "--spider", "http://127.0.0.1:3000/health"]
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "src/server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A couple of things I learned here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;USER 1000&lt;/code&gt; means the app doesn't run as root inside the container.&lt;/li&gt;
&lt;li&gt;The build number gets passed in as a build arg, so every image knows which build it came from. That's what makes the smoke test possible later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The pipeline (&lt;code&gt;azure-pipelines.yml&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;It's one YAML file with three stages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 1: CI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UseNode@1&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;22.x'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run test:ci&lt;/span&gt;
      &lt;span class="na"&gt;workingDirectory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PublishTestResults@2&lt;/span&gt;
      &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;succeededOrFailed()&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;testResultsFormat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;JUnit'&lt;/span&gt;
        &lt;span class="na"&gt;testResultsFiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app/test-results.xml'&lt;/span&gt;
        &lt;span class="na"&gt;failTaskOnFailedTests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Stage 2: Build and push to ACR
&lt;/h3&gt;

&lt;p&gt;The image gets two tags: the build ID (which never changes) and &lt;code&gt;latest&lt;/code&gt;. Before pushing, the pipeline starts the container on the build agent and checks &lt;code&gt;/health&lt;/code&gt; once, just to make sure it actually starts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Docker@2&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;image'&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
    &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(imageRepository)&lt;/span&gt;
    &lt;span class="na"&gt;containerRegistry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(dockerRegistryServiceConnection)&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;$(imageTag)&lt;/span&gt;
      &lt;span class="s"&gt;latest&lt;/span&gt;
    &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
      &lt;span class="s"&gt;--build-arg APP_VERSION=1.0.$(Build.BuildId)&lt;/span&gt;
      &lt;span class="s"&gt;--build-arg BUILD_ID=$(Build.BuildId)&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Docker@2&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Push&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ACR'&lt;/span&gt;
  &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;and(succeeded(), eq(variables.isMain, &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="s"&gt;))&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;push&lt;/span&gt;
    &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(imageRepository)&lt;/span&gt;
    &lt;span class="na"&gt;containerRegistry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(dockerRegistryServiceConnection)&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;$(imageTag)&lt;/span&gt;
      &lt;span class="s"&gt;latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Stage 3: Deploy, check, roll back
&lt;/h3&gt;

&lt;p&gt;This was the part that took me the longest to get right. Before deploying, the pipeline saves which image is currently live. Then it deploys the new one and keeps calling &lt;code&gt;/health&lt;/code&gt; until the version matches this build.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AzureWebAppContainer@1&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;azureSubscription&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(azureSubscription)&lt;/span&gt;
    &lt;span class="na"&gt;appName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(webAppName)&lt;/span&gt;
    &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(acrLoginServer)/$(imageRepository):$(imageTag)'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;URL="https://$(webAppName).azurewebsites.net/health"&lt;/span&gt;
    &lt;span class="s"&gt;for i in $(seq 1 30); do&lt;/span&gt;
      &lt;span class="s"&gt;BODY=$(curl -fsS "$URL" || true)&lt;/span&gt;
      &lt;span class="s"&gt;if echo "$BODY" | grep -q '"version":"1.0.$(Build.BuildId)"'; then&lt;/span&gt;
        &lt;span class="s"&gt;echo "Deployment verified"; exit 0&lt;/span&gt;
      &lt;span class="s"&gt;fi&lt;/span&gt;
      &lt;span class="s"&gt;sleep 10&lt;/span&gt;
    &lt;span class="s"&gt;done&lt;/span&gt;
    &lt;span class="s"&gt;exit 1&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Post-deploy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;smoke&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;test'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AzureCLI@2&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Roll&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;back&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;previous&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;image'&lt;/span&gt;
  &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;and(failed(), ne(variables.previousImage, ''))&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;azureSubscription&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(azureSubscription)&lt;/span&gt;
    &lt;span class="na"&gt;scriptType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
    &lt;span class="na"&gt;scriptLocation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inlineScript&lt;/span&gt;
    &lt;span class="na"&gt;inlineScript&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;az webapp config container set -g $(resourceGroup) -n $(webAppName) \&lt;/span&gt;
        &lt;span class="s"&gt;--container-image-name "$(previousImage)"&lt;/span&gt;
      &lt;span class="s"&gt;az webapp restart -g $(resourceGroup) -n $(webAppName)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first I put the rollback in an &lt;code&gt;on: failure:&lt;/code&gt; hook, but variables set during the deploy job weren't available there. Moving it into the same job with &lt;code&gt;condition: failed()&lt;/code&gt; fixed that.&lt;/p&gt;

&lt;p&gt;To test the rollback without actually breaking the app, there's a checkbox when you run the pipeline manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;simulateFailure&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Simulate&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;release&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(demo&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;automatic&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;rollback)'&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;boolean&lt;/span&gt;
  &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When it's ticked, the smoke test fails on purpose and you can watch the rollback step run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The infrastructure (Terraform)
&lt;/h2&gt;

&lt;p&gt;Everything in Azure is created by Terraform in &lt;code&gt;infra/terraform/&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Resource group&lt;/li&gt;
&lt;li&gt;Container registry (Basic, &lt;strong&gt;admin account turned off&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;App Service plan (Linux, B1) and the Web App&lt;/li&gt;
&lt;li&gt;Log Analytics and Application Insights&lt;/li&gt;
&lt;li&gt;Alerts for HTTP 5xx errors and failed health checks, sent to my email&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bit I like most is that the Web App pulls images using a &lt;strong&gt;managed identity&lt;/strong&gt; with the &lt;code&gt;AcrPull&lt;/code&gt; role. So no registry password is stored anywhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_linux_web_app"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;https_only&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SystemAssigned"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;site_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;container_registry_use_managed_identity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;health_check_path&lt;/span&gt;                       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/health"&lt;/span&gt;
    &lt;span class="nx"&gt;minimum_tls_version&lt;/span&gt;                     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1.2"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_role_assignment"&lt;/span&gt; &lt;span class="s2"&gt;"acr_pull"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;scope&lt;/span&gt;                &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_container_registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;role_definition_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AcrPull"&lt;/span&gt;
  &lt;span class="nx"&gt;principal_id&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_linux_web_app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;principal_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you'd rather not use Terraform, there's also &lt;code&gt;scripts/bootstrap-azure.sh&lt;/code&gt;, which creates the same things with the Azure CLI.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to set it up yourself
&lt;/h2&gt;

&lt;p&gt;The full steps are in &lt;a href="https://github.com/mikegbenga/CodeAlpha_AzureCICD/blob/main/docs/SETUP.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/SETUP.md&lt;/code&gt;&lt;/a&gt;. The short version:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create the Azure resources&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   az login
   &lt;span class="nb"&gt;cd &lt;/span&gt;infra/terraform
   &lt;span class="nb"&gt;cp &lt;/span&gt;terraform.tfvars.example terraform.tfvars   &lt;span class="c"&gt;# add your subscription ID and email&lt;/span&gt;
   terraform init &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;In Azure DevOps, create two service connections&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sc-acr-codealpha&lt;/code&gt;: Docker Registry → Azure Container Registry&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sc-azure-codealpha&lt;/code&gt;: Azure Resource Manager (workload identity federation)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create an environment&lt;/strong&gt; called &lt;code&gt;codealpha-production&lt;/code&gt;. You can add an approval step here if you want a manual gate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create the pipeline&lt;/strong&gt; from the existing &lt;code&gt;azure-pipelines.yml&lt;/code&gt; in the repo.&lt;/li&gt;
&lt;li&gt;Push to &lt;code&gt;main&lt;/code&gt; and watch the three stages run.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; new Azure DevOps organisations don't get free hosted build agents straight away. You have to &lt;a href="https://aka.ms/azpipelines-parallelism-request" rel="noopener noreferrer"&gt;request them&lt;/a&gt;, and approval can take a few days. Request it on day one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Monitoring
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline:&lt;/strong&gt; each run shows the stage timings, the Tests tab shows the JUnit results, and the &lt;code&gt;codealpha-production&lt;/code&gt; environment keeps the deployment history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App:&lt;/strong&gt; Application Insights for requests and failures, and Log Analytics for the JSON request logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerts:&lt;/strong&gt; an email if there are more than 5 server errors in 5 minutes, or if the health check drops.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's also a small script, &lt;code&gt;scripts/monitor-pipeline.sh&lt;/code&gt;, that shows the last pipeline runs, the live health, the running image and the log tail in one place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problems I hit
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;node --test test/&lt;/code&gt; didn't find the tests on Node 22&lt;/td&gt;
&lt;td&gt;Pass the files directly: &lt;code&gt;test/*.test.js&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback step couldn't see &lt;code&gt;previousImage&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Move it into the deploy job with &lt;code&gt;condition: failed()&lt;/code&gt; instead of an &lt;code&gt;on: failure&lt;/code&gt; hook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App shows 503 after deploy&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;WEBSITES_PORT&lt;/code&gt; has to be &lt;code&gt;3000&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image pull fails in App Service&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;AcrPull&lt;/code&gt; role is missing, or &lt;code&gt;acrUseManagedIdentityCreds&lt;/code&gt; is off&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Automating every step makes releases a lot less stressful.&lt;/li&gt;
&lt;li&gt;Tagging images with the build number means I always know what's running.&lt;/li&gt;
&lt;li&gt;A health check straight after deploying is what makes an automatic rollback possible.&lt;/li&gt;
&lt;li&gt;Managed identity is nicer than copying registry passwords around.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next up in this series: running a web server in Docker the proper way.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://www.codealpha.tech" rel="noopener noreferrer"&gt;CodeAlpha&lt;/a&gt; for the internship.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>azure</category>
      <category>docker</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Cloud Engineering for Beginners: A Friendly, Plain-English Guide</title>
      <dc:creator>MICHAEL ABAJEMITO</dc:creator>
      <pubDate>Mon, 09 Nov 2026 16:00:42 +0000</pubDate>
      <link>https://dev.to/mikegbenga/cloud-engineering-for-beginners-a-friendly-plain-english-guide-f0b</link>
      <guid>https://dev.to/mikegbenga/cloud-engineering-for-beginners-a-friendly-plain-english-guide-f0b</guid>
      <description>&lt;h2&gt;
  
  
  Let's start with a confession
&lt;/h2&gt;

&lt;p&gt;When I first heard people talk about "the cloud", I pictured something floating in the sky, holding everybody's photos and emails. It sounded mysterious, a bit magical, and honestly a little intimidating. People threw around words like &lt;strong&gt;EC2&lt;/strong&gt;, &lt;strong&gt;Kubernetes&lt;/strong&gt;, &lt;strong&gt;VPC&lt;/strong&gt; and &lt;strong&gt;serverless&lt;/strong&gt; as if everyone was born knowing them.&lt;/p&gt;

&lt;p&gt;Here is the good news: the cloud is not magic, and you do not need to be a genius to understand it. Underneath all the jargon is a simple idea that you already understand from everyday life. By the end of this guide you should be able to explain the cloud to a friend over a cup of tea, know what a cloud engineer actually does all day, and have a clear, realistic path for getting started yourself.&lt;/p&gt;

&lt;p&gt;No prior experience is assumed. If a word sounds technical, I will explain it. Grab a drink, get comfortable, and let's walk through it together.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What is "the cloud", really?
&lt;/h2&gt;

&lt;p&gt;Here is the simplest honest definition you will ever hear:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The cloud is simply other people's computers, which you rent over the internet and pay for only while you use them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it. Somewhere in the world there are huge buildings called &lt;strong&gt;data centres&lt;/strong&gt;, filled with thousands of powerful computers (we call them &lt;strong&gt;servers&lt;/strong&gt;), stacked in rows of metal cabinets, cooled by giant air-conditioning systems and protected by guards, fences and cameras. Companies like Amazon, Microsoft and Google own these buildings. Instead of buying your own servers, you rent a slice of theirs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The electricity analogy
&lt;/h3&gt;

&lt;p&gt;Think about electricity. A hundred years ago, many factories generated their own power with their own generators. They had to buy the machines, hire people to maintain them, buy fuel, and deal with breakdowns. Today, almost nobody does that. You plug into the national grid, use what you need, and pay a bill at the end of the month.&lt;/p&gt;

&lt;p&gt;The cloud does the same thing for computing. Before the cloud, every company had to buy and run its own servers. Now they can "plug in" to a cloud provider and pay for exactly what they use, just like electricity or water.&lt;/p&gt;

&lt;h3&gt;
  
  
  Life before the cloud: the server room story
&lt;/h3&gt;

&lt;p&gt;Imagine you run a growing online shop in 2008. To put your website online, you would have to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Buy physical servers (very expensive, paid upfront).&lt;/li&gt;
&lt;li&gt;Find a cool, secure room to keep them, with backup power in case of an outage.&lt;/li&gt;
&lt;li&gt;Wait weeks for the hardware to be delivered and set up.&lt;/li&gt;
&lt;li&gt;Guess how much traffic you will get. Buy too little, and your site crashes on a busy day. Buy too much, and expensive machines sit idle most of the year.&lt;/li&gt;
&lt;li&gt;Hire people to replace broken disks, apply updates and fix things at 2 a.m.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now imagine the same shop today. You open a website, click a few buttons (or run a short script), and within minutes you have a server running in a data centre in another country. When a big sale brings ten times the usual visitors, the cloud can add more servers automatically. When the rush is over, it removes them, and you stop paying for them. That shift, from &lt;strong&gt;owning&lt;/strong&gt; to &lt;strong&gt;renting on demand&lt;/strong&gt;, is the heart of the cloud revolution.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Remember this: the cloud gives you computing power the way a tap gives you water. Turn it on when you need it, turn it off when you don't, and pay for what flowed through.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  2. So what is cloud engineering?
&lt;/h2&gt;

&lt;p&gt;If the cloud is a giant rental warehouse of computers, then &lt;strong&gt;cloud engineering&lt;/strong&gt; is the skill of designing, building, running and looking after systems inside that warehouse, so that applications are reliable, secure, fast and affordable.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;cloud engineer&lt;/strong&gt; is a bit like a combination of an architect, a builder and a building manager:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;As an architect&lt;/strong&gt;, they plan how an application should be laid out in the cloud: which services to use, how they connect, and how to keep everything safe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;As a builder&lt;/strong&gt;, they actually set things up: servers, networks, databases, storage and permissions, usually using code rather than clicking around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;As a building manager&lt;/strong&gt;, they keep everything running: watching for problems, fixing issues, applying updates, controlling costs and planning for growth.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A day in the life of a cloud engineer
&lt;/h3&gt;

&lt;p&gt;No two days are the same, but a typical day might look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;What they might be doing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Morning&lt;/td&gt;
&lt;td&gt;Check the monitoring dashboards. Did anything break overnight? Are any servers struggling? Is the monthly bill growing faster than expected?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mid-morning&lt;/td&gt;
&lt;td&gt;Join a short team meeting with developers to plan a new feature that needs a database and file storage.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Late morning&lt;/td&gt;
&lt;td&gt;Write code (for example, Terraform) that creates the new database and storage, and submit it for review.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Afternoon&lt;/td&gt;
&lt;td&gt;Improve the automated pipeline that tests and deploys the developers' code to the cloud every time they make a change.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Late afternoon&lt;/td&gt;
&lt;td&gt;Investigate why one service is slow, find a misconfigured setting, fix it, and write a short note so the team learns from it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Occasionally&lt;/td&gt;
&lt;td&gt;Respond to an alert at an awkward hour when something important goes down. (Good engineers work hard to make these rare.)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice something? Very little of this is about touching physical hardware. Cloud engineering is mostly about &lt;strong&gt;thinking clearly, writing configuration and code, automating repetitive work, and solving problems&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related job titles you will hear
&lt;/h3&gt;

&lt;p&gt;The cloud world has many overlapping titles. Don't let them confuse you; they share much of the same foundation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;In simple words&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Engineer&lt;/td&gt;
&lt;td&gt;Builds and runs systems in the cloud. A great general starting point.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Architect&lt;/td&gt;
&lt;td&gt;Designs the big picture: how all the pieces fit together. Usually a more senior role.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DevOps Engineer&lt;/td&gt;
&lt;td&gt;Focuses on automating how software is built, tested and released, often in the cloud.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Site Reliability Engineer (SRE)&lt;/td&gt;
&lt;td&gt;Focuses on keeping services up, fast and reliable, using software engineering skills.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Security Engineer&lt;/td&gt;
&lt;td&gt;Focuses on protecting cloud systems and data from attacks and mistakes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platform Engineer&lt;/td&gt;
&lt;td&gt;Builds internal tools and "paved roads" so developers can use the cloud easily and safely.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  3. Meet the cloud providers
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;cloud provider&lt;/strong&gt; is a company that owns the data centres and rents out computing services. Three names dominate the global market:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Web Services (AWS)&lt;/strong&gt;: the pioneer, launched in 2006, with an enormous catalogue of services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft Azure&lt;/strong&gt;: very popular with businesses that already use Microsoft products such as Windows Server, Office 365 and Active Directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Cloud Platform (GCP)&lt;/strong&gt;: known for strengths in data, analytics, machine learning and Kubernetes (which Google originally created).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are many others too, including Oracle Cloud, IBM Cloud, Alibaba Cloud, DigitalOcean and Linode (now Akamai), as well as regional providers in different countries.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Beginner's tip: don't try to learn all of them at once. Pick one, learn it well, and you will find the others surprisingly familiar. The names differ, but the ideas are the same. A virtual server is called EC2 on AWS, a Virtual Machine on Azure, and Compute Engine on Google Cloud, but it is the same idea in each.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  4. The three ways to use the cloud: IaaS, PaaS and SaaS
&lt;/h2&gt;

&lt;p&gt;You will constantly hear the terms &lt;strong&gt;IaaS&lt;/strong&gt;, &lt;strong&gt;PaaS&lt;/strong&gt; and &lt;strong&gt;SaaS&lt;/strong&gt;. They simply describe how much of the work you do yourself versus how much the provider does for you. The classic way to explain them is with pizza.&lt;/p&gt;

&lt;h3&gt;
  
  
  The pizza analogy
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Pizza version&lt;/th&gt;
&lt;th&gt;Cloud version&lt;/th&gt;
&lt;th&gt;Who does what&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;On-premises (no cloud)&lt;/td&gt;
&lt;td&gt;You make pizza at home: buy flour, make dough, own the oven, cook, serve.&lt;/td&gt;
&lt;td&gt;You own the servers, the building, the network and everything on top.&lt;/td&gt;
&lt;td&gt;You do everything.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IaaS: Infrastructure as a Service&lt;/td&gt;
&lt;td&gt;You rent a kitchen with an oven. You bring ingredients and cook.&lt;/td&gt;
&lt;td&gt;You rent virtual servers, storage and networks. You install and manage the operating system and apps.&lt;/td&gt;
&lt;td&gt;Provider: hardware. You: almost everything else.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PaaS: Platform as a Service&lt;/td&gt;
&lt;td&gt;You order a take-and-bake pizza. It is prepared for you; you just bake and serve.&lt;/td&gt;
&lt;td&gt;You upload your code; the provider runs the servers, operating system and scaling.&lt;/td&gt;
&lt;td&gt;Provider: platform. You: your code and data.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SaaS: Software as a Service&lt;/td&gt;
&lt;td&gt;You order a delivered pizza. Just eat.&lt;/td&gt;
&lt;td&gt;You simply use finished software through a browser, like Gmail, Microsoft 365 or Zoom.&lt;/td&gt;
&lt;td&gt;Provider: everything. You: just use it.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Real examples help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IaaS&lt;/strong&gt;: AWS EC2, Azure Virtual Machines, Google Compute Engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PaaS&lt;/strong&gt;: Azure App Service, AWS Elastic Beanstalk, Google App Engine, Heroku.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SaaS&lt;/strong&gt;: Gmail, Microsoft 365, Salesforce, Dropbox, Zoom.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a cloud engineer, you will mostly work with IaaS and PaaS, and increasingly with &lt;strong&gt;serverless&lt;/strong&gt; services, where you don't think about servers at all. You upload a small piece of code (a "function"), and it runs only when something triggers it, such as a file upload or a web request. You pay per run. AWS Lambda, Azure Functions and Google Cloud Functions are popular examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Where does your cloud live? Public, private, hybrid and multi-cloud
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Public cloud&lt;/strong&gt;: you share the provider's huge infrastructure with other customers (safely separated). This is what most people mean by "the cloud".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private cloud&lt;/strong&gt;: cloud-style technology used by a single organisation, often in its own data centre. Banks and governments sometimes choose this for strict control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid cloud&lt;/strong&gt;: a mix of private (or on-premises) and public cloud working together. For example, a bank keeps its core records in-house but runs its mobile app in the public cloud.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-cloud&lt;/strong&gt;: using more than one public provider, such as AWS for some workloads and Azure for others.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of it like housing: public cloud is renting an apartment in a big, well-managed building; private cloud is owning your own house; hybrid is living in your house but renting a storage unit across town.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The building blocks: the five things almost every system needs
&lt;/h2&gt;

&lt;p&gt;No matter how complicated a cloud system looks, it is built from a handful of basic building blocks. If you understand these five, you understand most of the cloud.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building block 1: Compute (the brains)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Compute&lt;/strong&gt; is processing power, the part that actually runs your programs. In the cloud, compute comes in a few flavours:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Virtual machines (VMs)&lt;/strong&gt;: a full computer created in software, with its own operating system. You choose how much CPU and memory it has. Think of it as renting a whole flat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Containers&lt;/strong&gt;: lightweight packages that hold an application and everything it needs to run, so it behaves the same everywhere. Think of shipping containers: the same box fits on any ship, train or truck. &lt;strong&gt;Docker&lt;/strong&gt; is the most popular tool for creating them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serverless functions&lt;/strong&gt;: tiny pieces of code that run on demand. Think of calling a taxi only when you need a ride, rather than owning a car.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Building block 2: Storage (the memory and filing cabinets)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Object storage&lt;/strong&gt; (like AWS S3 or Azure Blob Storage): perfect for files such as images, videos, backups and documents. Imagine an endless, extremely durable warehouse of labelled boxes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block storage&lt;/strong&gt;: virtual hard drives attached to your virtual machines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File storage&lt;/strong&gt;: shared folders that several machines can use at the same time, like a shared office drive.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Building block 3: Databases (the organised records)
&lt;/h3&gt;

&lt;p&gt;Most applications need to remember things: users, orders, payments, messages. Databases store this information in an organised way.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Relational (SQL) databases&lt;/strong&gt;, such as MySQL, PostgreSQL and Microsoft SQL Server, store data in tables with rows and columns, like a very powerful spreadsheet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NoSQL databases&lt;/strong&gt;, such as MongoDB, DynamoDB and Cosmos DB, are more flexible and are often used for very large or fast-changing data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cloud offers these as &lt;strong&gt;managed databases&lt;/strong&gt;: the provider handles backups, updates and hardware, while you focus on your data. That alone saves teams countless hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building block 4: Networking (the roads and gates)
&lt;/h3&gt;

&lt;p&gt;Networking connects everything together and to the internet, while keeping out what shouldn't get in. A few key ideas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Virtual network (VPC or VNet)&lt;/strong&gt;: your own private, fenced-off area inside the cloud. Think of it as a gated estate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subnets&lt;/strong&gt;: smaller sections inside that estate. Public subnets face the internet (like the reception area); private subnets are hidden (like the back office).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firewalls and security groups&lt;/strong&gt;: the security guards at each gate, deciding who may come in and on which door.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load balancer&lt;/strong&gt;: a friendly receptionist who spreads incoming visitors evenly across several servers, so no single one is overwhelmed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS&lt;/strong&gt;: the internet's phonebook, turning names like example.com into the numeric addresses computers use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDN (Content Delivery Network)&lt;/strong&gt;: copies of your files stored in many cities around the world, so users download from somewhere close to them, which is much faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Building block 5: Identity and security (the keys)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Identity and Access Management (IAM)&lt;/strong&gt; decides who can do what. Every person and every application gets an identity, and each identity is given only the permissions it truly needs. This is called the &lt;strong&gt;principle of least privilege&lt;/strong&gt;: give the cleaner a key to the corridor, not to the safe.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Put the five together: a typical web application has users who reach it through DNS and a load balancer (networking), which sends them to servers or containers (compute) that read and write data in a database, store files in object storage, and all of it is protected by IAM and firewalls (security). That one sentence describes thousands of real systems.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  7. The big ideas that make the cloud special
&lt;/h2&gt;

&lt;p&gt;Beyond the building blocks, a few concepts explain why companies love the cloud. You will hear these words in every interview and meeting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scalability and elasticity
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Scalability&lt;/strong&gt; means your system can grow to handle more work. &lt;strong&gt;Elasticity&lt;/strong&gt; means it can grow &lt;em&gt;and shrink&lt;/em&gt; automatically as demand changes. Picture a restaurant that magically adds tables when a crowd arrives and removes them when it's quiet, and only pays rent for the tables in use.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scaling up (vertical)&lt;/strong&gt;: making one server bigger, like swapping a small car for a bus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaling out (horizontal)&lt;/strong&gt;: adding more servers, like adding more cars to the fleet. In the cloud, scaling out is usually preferred because there is no single point of failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pay-as-you-go
&lt;/h3&gt;

&lt;p&gt;Instead of a huge upfront purchase (what accountants call &lt;strong&gt;CapEx&lt;/strong&gt;), you pay a running bill for what you use (&lt;strong&gt;OpEx&lt;/strong&gt;). A startup can launch with very little money, and a large company can experiment without buying hardware it may never need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Regions and availability zones
&lt;/h3&gt;

&lt;p&gt;Cloud providers have data centres spread across the world. A &lt;strong&gt;region&lt;/strong&gt; is a geographic area, such as "West Europe" or "South Africa North". Inside each region are several &lt;strong&gt;availability zones&lt;/strong&gt;: separate data centres with their own power and cooling, a few kilometres apart.&lt;/p&gt;

&lt;p&gt;Why does this matter? If you run your application in two or three zones and one has a power failure, the others keep serving your users. It's the cloud version of not putting all your eggs in one basket. You also choose regions close to your users (for speed) or in specific countries (for legal reasons about where data may be stored).&lt;/p&gt;

&lt;h3&gt;
  
  
  High availability and disaster recovery
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High availability&lt;/strong&gt; means designing systems so they keep running even when parts fail. Assume things &lt;em&gt;will&lt;/em&gt; break, and plan for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disaster recovery&lt;/strong&gt; means having a plan, and tested backups, to recover if something big goes wrong, such as a whole region going down or data being deleted by accident.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The shared responsibility model
&lt;/h3&gt;

&lt;p&gt;This is one of the most important, and most misunderstood, ideas in cloud security. The provider and the customer share the job of keeping things safe:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The provider is responsible for…&lt;/th&gt;
&lt;th&gt;You (the customer) are responsible for…&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Security OF the cloud: physical buildings, guards, hardware, power, the global network and the core software that runs the cloud.&lt;/td&gt;
&lt;td&gt;Security IN the cloud: your data, your user accounts and passwords, who has access, your firewall rules, your application code, and patching your own virtual machines.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In plain words: the landlord secures the building, but you still have to lock your own front door. Many real-world cloud data leaks happen not because the provider was hacked, but because a customer left a storage bucket open to the public or used a weak password.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. How modern cloud engineers actually work
&lt;/h2&gt;

&lt;p&gt;Ten years ago, many people set up cloud resources by clicking around in a web console. That still works for learning, but professional teams have moved on to methods that are faster, safer and repeatable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Infrastructure as Code (IaC)
&lt;/h3&gt;

&lt;p&gt;Instead of clicking buttons, you &lt;strong&gt;describe your infrastructure in text files&lt;/strong&gt;, and a tool builds it for you. It's like the difference between giving a builder verbal instructions and giving them a precise blueprint. With a blueprint you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build the exact same setup again and again (for testing, staging and production).&lt;/li&gt;
&lt;li&gt;Keep a history of every change in &lt;strong&gt;Git&lt;/strong&gt;, so you know who changed what and why, and can undo mistakes.&lt;/li&gt;
&lt;li&gt;Review changes before they happen, just like developers review code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Popular tools include &lt;strong&gt;Terraform&lt;/strong&gt; (works with almost every provider), &lt;strong&gt;AWS CloudFormation&lt;/strong&gt;, &lt;strong&gt;Azure Bicep&lt;/strong&gt;, &lt;strong&gt;Pulumi&lt;/strong&gt;, and &lt;strong&gt;Ansible&lt;/strong&gt; (great for configuring servers after they are created). Here is a tiny taste of what Terraform looks like. Even without experience, you can probably guess what it does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_resource_group"&lt;/span&gt; &lt;span class="s2"&gt;"shop"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"online-shop-rg"&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"West Europe"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That short block says: "create a container for my resources called online-shop-rg in the West Europe region." Readable, repeatable, reviewable.&lt;/p&gt;

&lt;h3&gt;
  
  
  DevOps and CI/CD
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;DevOps&lt;/strong&gt; is a culture and set of practices that brings software developers (Dev) and operations teams (Ops) together, so software can be released quickly and reliably. A key tool of DevOps is the &lt;strong&gt;CI/CD pipeline&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Continuous Integration (CI)&lt;/strong&gt;: every time a developer saves a change, it is automatically built and tested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuous Delivery or Deployment (CD)&lt;/strong&gt;: if the tests pass, the change is automatically packaged and released to the cloud.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of it as a factory conveyor belt with quality checks at each station. Tools include GitHub Actions, Azure DevOps, GitLab CI and Jenkins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Containers and Kubernetes
&lt;/h3&gt;

&lt;p&gt;Once you have lots of containers, you need something to organise them: start them, restart them if they crash, spread them across servers and add more when traffic grows. That "orchestra conductor" is called &lt;strong&gt;Kubernetes&lt;/strong&gt; (often shortened to &lt;strong&gt;K8s&lt;/strong&gt;). Every major cloud offers a managed version: Amazon EKS, Azure AKS and Google GKE.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Don't rush into Kubernetes. It is powerful but complex. Learn Linux, networking, one cloud and Docker first. Kubernetes will make far more sense once those foundations are solid.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Monitoring and observability
&lt;/h3&gt;

&lt;p&gt;You can't fix what you can't see. Cloud engineers use &lt;strong&gt;monitoring&lt;/strong&gt; tools to watch the health of systems: CPU usage, error rates, response times and costs. They set &lt;strong&gt;alerts&lt;/strong&gt; that send a message when something looks wrong, and they collect &lt;strong&gt;logs&lt;/strong&gt; (detailed diaries of what applications are doing) to investigate problems. Examples include Amazon CloudWatch, Azure Monitor, Google Cloud Monitoring, Prometheus and Grafana.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Cloud security in plain English
&lt;/h2&gt;

&lt;p&gt;Security is not a separate topic you learn "later"; it's part of everything you build. Here are the habits that prevent the vast majority of beginner disasters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Turn on multi-factor authentication (MFA)&lt;/strong&gt; for every account, especially the main "root" or administrator account. Then stop using the root account for daily work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never put passwords or secret keys in your code&lt;/strong&gt; or upload them to GitHub. Use a secrets manager such as AWS Secrets Manager, Azure Key Vault or Google Secret Manager. Bots scan public code for leaked keys within minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow least privilege.&lt;/strong&gt; Give people and applications only the permissions they need, nothing more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep storage private by default.&lt;/strong&gt; Only make something public if you truly mean to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encrypt data&lt;/strong&gt;, both while it is stored (at rest) and while it travels (in transit, using HTTPS).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patch and update&lt;/strong&gt; your virtual machines and software regularly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Back up important data&lt;/strong&gt;, and occasionally test that you can actually restore it. An untested backup is just a hope.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  10. Let's talk about money (before the cloud talks to you)
&lt;/h2&gt;

&lt;p&gt;Almost every beginner has a story about a surprise cloud bill. Pay-as-you-go is wonderful, but it also means things you forget to switch off keep costing money. Protect yourself from day one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set up a budget alert&lt;/strong&gt; immediately after creating your account, so you get an email if spending passes a small amount you choose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the free tier&lt;/strong&gt; wisely. All major providers offer free credits or always-free services for learning, but read the limits carefully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete what you're not using.&lt;/strong&gt; When you finish a lab or tutorial, clean up every resource you created. Infrastructure as Code makes this easy: one command destroys everything you built.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop or shut down&lt;/strong&gt; test virtual machines when you're not using them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch out for the sneaky costs&lt;/strong&gt;: data leaving the cloud (egress), forgotten disks and IP addresses, and large database instances.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In companies, managing cloud spending has become its own discipline, often called &lt;strong&gt;FinOps&lt;/strong&gt;. Engineers who can save money are highly valued.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Your first cloud project: a gentle walkthrough
&lt;/h2&gt;

&lt;p&gt;Reading is good, but doing is where understanding really clicks. Here is a beginner-friendly progression of projects. Each builds on the last.&lt;/p&gt;

&lt;h3&gt;
  
  
  Project 1: Host a personal website (the "hello, cloud" moment)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create a free-tier account with one provider and set up a budget alert.&lt;/li&gt;
&lt;li&gt;Write a simple HTML page about yourself.&lt;/li&gt;
&lt;li&gt;Upload it to object storage (for example, an S3 bucket or Azure Storage account) and switch on static website hosting.&lt;/li&gt;
&lt;li&gt;Open the web address the cloud gives you. Your page is now live on the internet, served from a data centre.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You just used storage, networking and permissions, three of the five building blocks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Project 2: Launch and connect to a virtual machine
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create a small Linux virtual machine.&lt;/li&gt;
&lt;li&gt;Configure its firewall (security group) to allow only SSH from your own IP address, plus web traffic on port 80.&lt;/li&gt;
&lt;li&gt;Connect to it securely using SSH, install a web server like Nginx, and serve your page from it.&lt;/li&gt;
&lt;li&gt;Shut it down (or delete it) when you're done.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Project 3: Rebuild Project 2 with code
&lt;/h3&gt;

&lt;p&gt;Now repeat Project 2 using Terraform. Then destroy everything with a single command and recreate it in seconds. This is the moment many people truly understand why Infrastructure as Code matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Project 4: Containerise and automate
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Package a small web app in a Docker container.&lt;/li&gt;
&lt;li&gt;Store your code on GitHub.&lt;/li&gt;
&lt;li&gt;Create a GitHub Actions pipeline that builds the container and deploys it to the cloud every time you push a change.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Congratulations: you have now touched compute, storage, networking, security, Infrastructure as Code, containers and CI/CD. Put these projects on GitHub with a clear README. They become your portfolio, which is often more convincing to employers than any certificate.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. A realistic learning roadmap
&lt;/h2&gt;

&lt;p&gt;You don't need a computer science degree to become a cloud engineer, but you do need solid foundations. Here's a sensible order. Timelines vary from person to person; consistency matters more than speed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;What to learn&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Foundations&lt;/td&gt;
&lt;td&gt;How computers and the internet work; Linux command line basics; networking basics (IP addresses, ports, DNS, HTTP/HTTPS, firewalls).&lt;/td&gt;
&lt;td&gt;Most cloud servers run Linux, and nearly every cloud problem is secretly a networking problem.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. One cloud provider&lt;/td&gt;
&lt;td&gt;Pick AWS, Azure or GCP. Learn compute, storage, networking, databases, IAM, monitoring and billing.&lt;/td&gt;
&lt;td&gt;This is your home base. Depth in one beats shallow knowledge of three.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Scripting&lt;/td&gt;
&lt;td&gt;Python or Bash; enough to automate tasks and read code comfortably.&lt;/td&gt;
&lt;td&gt;Automation is the superpower of cloud engineers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Git and GitHub&lt;/td&gt;
&lt;td&gt;Saving versions, branching and pull requests.&lt;/td&gt;
&lt;td&gt;Everything modern, including infrastructure, lives in Git.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Infrastructure as Code&lt;/td&gt;
&lt;td&gt;Terraform first, plus the basics of Ansible.&lt;/td&gt;
&lt;td&gt;This is how professionals build real cloud environments.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Containers&lt;/td&gt;
&lt;td&gt;Docker, then the fundamentals of Kubernetes.&lt;/td&gt;
&lt;td&gt;Most modern applications ship in containers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7. CI/CD and DevOps&lt;/td&gt;
&lt;td&gt;GitHub Actions, Azure DevOps or Jenkins pipelines.&lt;/td&gt;
&lt;td&gt;Shipping software safely and quickly is the end goal.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8. Security and cost&lt;/td&gt;
&lt;td&gt;IAM in depth, secrets management, encryption, budgets and FinOps habits.&lt;/td&gt;
&lt;td&gt;These separate trusted engineers from risky ones.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9. Monitoring&lt;/td&gt;
&lt;td&gt;Logs, metrics, alerts and dashboards.&lt;/td&gt;
&lt;td&gt;Running systems well is as important as building them.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Certifications: helpful, but not magic
&lt;/h3&gt;

&lt;p&gt;Certifications give structure to your learning and help your CV get noticed. Good beginner starting points are the entry-level "fundamentals" exams from each provider, such as &lt;strong&gt;AWS Certified Cloud Practitioner&lt;/strong&gt;, &lt;strong&gt;Microsoft Azure Fundamentals (AZ-900)&lt;/strong&gt; and &lt;strong&gt;Google Cloud Digital Leader&lt;/strong&gt;. After that, associate-level exams such as &lt;strong&gt;AWS Solutions Architect Associate&lt;/strong&gt; or &lt;strong&gt;Azure Administrator (AZ-104)&lt;/strong&gt; are widely respected. Exam names and versions change from time to time, so always check the provider's official certification page before you book.&lt;/p&gt;

&lt;p&gt;One honest word of advice: &lt;strong&gt;a certificate shows you studied; a project shows you can do the work.&lt;/strong&gt; Aim for both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Soft skills nobody tells you about
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Curiosity&lt;/strong&gt;: the cloud changes constantly. Enjoying learning is a genuine advantage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear communication&lt;/strong&gt;: explaining technical trade-offs in simple words to managers and clients is a rare and valuable skill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation&lt;/strong&gt;: writing things down saves your team (and your future self) hours of confusion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calm problem-solving&lt;/strong&gt;: when systems break, the engineer who stays calm, reads the logs and works step by step is the hero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thinking about the business&lt;/strong&gt;: the best cloud engineers ask, "What does this cost, and what value does it bring?"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  13. Common beginner mistakes (so you can skip them)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Trying to learn everything at once.&lt;/strong&gt; Three clouds, Kubernetes and five tools in your first month leads to burnout. Go deep, one layer at a time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only watching videos.&lt;/strong&gt; Tutorials feel productive, but real learning happens when you build, break and fix things yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring Linux and networking.&lt;/strong&gt; These foundations make everything else easier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaving resources running.&lt;/strong&gt; Always clean up, and always have a budget alert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard-coding secrets.&lt;/strong&gt; Never commit passwords or access keys to Git.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using the root or admin account for everything.&lt;/strong&gt; Create separate users with limited permissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Being afraid to break things.&lt;/strong&gt; Your learning account is a sandbox. Breaking things there is how you learn not to break things in production.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  14. Myths about cloud engineering
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Myth&lt;/th&gt;
&lt;th&gt;Reality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"You must be a programming genius."&lt;/td&gt;
&lt;td&gt;You need to be comfortable with scripting and reading code, not to build the next big app. Many great cloud engineers came from IT support, networking or system administration.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"The cloud is always cheaper."&lt;/td&gt;
&lt;td&gt;It can be, if it's designed and managed well. Poorly managed cloud environments can be very expensive.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"The cloud is automatically secure."&lt;/td&gt;
&lt;td&gt;Providers secure the infrastructure; you must secure your configuration, data and access.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Servers disappear in the cloud."&lt;/td&gt;
&lt;td&gt;They still exist; you just don't manage the hardware. Even "serverless" runs on servers that someone else manages.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"It's too late to start."&lt;/td&gt;
&lt;td&gt;Organisations keep moving workloads to the cloud, and skilled people are still needed everywhere, from startups and banks to governments and nonprofits.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  15. A beginner's glossary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Plain-English meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud provider&lt;/td&gt;
&lt;td&gt;A company that rents out computing resources over the internet (AWS, Azure, Google Cloud).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data centre&lt;/td&gt;
&lt;td&gt;A large, secure building full of servers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server&lt;/td&gt;
&lt;td&gt;A powerful computer that provides services to other computers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtual machine (VM)&lt;/td&gt;
&lt;td&gt;A computer created in software, running on a physical server.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container&lt;/td&gt;
&lt;td&gt;A lightweight package containing an app and everything it needs to run.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;The most popular tool for building and running containers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes (K8s)&lt;/td&gt;
&lt;td&gt;A system that manages and scales large numbers of containers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serverless&lt;/td&gt;
&lt;td&gt;Running code without managing any servers; you pay per execution.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region&lt;/td&gt;
&lt;td&gt;A geographic area where a provider has data centres.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability zone&lt;/td&gt;
&lt;td&gt;An independent data centre (or group of them) within a region.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPC / VNet&lt;/td&gt;
&lt;td&gt;Your private, isolated network inside the cloud.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load balancer&lt;/td&gt;
&lt;td&gt;Spreads incoming traffic across several servers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM&lt;/td&gt;
&lt;td&gt;Identity and Access Management: who can do what.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object storage&lt;/td&gt;
&lt;td&gt;Storage for files of any kind, accessed over the internet.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IaC&lt;/td&gt;
&lt;td&gt;Infrastructure as Code: defining infrastructure in text files.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terraform&lt;/td&gt;
&lt;td&gt;A popular Infrastructure as Code tool that works with many clouds.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD&lt;/td&gt;
&lt;td&gt;Automatically testing and releasing code changes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scalability&lt;/td&gt;
&lt;td&gt;The ability to handle more work by adding resources.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elasticity&lt;/td&gt;
&lt;td&gt;Automatically adding and removing resources as demand changes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High availability&lt;/td&gt;
&lt;td&gt;Designing systems to keep running even when parts fail.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency&lt;/td&gt;
&lt;td&gt;The delay before data arrives; lower is faster.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Egress&lt;/td&gt;
&lt;td&gt;Data leaving the cloud, which often costs money.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FinOps&lt;/td&gt;
&lt;td&gt;The practice of managing and optimising cloud spending.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Final thoughts: you can do this
&lt;/h2&gt;

&lt;p&gt;Let's go back to where we started. The cloud is simply other people's computers, rented over the internet and paid for as you go. Cloud engineering is the craft of building and running things on those computers so they are reliable, secure, fast and affordable.&lt;/p&gt;

&lt;p&gt;Every expert you admire once typed their first command, broke their first server and stared confused at their first error message. The difference between them and someone who never started is not talent; it's that they kept going, one small project at a time.&lt;/p&gt;

&lt;p&gt;So here is your challenge for this week: &lt;strong&gt;create a free cloud account, set a budget alert, and put a one-page website online.&lt;/strong&gt; It might take an afternoon. When you see your page live on the internet, served from a data centre somewhere in the world, you will feel the spark that got many of us hooked.&lt;/p&gt;

&lt;p&gt;Welcome to the cloud. The sky is not the limit; it's just where your servers live.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally written by Michael Gbenga Abajemito. If this helped you, share it with someone starting their cloud journey.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>beginners</category>
      <category>devops</category>
      <category>aws</category>
    </item>
    <item>
      <title>Step-by-Step Guide: Create a Resource Group in Azure</title>
      <dc:creator>MICHAEL ABAJEMITO</dc:creator>
      <pubDate>Tue, 17 Mar 2026 14:05:22 +0000</pubDate>
      <link>https://dev.to/mikegbenga/step-by-step-guide-create-a-resource-group-in-azure-3600</link>
      <guid>https://dev.to/mikegbenga/step-by-step-guide-create-a-resource-group-in-azure-3600</guid>
      <description>&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Sign in to Azure Portal&lt;br&gt;
Open your browser.&lt;br&gt;
Navigate to:&lt;a href="https://portal.azure.com" rel="noopener noreferrer"&gt;https://portal.azure.com&lt;/a&gt;&lt;br&gt;
Log in with your Azure account credentials.&lt;br&gt;
This opens the Microsoft Azure Portal dashboard.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz7ivi9kvr20li7ahibz3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz7ivi9kvr20li7ahibz3.png" alt=" " width="800" height="386"&gt;&lt;/a&gt; &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open the Resource Groups Service&lt;br&gt;
In the left navigation menu, click Resource groups.&lt;br&gt;
If it is not visible:&lt;br&gt;
• On the Search Bar&lt;br&gt;
• Search for Resource groups&lt;br&gt;
• Select it.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy9p4g6wcpb575bi7s906.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy9p4g6wcpb575bi7s906.png" alt=" " width="799" height="424"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Start Creating a Resource Group&lt;br&gt;
Click the + Create button at the top of the page.&lt;br&gt;
This opens the Create Resource Group configuration page.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdpmt1tj47jghf7cm8pvl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdpmt1tj47jghf7cm8pvl.png" alt=" " width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure Basic Settings
Fill in the required fields:
Subscription
Select the Azure subscription where the resource group will be created.
Example: Subscription: Pay-As-You-Go
Resource Group Name
Enter a unique name.
Example:
GreenCity-RG
Best practice:
-RG
Example naming patterns:
FintechApp-RG
DataAnalytics-RG
GreenCityPlatform-RG
Region
Select the geographic region where metadata for the resource group will be stored.
Example:
West Europe
East US
UK South
Note:
This does not restrict resources to that region, but it is recommended to align them.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkf08r6kh57x6irfjzb2x.png" alt=" " width="800" height="404"&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffk9mp7p8j4mdclsf25rf.png" alt=" " width="800" height="426"&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Review Configuration&lt;br&gt;
Click Review + Create.&lt;br&gt;
Azure validates:&lt;br&gt;
• Naming rules&lt;br&gt;
• Subscription permissions&lt;br&gt;
• Region availability&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkokferskx9a755bs1as8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkokferskx9a755bs1as8.png" alt=" " width="800" height="716"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create the Resource Group&lt;br&gt;
Click Create.&lt;br&gt;
Azure will deploy the resource group in a few seconds.&lt;br&gt;
After that Go back and select Resource Group and the newly created resource group will be displayed.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn1ap6bdxoa1lhzmx2yga.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn1ap6bdxoa1lhzmx2yga.png" alt=" " width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
