<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: David Cletus</title>
    <description>The latest articles on DEV Community by David Cletus (@4thman).</description>
    <link>https://dev.to/4thman</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%2F3927892%2F43aac6d7-df74-4a90-bdbe-8ba55edf1a70.jpeg</url>
      <title>DEV Community: David Cletus</title>
      <link>https://dev.to/4thman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/4thman"/>
    <language>en</language>
    <item>
      <title>Hands-On AWS: Spin Up IAM, S3 and a Jenkins Server on EC2 (and Fix What Breaks)</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Wed, 14 Oct 2026 22:29:26 +0000</pubDate>
      <link>https://dev.to/4thman/hands-on-aws-spin-up-iam-s3-and-a-jenkins-server-on-ec2-and-fix-what-breaks-45h4</link>
      <guid>https://dev.to/4thman/hands-on-aws-spin-up-iam-s3-and-a-jenkins-server-on-ec2-and-fix-what-breaks-45h4</guid>
      <description>&lt;blockquote&gt;
&lt;h2&gt;
  
  
  &lt;em&gt;A step-by-step AWS lab with lots screenshots: create and delete an IAM user, host a site on S3, launch an EC2 server and run Jenkins on it, including the two errors I hit and how I fixed them.&lt;/em&gt;
&lt;/h2&gt;
&lt;/blockquote&gt;

&lt;p&gt;Almost every AWS learning path starts with the same three moves: decide who is allowed to do what, store a file somewhere, and start a server. That is &lt;strong&gt;IAM&lt;/strong&gt;, &lt;strong&gt;S3&lt;/strong&gt; and &lt;strong&gt;EC2&lt;/strong&gt;, and nearly everything else on AWS sits on top of those three.&lt;/p&gt;

&lt;p&gt;So I did all three for real, took a screenshot of every click, hit two errors along the way, fixed them, and then deleted everything at the end of each part so nothing was left running on my bill.&lt;/p&gt;

&lt;p&gt;This post is the full walkthrough. By the end you will have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Created an IAM user, signed in as that user, forced a password reset, and deleted the user&lt;/li&gt;
&lt;li&gt;Created an S3 bucket, uploaded a website into it, made it public, and opened it in a browser&lt;/li&gt;
&lt;li&gt;Launched an Ubuntu server on EC2, connected over SSH, installed Java and Jenkins, and unlocked Jenkins in the browser&lt;/li&gt;
&lt;li&gt;Seen exactly what two common AWS errors look like, why they happen, and how to fix them&lt;/li&gt;
&lt;li&gt;Cleaned up every resource so you do not pay for what you are not using&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The plan
&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%2Ffb8unynwb41cmiel4n0s.jpg" 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%2Ffb8unynwb41cmiel4n0s.jpg" alt=" " width="799" height="365"&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;Part&lt;/th&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;What I built&lt;/th&gt;
&lt;th&gt;How I cleaned up&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;IAM&lt;/td&gt;
&lt;td&gt;A user called &lt;code&gt;4thman&lt;/code&gt; with console access and a forced password reset&lt;/td&gt;
&lt;td&gt;Deleted the user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;S3&lt;/td&gt;
&lt;td&gt;A bucket called &lt;code&gt;4thman-bucket&lt;/code&gt; serving my CV website&lt;/td&gt;
&lt;td&gt;Emptied and deleted the bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;EC2&lt;/td&gt;
&lt;td&gt;An Ubuntu server called &lt;code&gt;4thman-server&lt;/code&gt; running Jenkins&lt;/td&gt;
&lt;td&gt;Stopped, then terminated the instance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An AWS account. I worked as the root user for this lab (more on why that is not ideal in a moment).&lt;/li&gt;
&lt;li&gt;A terminal with SSH. I used Git Bash on Windows. macOS and Linux terminals work the same way.&lt;/li&gt;
&lt;li&gt;A small static website if you want to repeat the S3 part. I used my own CV website, a one-page site built from a Tooplate template called StackCraft: one &lt;code&gt;index.html&lt;/code&gt;, one CSS file, one JavaScript file and an &lt;code&gt;images&lt;/code&gt; folder. Any simple HTML, CSS and JS site will do.&lt;/li&gt;
&lt;li&gt;A second browser window in incognito or private mode, for testing the IAM user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A note on cost: everything here is small, but "small" only stays small if you clean up. That is why every part ends with a teardown. Free tier and credits differ between accounts, so check your own billing page.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: IAM, deciding who gets to do what
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why I did not just keep using root
&lt;/h3&gt;

&lt;p&gt;Look at the top right corner of the next screenshot. It says &lt;strong&gt;root&lt;/strong&gt;. The root user owns the account and can do absolutely anything, including closing it. You use it to get started, and then you stop using it. Day-to-day work belongs to an IAM user with only the permissions it needs. That principle is called &lt;strong&gt;least privilege&lt;/strong&gt;, and IAM is the service that enforces it.&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%2Fnf11wxkn64f1zt62xkjs.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%2Fnf11wxkn64f1zt62xkjs.png" alt=" " width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AWS Console Home, signed in as the root user&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Open IAM
&lt;/h3&gt;

&lt;p&gt;In the search bar at the top of the console, type &lt;code&gt;iam&lt;/code&gt; and pick &lt;strong&gt;IAM&lt;/strong&gt; from the Services list.&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%2Fur1cj6dx40dz990owun6.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%2Fur1cj6dx40dz990owun6.png" alt=" " width="799" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Searching for IAM in the console search bar&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Create the user
&lt;/h3&gt;

&lt;p&gt;In the sidebar, open &lt;strong&gt;IAM users&lt;/strong&gt;. The count says &lt;code&gt;(0)&lt;/code&gt;, so this account has no IAM users yet. Click &lt;strong&gt;Create user&lt;/strong&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%2Fdtk0s5wksskhyitbeiag.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%2Fdtk0s5wksskhyitbeiag.png" alt=" " width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;IAM users page with no users yet and the Create user button&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Fill in the user details
&lt;/h3&gt;

&lt;p&gt;This screen has four decisions on it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;User name:&lt;/strong&gt; &lt;code&gt;4thman&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provide user access to the AWS Management Console:&lt;/strong&gt; ticked, because I want this user to be able to sign in on the website and not just use the CLI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Console password:&lt;/strong&gt; I chose &lt;strong&gt;Custom password&lt;/strong&gt; so I could pick my own. The rules are at least 8 characters and a mix of at least three of: uppercase, lowercase, numbers and symbols.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Users must create a new password at next sign-in:&lt;/strong&gt; ticked. AWS marks this as recommended, and it is a good habit. Whoever receives the temporary password has to replace it with their own on first login.&lt;/li&gt;
&lt;/ol&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%2Fiebmtzw3vwi20mflenoc.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%2Fiebmtzw3vwi20mflenoc.png" alt=" " width="799" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;User details: name, console access, custom password and forced reset&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Permissions
&lt;/h3&gt;

&lt;p&gt;The next step offers three ways to give permissions: add the user to a group, copy permissions from another user, or attach policies directly. AWS recommends groups, because you manage "the Developers" once instead of managing every developer separately.&lt;/p&gt;

&lt;p&gt;For this lab I did not want this user to be able to do anything yet, so I left the defaults alone and clicked &lt;strong&gt;Next&lt;/strong&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%2Fz7lgsvp8o76szwfq8ggd.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%2Fz7lgsvp8o76szwfq8ggd.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Set permissions step, left on the defaults&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Review and create
&lt;/h3&gt;

&lt;p&gt;The review page shows the choices I made: custom password, password reset required. The part I want you to notice is &lt;strong&gt;Permissions summary&lt;/strong&gt;. I never attached a policy, but one is listed: &lt;code&gt;IAMUserChangePassword&lt;/code&gt;. AWS adds it automatically when you force a password reset, because the user needs permission to change their own password. It lets them do that and nothing else, which is exactly least privilege at work.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create user&lt;/strong&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%2Fbq6nmxlowii0jqn29pc9.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%2Fbq6nmxlowii0jqn29pc9.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Review page showing IAMUserChangePassword as the only policy&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Save the sign-in details
&lt;/h3&gt;

&lt;p&gt;The next screen is the only time AWS shows you the console password. It also gives you the sign-in URL, which follows this pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://&amp;lt;your-account-id&amp;gt;.signin.aws.amazon.com/console
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the details or click &lt;strong&gt;Download .csv file&lt;/strong&gt; before leaving this 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%2F54tldbzkcavzm62frlgj.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%2F54tldbzkcavzm62frlgj.png" alt=" " width="799" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;User created, with the console sign-in URL, user name and password&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Sign in as the new user
&lt;/h3&gt;

&lt;p&gt;I opened an &lt;strong&gt;incognito window&lt;/strong&gt; so I could stay signed in as root in my normal window. On the IAM user sign-in page I entered the account ID, the user name &lt;code&gt;4thman&lt;/code&gt; and the temporary password.&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%2Fhq6d8zitsxpgk0p9l0v8.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%2Fhq6d8zitsxpgk0p9l0v8.png" alt=" " width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Signing in as the new IAM user in an incognito window&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Straight after clicking Sign in, AWS showed this:&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%2Fyymbjytpbcpphr7l5u6f.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%2Fyymbjytpbcpphr7l5u6f.png" alt=" " width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Password reset screen shown at first sign-in&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That message says the password "has expired or requires a reset", which sounds like a problem. It is not. It is the "must create a new password" option from Step 3 doing its job. I entered the old password once, the new one twice, watched the green &lt;strong&gt;Matches&lt;/strong&gt; appear, and clicked &lt;strong&gt;Confirm Password Change&lt;/strong&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%2Fbmu16kksgg2zdo2oavr7.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%2Fbmu16kksgg2zdo2oavr7.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Password reset successful&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then I signed in again with the new password and landed on the console as &lt;code&gt;4thman&lt;/code&gt;. Notice the account menu at the top right now shows the user name instead of root.&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%2F8l6vd28wjvom637wkz0i.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%2F8l6vd28wjvom637wkz0i.png" alt=" " width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Console Home as the 4thman IAM user&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One thing to watch: this console opened in &lt;strong&gt;Europe (Stockholm)&lt;/strong&gt;, because the sign-in page had redirected me to a Stockholm sign-in endpoint. IAM users are global, so that does not affect the user. But most other services are regional, so always check the region selector before you create anything.&lt;/p&gt;

&lt;p&gt;This user can open the console but cannot do much with it. The only policy attached lets them change their own password. That is the point of starting with nothing and adding permissions deliberately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8: Delete the user
&lt;/h3&gt;

&lt;p&gt;Back in my root window, the IAM users list now shows &lt;code&gt;4thman&lt;/code&gt; with real activity: last activity and console sign-in a couple of minutes ago, password age of a few minutes. I ticked the box next to the user and clicked &lt;strong&gt;Delete user&lt;/strong&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%2Fgmdmf6djgm3cma04k0ov.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%2Fgmdmf6djgm3cma04k0ov.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Selecting the user and clicking Delete user&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AWS makes you type &lt;code&gt;confirm&lt;/code&gt; before it deletes anything. It is a small speed bump on purpose. Deleting a user also removes its credentials, so there is no undo.&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%2Fipz6dsoqv4aympg9df4z.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%2Fipz6dsoqv4aympg9df4z.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Typing confirm to delete the user&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;IAM done. One user created, tested and removed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: S3, storing files and serving a website
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What S3 actually is
&lt;/h3&gt;

&lt;p&gt;S3 is object storage. You create a &lt;strong&gt;bucket&lt;/strong&gt; (a container), and everything you put in it is an &lt;strong&gt;object&lt;/strong&gt;. Two details are worth knowing before you start:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bucket names are unique across &lt;strong&gt;all&lt;/strong&gt; AWS accounts in the world, not just yours.&lt;/li&gt;
&lt;li&gt;S3 has no real folders. When the console shows an &lt;code&gt;images/&lt;/code&gt; folder, that is just a shared prefix in the file names. You will see this when the folder appears in the object list later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1: Find S3 and create a bucket
&lt;/h3&gt;

&lt;p&gt;Search for &lt;code&gt;s3 buckets&lt;/code&gt; and open &lt;strong&gt;S3&lt;/strong&gt;. If you have no buckets yet, the landing page shows a big &lt;strong&gt;Create bucket&lt;/strong&gt; button.&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%2F0r9lyzkb1w1nraerh3eg.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%2F0r9lyzkb1w1nraerh3eg.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Searching for S3 in the console&lt;/em&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%2Fekeapizsxzs9eehvlf36.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%2Fekeapizsxzs9eehvlf36.png" alt=" " width="799" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The S3 landing page and its Create bucket button&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Name the bucket
&lt;/h3&gt;

&lt;p&gt;I confirmed the region was &lt;strong&gt;US East (N. Virginia) us-east-1&lt;/strong&gt;, kept the type as &lt;strong&gt;General purpose&lt;/strong&gt;, kept the default global namespace, and named the bucket &lt;code&gt;4thman-bucket&lt;/code&gt;. Bucket names must be 3 to 63 characters and can only contain lowercase letters, numbers, periods and hyphens.&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%2Fara8tp0w9k74x86hdx4s.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%2Fara8tp0w9k74x86hdx4s.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Region, bucket type and the bucket name 4thman-bucket&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Leave the safe defaults alone
&lt;/h3&gt;

&lt;p&gt;Scroll down and you will see two settings I deliberately did not change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Object Ownership: ACLs disabled (recommended).&lt;/strong&gt; Access is controlled through policies only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block all public access: ticked.&lt;/strong&gt; All four sub-settings are on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS builds buckets private by default, and that is a good thing. These two defaults are also the reason I hit an error in a few minutes, so keep them in mind.&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%2Fpha01v597fmektxjhx41.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%2Fpha01v597fmektxjhx41.png" alt=" " width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Object Ownership with ACLs disabled and Block all public access ticked&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At the bottom, default encryption is already on (SSE-S3, with a bucket key enabled). I left it and clicked &lt;strong&gt;Create bucket&lt;/strong&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%2F17nk8l3vqdlammz8gtim.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%2F17nk8l3vqdlammz8gtim.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Default encryption settings and the final Create bucket button&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Upload the CV website
&lt;/h3&gt;

&lt;p&gt;The bucket now exists and says &lt;strong&gt;Objects (0)&lt;/strong&gt;. Click &lt;strong&gt;Upload&lt;/strong&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%2Fbug1zssjfkehm5z56goa.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%2Fbug1zssjfkehm5z56goa.png" alt=" " width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bucket created and empty, with the Upload button&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I added the four files that make up my CV website: &lt;code&gt;index.html&lt;/code&gt;, &lt;code&gt;tooplate-stackcraft-script.js&lt;/code&gt;, &lt;code&gt;tooplate-stackcraft-style.css&lt;/code&gt;, and &lt;code&gt;about.jpg&lt;/code&gt; inside an &lt;code&gt;images/&lt;/code&gt; folder. The upload summary reports 4 files totalling 229.1 KB with 0 failures. Notice the destination, &lt;code&gt;s3://4thman-bucket&lt;/code&gt;, and that &lt;code&gt;about.jpg&lt;/code&gt; shows &lt;code&gt;images/&lt;/code&gt; in the Folder column.&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%2F3ixgrbdeail0as05325g.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%2F3ixgrbdeail0as05325g.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Upload succeeded: 4 files, 229.1 KB, 0 failed&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The files are in, but because the bucket is private, nobody on the internet can open them yet. Time to change that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error 1: "Access denied" when making the files public
&lt;/h3&gt;

&lt;p&gt;I went back to the object list, ticked every file and folder, and opened the &lt;strong&gt;Actions&lt;/strong&gt; menu to choose &lt;strong&gt;Make public using ACL&lt;/strong&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%2Fqszce268b9mvrksywpkl.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%2Fqszce268b9mvrksywpkl.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Selecting all objects and opening the Actions menu&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The next page shows two banners, and both matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;green&lt;/strong&gt; one: &lt;em&gt;Successfully enabled ACLs (Bucket owner preferred)&lt;/em&gt;. Because I created the bucket with ACLs disabled, and "Make public" works by setting an object ACL, the console switched the ownership setting for me.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;blue&lt;/strong&gt; one: &lt;em&gt;Public access is blocked because Block Public Access settings are turned on for this bucket.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I read the green one, ignored the blue one, and clicked &lt;strong&gt;Make public&lt;/strong&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%2F042hgc5ejx1oyqk09ae6.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%2F042hgc5ejx1oyqk09ae6.png" alt=" " width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;First Make public attempt, with the ACL banner and the Block Public Access warning&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The result, for all four objects:&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%2Fhonuflb7nsoenodfsyam.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%2Fhonuflb7nsoenodfsyam.png" alt=" " width="799" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Failed to edit public access: Access denied on all four objects&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failed to edit public access: Access denied.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading the error properly
&lt;/h3&gt;

&lt;p&gt;My first instinct with "Access denied" was to check my permissions. But I was the root user, so permissions were not the problem. The blue banner had already told me the cause.&lt;/p&gt;

&lt;p&gt;There are two separate gates on public access in S3:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Object ACLs and ownership&lt;/strong&gt;, which decide how an individual object can be made public. The console fixed this one for me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block Public Access&lt;/strong&gt;, a bucket-level safety switch that overrides ACLs and bucket policies. It was still on, and it wins every time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Even if the object says "public", Block Public Access says "no".&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;Open the bucket and go to the &lt;strong&gt;Permissions&lt;/strong&gt; tab.&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%2Fetyurihy418uv3ox1kgf.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%2Fetyurihy418uv3ox1kgf.png" alt=" " width="799" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Opening the Permissions tab of the bucket&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Under &lt;strong&gt;Block public access (bucket settings)&lt;/strong&gt; it says &lt;strong&gt;Block all public access: On&lt;/strong&gt;. Click &lt;strong&gt;Edit&lt;/strong&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%2F8zr92adzpri70s2276pn.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%2F8zr92adzpri70s2276pn.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Block all public access is On, with the Edit button&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Untick &lt;strong&gt;Block all public access&lt;/strong&gt;. That clears all four boxes underneath it. Click &lt;strong&gt;Save changes&lt;/strong&gt; and confirm the warning S3 shows you.&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%2Fscofww4tarmww8c0u7kk.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%2Fscofww4tarmww8c0u7kk.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Block all public access unticked, then Save changes&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now repeat the original action. Back on the &lt;strong&gt;Objects&lt;/strong&gt; tab I selected everything again and opened &lt;strong&gt;Actions&lt;/strong&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%2Fir0lqgze4awxl5lnjf46.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%2Fir0lqgze4awxl5lnjf46.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Selecting all objects again and opening Actions&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On the Make public page, I clicked &lt;strong&gt;Make public&lt;/strong&gt; again. This time there was no blue warning.&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%2Ftslkvm0u2s8hmmpq7kjc.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%2Ftslkvm0u2s8hmmpq7kjc.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Second Make public attempt, with no blocking warning&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The summary now reads: &lt;strong&gt;Successfully edited public access, 4 objects, 229.1 KB, 0 failed.&lt;/strong&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%2Fufa2kvot8c0mxzhk8v1i.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%2Fufa2kvot8c0mxzhk8v1i.png" alt=" " width="799" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Successfully edited public access: 4 objects, 0 failed&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  See it live
&lt;/h3&gt;

&lt;p&gt;I ticked &lt;code&gt;index.html&lt;/code&gt; only and clicked &lt;strong&gt;Copy URL&lt;/strong&gt;. The console confirmed with &lt;strong&gt;Object URL Copied&lt;/strong&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%2Foh0bn2hsm3shg19slrps.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%2Foh0bn2hsm3shg19slrps.png" alt=" " width="799" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Copying the object URL of index.html&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Pasting it into a new tab loaded my full CV website straight from S3, with my name, role, skills, work and experience sections all in place. The URL has this shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://&amp;lt;bucket-name&amp;gt;.s3.&amp;lt;region&amp;gt;.amazonaws.com/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CSS, JavaScript and image all loaded as well, because I made every object public, not just the HTML file.&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%2Fmnb8ydfetbq562n18jzm.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%2Fmnb8ydfetbq562n18jzm.png" alt=" " width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;My CV website loading straight from S3&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A word of caution.&lt;/strong&gt; Opening a whole bucket to the internet is fine for a throwaway demo. For a real site, keep the bucket private and put CloudFront in front of it, or at least use a bucket policy that only allows reading the specific objects you want public.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Cleanup: empty the bucket, then delete it
&lt;/h3&gt;

&lt;p&gt;S3 will not delete a bucket that still has objects in it. I selected everything and clicked &lt;strong&gt;Delete&lt;/strong&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%2F626f2bddawx6gfvrngci.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%2F626f2bddawx6gfvrngci.png" alt=" " width="800" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Selecting all objects and clicking Delete&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;S3 asks you to type &lt;code&gt;permanently delete&lt;/code&gt; to confirm. This cannot be undone. (The &lt;strong&gt;Empty&lt;/strong&gt; button on the bucket list does the same job in one step.)&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%2Fpy4z0gn49dwvpqs92w65.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%2Fpy4z0gn49dwvpqs92w65.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Typing permanently delete to confirm&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With the bucket empty, I went back to the bucket list, selected &lt;code&gt;4thman-bucket&lt;/code&gt; and clicked &lt;strong&gt;Delete&lt;/strong&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%2Fq2kxv6t6f7qa8wogjyn3.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%2Fq2kxv6t6f7qa8wogjyn3.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Selecting the empty bucket and clicking Delete&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To confirm, I typed the bucket name. Read the warnings on this page: once a bucket is deleted, its name is released and another AWS user can claim it.&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%2Fjymgi37m59y5djcx6iry.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%2Fjymgi37m59y5djcx6iry.png" alt=" " width="799" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Typing the bucket name to confirm deletion&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;S3 done. A bucket created, a website served from it, one error fixed, everything removed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: EC2, a server in the cloud, running Jenkins
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The vocabulary you need first
&lt;/h3&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;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EC2 instance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A virtual server running in AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AMI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The image the server starts from: operating system plus preinstalled software. I used an Ubuntu image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Instance type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The size of the machine. I used &lt;code&gt;t3.micro&lt;/code&gt; (2 vCPU, 1 GiB memory)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Key pair&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A public and private key used to log in over SSH. AWS keeps the public half. You keep the private &lt;code&gt;.pem&lt;/code&gt; file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security group&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A virtual firewall around the instance that decides which ports accept traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Jenkins&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;An open source automation server, widely used to build, test and deploy code (CI/CD)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 1: Launch the wizard
&lt;/h3&gt;

&lt;p&gt;Search for &lt;code&gt;ec2&lt;/code&gt; and open &lt;strong&gt;EC2&lt;/strong&gt;. (The green banner partly visible behind the search box is the bucket deletion confirming.)&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%2Fhqhwts4h39qrlarln0kh.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%2Fhqhwts4h39qrlarln0kh.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Searching for EC2 in the console&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On the EC2 landing page, click &lt;strong&gt;Launch instance&lt;/strong&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%2F52susxahyva8k5uyqblx.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%2F52susxahyva8k5uyqblx.png" alt=" " width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The EC2 landing page and its Launch instance button&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AWS then offered a choice of walkthroughs. I picked &lt;strong&gt;Learn about EC2 and launch test instances&lt;/strong&gt;. That is why a &lt;strong&gt;Tutorial&lt;/strong&gt; panel appears on the right side of the next screenshots: it lists the steps and, handily, ends with a reminder to terminate your instance.&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%2Ffae6h5lzm5yvcv7feg2h.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%2Ffae6h5lzm5yvcv7feg2h.png" alt=" " width="799" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Choosing the Learn about EC2 walkthrough&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Name it and pick an image
&lt;/h3&gt;

&lt;p&gt;I named the instance &lt;code&gt;4thman-server&lt;/code&gt;, chose &lt;strong&gt;Ubuntu&lt;/strong&gt; from the Quick Start images, and left the number of instances at 1. The Summary panel on the right tracks everything you choose: image, instance type, firewall and storage.&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%2Fvhtnuy2n12hw5r1g663f.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%2Fvhtnuy2n12hw5r1g663f.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Naming the instance, choosing Ubuntu and setting the number of instances&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Instance type and key pair
&lt;/h3&gt;

&lt;p&gt;I picked &lt;strong&gt;t3.micro&lt;/strong&gt;. Scrolling down to &lt;strong&gt;Key pair (login)&lt;/strong&gt;, I clicked &lt;strong&gt;Create new key pair&lt;/strong&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%2Fuueo8f255lha10ophbtk.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%2Fuueo8f255lha10ophbtk.png" alt=" " width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instance type and the Create new key pair link&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I named it &lt;code&gt;4thman-key&lt;/code&gt;, kept &lt;strong&gt;RSA&lt;/strong&gt; as the type and &lt;strong&gt;.pem&lt;/strong&gt; as the format (the format for OpenSSH, which is what Git Bash uses), and clicked &lt;strong&gt;Create key pair&lt;/strong&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%2F1hgxb6lfucc3wo7xpttp.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%2F1hgxb6lfucc3wo7xpttp.png" alt=" " width="800" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Creating the key pair 4thman-key as RSA and .pem&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The browser downloaded &lt;code&gt;4thman-key.pem&lt;/code&gt; immediately.&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%2Feve7ogx8qwyv8dkhgeim.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%2Feve7ogx8qwyv8dkhgeim.png" alt=" " width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;4thman-key.pem downloaded by the browser&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Guard this file.&lt;/strong&gt; AWS only keeps the public half of the key. This download is the only copy of the private half. Lose it and you cannot SSH into the server. Never commit it to GitHub.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 4: Network and security group
&lt;/h3&gt;

&lt;p&gt;In &lt;strong&gt;Network settings&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto-assign public IP:&lt;/strong&gt; Enable, so the server is reachable from the internet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create security group&lt;/strong&gt; (a new firewall for this instance)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow SSH traffic from:&lt;/strong&gt; Anywhere (&lt;code&gt;0.0.0.0/0&lt;/code&gt;), so I can log in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow HTTP traffic from the internet:&lt;/strong&gt; ticked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow HTTPS traffic from the internet:&lt;/strong&gt; left unticked&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS shows a yellow warning that &lt;code&gt;0.0.0.0/0&lt;/code&gt; means every IP address. For a short-lived lab that is acceptable. For anything longer, choose &lt;strong&gt;My IP&lt;/strong&gt; instead of Anywhere.&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%2Fm43jlnc0exsj7uui9yis.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%2Fm43jlnc0exsj7uui9yis.png" alt=" " width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Network settings: public IP, new security group, SSH and HTTP allowed&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Review and launch
&lt;/h3&gt;

&lt;p&gt;Storage stayed at the default 8 GiB gp3 root volume. The Summary panel showed one instance, the Ubuntu image, &lt;code&gt;t3.micro&lt;/code&gt;, a new security group and one 8 GiB volume. I clicked &lt;strong&gt;Launch instance&lt;/strong&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%2Fq1kaijiignvfbmo5ew41.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%2Fq1kaijiignvfbmo5ew41.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Summary panel and the Launch instance button&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on the screenshots in this section.&lt;/strong&gt; Some of the launch screenshots (the key pair, security group, launch summary, success banner and running view) were captured on an earlier launch of the same setup, so the instance size, instance ID and IP address differ from the server used in the SSH and Jenkins steps. The clicks are identical. When I talk about the server's public IP from here on, use the one on your own instance page.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AWS replied with a green &lt;strong&gt;Success&lt;/strong&gt; banner and the new instance ID. The page also suggests next steps, including &lt;strong&gt;Create billing usage alerts&lt;/strong&gt;. Next time I will set that up before launching anything.&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%2F268kkk09rflru1nkvxqp.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%2F268kkk09rflru1nkvxqp.png" alt=" " width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instance launch initiated successfully&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On the &lt;strong&gt;Instances&lt;/strong&gt; page the new server went to &lt;strong&gt;Running&lt;/strong&gt;, and the status checks passed. In the details panel, copy the &lt;strong&gt;Public IPv4 address&lt;/strong&gt;. You need it to connect.&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%2Ft3tv9pl5reehju126use.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%2Ft3tv9pl5reehju126use.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instance running, with its public IPv4 address&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Lock down the key file and connect over SSH
&lt;/h3&gt;

&lt;p&gt;While you are here, fix the key's permissions. SSH refuses to use a private key that other users on the machine can read, so restrict it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;Downloads
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; 4thman-key.pem
&lt;span class="nb"&gt;chmod &lt;/span&gt;400 4thman-key.pem
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; 4thman-key.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;chmod 400&lt;/code&gt; means read-only for the file owner and nothing for anyone else. In the screenshot the listing goes from &lt;code&gt;-rw-r--r--&lt;/code&gt; to &lt;code&gt;-r--r--r--&lt;/code&gt;. Because this is Git Bash on a Windows drive, permission bits do not map exactly like on Linux, but SSH accepted the key anyway.&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%2Frv78ibbu1n8msn5p4du0.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%2Frv78ibbu1n8msn5p4du0.png" alt=" " width="799" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Restricting the key file with chmod 400&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now connect. The default user on Ubuntu images is &lt;code&gt;ubuntu&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"4thman-key.pem"&lt;/span&gt; ubuntu@&amp;lt;your-public-ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first time, SSH asks whether you trust this host. Type &lt;code&gt;yes&lt;/code&gt;. The Ubuntu welcome banner appears and the prompt changes to &lt;code&gt;ubuntu@ip-172-31-25-206:~$&lt;/code&gt;, which means every command from now on runs on the server in AWS and not on my laptop.&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%2Fi891mm1j3tfti44li7dy.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%2Fi891mm1j3tfti44li7dy.png" alt=" " width="800" height="748"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Connected to the server over SSH&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Update the server
&lt;/h3&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;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;apt update&lt;/code&gt; refreshes the list of available packages. &lt;code&gt;apt upgrade -y&lt;/code&gt; installs updates without asking for confirmation. On a fresh image there was nothing pending, and apt reported &lt;strong&gt;All packages are up to date&lt;/strong&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%2Fa2zdb31i0fm1qxzq1g71.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%2Fa2zdb31i0fm1qxzq1g71.png" alt=" " width="799" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;sudo apt update and sudo apt upgrade -y&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8: Install Java
&lt;/h3&gt;

&lt;p&gt;Jenkins is a Java application, so Java comes first. &lt;code&gt;fontconfig&lt;/code&gt; is a small dependency Jenkins needs for rendering fonts.&lt;br&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;apt &lt;span class="nb"&gt;install &lt;/span&gt;fontconfig openjdk-21-jre &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fg6kg22h5uapy99mki0g5.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%2Fg6kg22h5uapy99mki0g5.png" alt=" " width="800" height="186"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Installing fontconfig and OpenJDK 21&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Check it worked:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;You should see an OpenJDK 21 version string. You can see mine at the top of the screenshot in the next step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 9: Add the Jenkins repository
&lt;/h3&gt;

&lt;p&gt;Ubuntu's default repositories do not include Jenkins. The Jenkins project publishes its own package repository, so we add that and tell apt to trust its signing key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/apt/keyrings

&lt;span class="nb"&gt;sudo &lt;/span&gt;wget &lt;span class="nt"&gt;-O&lt;/span&gt; /etc/apt/keyrings/jenkins-keyring.asc &lt;span class="se"&gt;\&lt;/span&gt;
  https://pkg.jenkins.io/debian-stable/jenkins.io-2026.key

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [signed-by=/etc/apt/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/jenkins.list &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null

&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each piece does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mkdir -p /etc/apt/keyrings&lt;/code&gt; makes sure the folder for trusted keys exists&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wget&lt;/code&gt; downloads the Jenkins signing key into it, so apt can verify the packages really come from the Jenkins project&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;echo ... | sudo tee&lt;/code&gt; writes a new repository entry that points at &lt;code&gt;pkg.jenkins.io&lt;/code&gt; and says which key to check it against&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apt update&lt;/code&gt; reads the new repository. In the output you can see &lt;code&gt;pkg.jenkins.io/debian-stable&lt;/code&gt; being fetched&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key file name contains a year. If this step fails, check the official Jenkins installation page for the current key name.&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%2F57cy33tm8dvahzkchs6i.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%2F57cy33tm8dvahzkchs6i.png" alt=" " width="800" height="677"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Java version check, Jenkins signing key, repository entry and apt update&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 10: Install Jenkins
&lt;/h3&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;apt &lt;span class="nb"&gt;install &lt;/span&gt;jenkins &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output shows Jenkins &lt;code&gt;2.580.1&lt;/code&gt; (about 53.9 MB) installing along with a small helper package called &lt;code&gt;net-tools&lt;/code&gt;. At the very end, Ubuntu prints a message about a &lt;strong&gt;pending kernel upgrade&lt;/strong&gt; and suggests a reboot. That is a notice, not an error. Jenkins installs and runs fine without the reboot.&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%2Flkvm4niuknesm7liovb0.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%2Flkvm4niuknesm7liovb0.png" alt=" " width="800" height="688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Installing Jenkins with apt&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 11: Start Jenkins and check it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; jenkins
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status jenkins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;enable --now&lt;/code&gt; does two things at once: it starts Jenkins immediately and makes it start automatically every time the server boots. &lt;code&gt;status&lt;/code&gt; shows the result. Look for the green &lt;strong&gt;active (running)&lt;/strong&gt; line, and notice the Java process and the memory use (about 300 MB for me). Press &lt;code&gt;q&lt;/code&gt; to leave the status view.&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%2Frncy7pfglli2jf4e3e9h.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%2Frncy7pfglli2jf4e3e9h.png" alt=" " width="799" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Enabling Jenkins and confirming active (running)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Jenkins is running. Time to open it in the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error 2: "This site can't be reached"
&lt;/h3&gt;

&lt;p&gt;I took the server's public IP, added Jenkins' default port, and opened &lt;code&gt;http://&amp;lt;your-public-ip&amp;gt;:8080&lt;/code&gt; in Chrome.&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%2F5als75ul0u4plqa0n14h.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%2F5als75ul0u4plqa0n14h.png" alt=" " width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Chrome showing ERR_CONNECTION_TIMED_OUT on port 8080&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This site can't be reached. The server took too long to respond. ERR_CONNECTION_TIMED_OUT.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading the error properly
&lt;/h3&gt;

&lt;p&gt;I had just seen Jenkins report &lt;strong&gt;active (running)&lt;/strong&gt;, so the service was fine. The clue is in the wording:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;connection refused&lt;/strong&gt; error means the machine answered and said "nothing is listening on that port". That points at the service.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;timed out&lt;/strong&gt; error means the packets never got an answer at all. Something in between silently dropped them. On AWS, that is almost always the &lt;strong&gt;security group&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I created the instance, I only allowed port 22 (SSH) and port 80 (HTTP). Jenkins listens on &lt;strong&gt;8080&lt;/strong&gt;, and nothing allowed that port in.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: open port 8080 in the security group
&lt;/h3&gt;

&lt;p&gt;In the EC2 console, open &lt;strong&gt;Instances&lt;/strong&gt;, select the server, and click the &lt;strong&gt;Security&lt;/strong&gt; tab. Under &lt;strong&gt;Security groups&lt;/strong&gt;, click the group's link (mine was &lt;code&gt;launch-wizard-2&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%2F5z02scovdm91m7e69yx5.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%2F5z02scovdm91m7e69yx5.png" alt=" " width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instances, Security tab and the security group link&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On the security group page, the &lt;strong&gt;Inbound rules&lt;/strong&gt; tab lists only two rules, HTTP and SSH. Click &lt;strong&gt;Edit inbound rules&lt;/strong&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%2Fkfrwhlqcaqfiwxsg9cbn.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%2Fkfrwhlqcaqfiwxsg9cbn.png" alt=" " width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Inbound rules showing only HTTP and SSH, with Edit inbound rules&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Add rule&lt;/strong&gt;. A new row appears with the type already set to Custom TCP and the port at &lt;code&gt;0&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%2F92fg7cp00gpx26a2m1ji.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%2F92fg7cp00gpx26a2m1ji.png" alt=" " width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Edit inbound rules with the Add rule button&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Set the port range to &lt;code&gt;8080&lt;/code&gt; and the source to &lt;code&gt;Anywhere-IPv4&lt;/code&gt; (&lt;code&gt;0.0.0.0/0&lt;/code&gt;). AWS warns again that this opens the port to everyone, which is fine for a lab and something to tighten later. Click &lt;strong&gt;Save rules&lt;/strong&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%2F1rbuvt7olkvff7uyh7hx.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%2F1rbuvt7olkvff7uyh7hx.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Custom TCP rule for port 8080 from Anywhere-IPv4, then Save rules&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Security group changes apply instantly. No restart, no reboot. I reloaded the browser tab and the error was gone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unlock Jenkins
&lt;/h3&gt;

&lt;p&gt;Jenkins greeted me with &lt;strong&gt;Unlock Jenkins&lt;/strong&gt;. The browser shows &lt;strong&gt;Not secure&lt;/strong&gt; in the address bar because we are on plain HTTP and not HTTPS. That is expected for a quick lab, and I will come back to it at the end.&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%2Fi5etct4br782tvolosu5.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%2Fi5etct4br782tvolosu5.png" alt=" " width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Unlock Jenkins page at port 8080&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The page says the first admin password lives in a file on the server. Back in the SSH session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /var/lib/jenkins/secrets/initialAdminPassword
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need &lt;code&gt;sudo&lt;/code&gt; because only root can read that file.&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%2Fccxisnhpyueglt9uqksp.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%2Fccxisnhpyueglt9uqksp.png" alt=" " width="800" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Reading the initial admin password on the server&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I copied the long string it printed, pasted it into the &lt;strong&gt;Administrator password&lt;/strong&gt; box and clicked &lt;strong&gt;Continue&lt;/strong&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%2F1m9quri9rnk6bzcv9cd4.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%2F1m9quri9rnk6bzcv9cd4.png" alt=" " width="800" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Pasting the administrator password and clicking Continue&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next came &lt;strong&gt;Customize Jenkins&lt;/strong&gt;, with two choices: &lt;strong&gt;Install suggested plugins&lt;/strong&gt; (the safe default) or &lt;strong&gt;Select plugins to install&lt;/strong&gt;. Jenkins 2.580.1 is up and answering on my server's public IP.&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%2Fuvjlad62muju4r4e6ih9.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%2Fuvjlad62muju4r4e6ih9.png" alt=" " width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Customize Jenkins: install suggested plugins or select plugins&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That is where I stopped for this post. From here Jenkins would install its plugins and ask you to create your first admin user. The goal of this lab was getting Jenkins running on a server I built myself, and that is done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleanup: stop and terminate the server
&lt;/h3&gt;

&lt;p&gt;This is the part people skip, and it is the part that costs money. In the EC2 dashboard, the &lt;strong&gt;Resources&lt;/strong&gt; box shows exactly what is still alive: 1 running instance, 1 key pair, 3 security groups and 1 volume.&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%2F16azl55curan85etwvte.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%2F16azl55curan85etwvte.png" alt=" " width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;EC2 dashboard Resources box before cleanup&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I selected the instance and opened the &lt;strong&gt;Instance state&lt;/strong&gt; menu.&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%2Fuzfbyho94uoxydul6etl.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%2Fuzfbyho94uoxydul6etl.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Selecting the instance and opening the Instance state menu&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I stopped it first. The status moves to &lt;strong&gt;Stopping&lt;/strong&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%2F8o0djy9tse8e1o4abazc.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%2F8o0djy9tse8e1o4abazc.png" alt=" " width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instance stopping&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then I terminated it. Terminated means deleted for good, so the state ends at &lt;strong&gt;Terminated&lt;/strong&gt;, as in the final screenshot. Both green banners are visible here: stopping and termination were both initiated.&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%2Fslzg047odm63fi8pt04m.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%2Fslzg047odm63fi8pt04m.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instance terminated&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A difference worth remembering: &lt;strong&gt;stopped is not deleted.&lt;/strong&gt; A stopped instance stops charging for compute, but its storage volume still exists and still bills. Terminate when you are done.&lt;/p&gt;

&lt;p&gt;The key pair and security groups stay behind after termination. They are free, but delete them in the EC2 console if you will not reuse them, and delete the &lt;code&gt;.pem&lt;/code&gt; from your computer too.&lt;/p&gt;




&lt;h2&gt;
  
  
  The two errors, side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;What I saw&lt;/th&gt;
&lt;th&gt;Real 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;1&lt;/td&gt;
&lt;td&gt;S3, Make public&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;Failed to edit public access: Access denied&lt;/em&gt; on all 4 objects&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Block Public Access&lt;/strong&gt; was on, and it overrides ACLs. (ACLs were also disabled by default, which the console fixed for me.)&lt;/td&gt;
&lt;td&gt;Permissions tab, Edit Block public access, untick, Save. Then Make public again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Jenkins in the browser&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;ERR_CONNECTION_TIMED_OUT&lt;/em&gt; on port 8080&lt;/td&gt;
&lt;td&gt;The security group only allowed ports 22 and 80&lt;/td&gt;
&lt;td&gt;Add an inbound Custom TCP rule for 8080&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And three smaller things that look scarier than they are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Password has expired or requires a reset"&lt;/strong&gt; at first IAM sign-in is the forced reset working as designed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Pending kernel upgrade"&lt;/strong&gt; after installing Jenkins is a reboot suggestion, not a failure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;chmod 400&lt;/code&gt; showing &lt;code&gt;-r--r--r--&lt;/code&gt;&lt;/strong&gt; on Git Bash is a Windows quirk, and SSH was fine with it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to would do differently during production
&lt;/h2&gt;

&lt;p&gt;This lab was about learning the moves, so I took shortcuts that I would not take on anything real:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stop using root.&lt;/strong&gt; I worked as root all the way through, which is the exact habit Part 1 is meant to break.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not use &lt;code&gt;0.0.0.0/0&lt;/code&gt; for SSH.&lt;/strong&gt; Pick &lt;strong&gt;My IP&lt;/strong&gt; so only my own address can reach port 22.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restrict port 8080 too,&lt;/strong&gt; and put Jenkins behind HTTPS with a reverse proxy before anyone enters a real password into it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep S3 buckets private&lt;/strong&gt; and serve public sites through CloudFront.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set a billing alert first,&lt;/strong&gt; before launching anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use groups for permissions,&lt;/strong&gt; not individual users.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;Three services, three cleanups, two errors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IAM&lt;/strong&gt; taught me that new users start with almost no power, and that is a feature&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3&lt;/strong&gt; taught me that "Access denied" does not always mean &lt;em&gt;your&lt;/em&gt; permissions, and that public access has more than one gate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EC2&lt;/strong&gt; taught me that a timeout and a refusal are different problems, and that the security group is the first place to look when a running service will not answer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of the fixes took less than a minute once I understood the message. The hard part was reading the error properly, not fixing it.&lt;/p&gt;

&lt;p&gt;Tried it yourself and ran into a different error? Drop the exact error message in the comments, and let’s troubleshoot it together! If this lab helped you, show some love, hit follow, and join me as I tackle new challenges and share real lessons from my cloud and DevOps journey, one lab at a time.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>jenkins</category>
      <category>beginners</category>
      <category>aws</category>
    </item>
    <item>
      <title>From “It Works on My Machine” to Azure: Deploying a Dockerized Notes App in 3 Ways, Breaking Things, Fixing Them, and Learning Along the Way</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Thu, 01 Oct 2026 23:31:50 +0000</pubDate>
      <link>https://dev.to/4thman/from-it-works-on-my-machine-to-azure-deploying-a-dockerized-notes-app-in-3-ways-breaking-2nlk</link>
      <guid>https://dev.to/4thman/from-it-works-on-my-machine-to-azure-deploying-a-dockerized-notes-app-in-3-ways-breaking-2nlk</guid>
      <description>&lt;p&gt;"It works on my machine" is a fine place to start. It's a terrible place to stop.&lt;/p&gt;

&lt;p&gt;I built a small Notes app with Node.js, Express and PostgreSQL, put it in Docker, and then deployed that same container image in three different ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Localhost&lt;/strong&gt; with Docker Compose&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure App Service for Containers&lt;/strong&gt; with &lt;strong&gt;Azure Database for PostgreSQL&lt;/strong&gt; (managed PaaS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An Azure Virtual Machine&lt;/strong&gt; running Docker Compose (IaaS)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I also did a quick detour through &lt;strong&gt;Azure Container Instances (ACI)&lt;/strong&gt; as a warm-up, because it taught me something useful.&lt;/p&gt;

&lt;p&gt;Nothing went perfectly on the first try, and I left the errors in this post. They taught me more than the parts that worked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/4thman/4thman-dockerised-notes-web-application.git" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Docker image:&lt;/strong&gt; &lt;a href="https://hub.docker.com/r/4thman/notes-app" rel="noopener noreferrer"&gt;4thman/notes-app on Docker Hub&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;A Notes app where you can add a note (title and content), see all notes sorted newest first, and delete notes. The notes live in PostgreSQL, so they need to survive restarts.&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%2Fm7kkxqw4l8ynr0nor48z.jpg" 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%2Fm7kkxqw4l8ynr0nor48z.jpg" alt=" " width="800" height="345"&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;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Plain HTML/CSS/JS served as static files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Node.js 20 + Express&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;PostgreSQL 16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Packaging&lt;/td&gt;
&lt;td&gt;Docker + Docker Compose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registry&lt;/td&gt;
&lt;td&gt;Docker Hub&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud&lt;/td&gt;
&lt;td&gt;Azure (ACI, App Service, PostgreSQL Flexible Server, VM)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  The three deployments at a glance
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Localhost&lt;/th&gt;
&lt;th&gt;App Service + managed Postgres&lt;/th&gt;
&lt;th&gt;VM + Compose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who manages the OS?&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;Azure&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who manages the database?&lt;/td&gt;
&lt;td&gt;A container&lt;/td&gt;
&lt;td&gt;Azure&lt;/td&gt;
&lt;td&gt;A container on the VM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public URL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;localhost:3000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;*.azurewebsites.net&lt;/code&gt; (HTTPS)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;VM-IP:3000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effort&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium (lots of settings)&lt;/td&gt;
&lt;td&gt;Medium (lots of setup)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Development&lt;/td&gt;
&lt;td&gt;Production-style apps&lt;/td&gt;
&lt;td&gt;Full control, learning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  What you need
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Docker Desktop (with WSL on Windows)&lt;/li&gt;
&lt;li&gt;Node.js and npm&lt;/li&gt;
&lt;li&gt;VS Code with Git Bash&lt;/li&gt;
&lt;li&gt;Azure CLI and an Azure subscription&lt;/li&gt;
&lt;li&gt;A Docker Hub account&lt;/li&gt;
&lt;li&gt;A GitHub account&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Part 1: Build the app and run it locally
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Project structure
&lt;/h3&gt;

&lt;p&gt;I created the project folders from the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;dockerapp-project
&lt;span class="nb"&gt;cd &lt;/span&gt;dockerapp-project
&lt;span class="nb"&gt;mkdir &lt;/span&gt;david-note
&lt;span class="nb"&gt;cd &lt;/span&gt;david-note
&lt;span class="nb"&gt;mkdir &lt;/span&gt;public
&lt;span class="nb"&gt;touch &lt;/span&gt;public/index.html Dockerfile server.js docker-compose.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Creating the project folder and opening it in VS Code:&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%2F7o1njp60s72qwwfhwawb.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%2F7o1njp60s72qwwfhwawb.png" alt=" " width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Creating the &lt;code&gt;david-note&lt;/code&gt; and &lt;code&gt;public&lt;/code&gt; folders and the &lt;code&gt;index.html&lt;/code&gt; file:&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%2Ffd2t37de4myykrzlcz5x.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%2Ffd2t37de4myykrzlcz5x.png" alt=" " width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Creating the &lt;code&gt;Dockerfile&lt;/code&gt; and &lt;code&gt;server.js&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%2Ff0yvikds4p2a51iw19ei.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%2Ff0yvikds4p2a51iw19ei.png" alt=" " width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You end up with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;david-note/
├── public/
│   └── index.html
├── server.js
├── Dockerfile
└── docker-compose.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The frontend
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;public/index.html&lt;/code&gt; is a single page with a form (title, content, an "Add Note" button) and a list of notes, each with a Delete link. It calls the backend API with &lt;code&gt;fetch&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%2Fn00jc6ktagaaglnozvmo.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%2Fn00jc6ktagaaglnozvmo.png" alt=" " width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The backend (&lt;code&gt;server.js&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The server does four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connects to PostgreSQL using environment variables&lt;/li&gt;
&lt;li&gt;Creates the &lt;code&gt;notes&lt;/code&gt; table on startup if it doesn't exist&lt;/li&gt;
&lt;li&gt;Exposes a small REST API&lt;/li&gt;
&lt;li&gt;Serves the static frontend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The connection pool is the part that matters for deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB_HOST&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;db&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB_PORT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB_USER&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;appuser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB_NAME&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notesdb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB_SSL&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rejectUnauthorized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything is configurable through environment variables. The same image can talk to a Postgres container on my laptop, a Postgres container on a VM, or Azure's managed Postgres, and I never rebuild it. The &lt;code&gt;DB_SSL&lt;/code&gt; flag matters later, because Azure's managed Postgres requires encrypted connections.&lt;/p&gt;

&lt;p&gt;On startup the app creates the table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;initDb&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
    CREATE TABLE IF NOT EXISTS notes (
      id SERIAL PRIMARY KEY,
      title VARCHAR(255) NOT NULL,
      content TEXT,
      created_at TIMESTAMP DEFAULT NOW()
    );
  `&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API has three routes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /api/notes&lt;/code&gt; lists notes, newest first&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /api/notes&lt;/code&gt; creates a note&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DELETE /api/notes/:id&lt;/code&gt; deletes a note&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's also a health route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I made a deliberate decision here. If the database connection fails at startup, the server logs the error and &lt;strong&gt;keeps running&lt;/strong&gt; instead of crashing. A crash loop gives you nothing to debug. A running server that logs "Database initialization failed" and still answers &lt;code&gt;/health&lt;/code&gt; tells you exactly where the problem is. That choice paid off during the Azure deployment.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;server.js&lt;/code&gt;, part 1 (imports, connection pool, table creation, GET route):&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%2Fe84kxd9f2eqiza55wape.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%2Fe84kxd9f2eqiza55wape.png" alt=" " width="799" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;server.js&lt;/code&gt;, part 2 (POST, DELETE, health check and startup):&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%2F652x6j9cmhl5h5na8psr.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%2F652x6j9cmhl5h5na8psr.png" alt=" " width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Dockerfile
&lt;/h3&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; node:20-alpine&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; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&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;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order matters. Copying &lt;code&gt;package*.json&lt;/code&gt; first and running &lt;code&gt;npm install&lt;/code&gt; before copying the rest of the code means Docker caches the dependency layer. If you only change &lt;code&gt;server.js&lt;/code&gt;, rebuilds take seconds instead of reinstalling everything.&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%2Fzhaldr33ipgdgq3r5rtx.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%2Fzhaldr33ipgdgq3r5rtx.png" alt=" " width="799" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Compose file
&lt;/h3&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;app&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;4thman/notes-app:V1&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;3000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="na"&gt;DB_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
      &lt;span class="na"&gt;DB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appuser&lt;/span&gt;
      &lt;span class="na"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;your-local-password&amp;gt;&lt;/span&gt;
      &lt;span class="na"&gt;DB_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;notesdb&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&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;db&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;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appuser&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;your-local-password&amp;gt;&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;notesdb&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db_data:/var/lib/postgresql/data&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;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details worth pointing out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;build: .&lt;/code&gt; and &lt;code&gt;image:&lt;/code&gt; together.&lt;/strong&gt; Compose builds the image from the local Dockerfile and tags it &lt;code&gt;4thman/notes-app:V1&lt;/code&gt;. That tag is what I push to Docker Hub later, so the image I tested locally is the image I deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DB_HOST: db&lt;/code&gt;.&lt;/strong&gt; Inside the Compose network, containers reach each other by service name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;db_data&lt;/code&gt; volume.&lt;/strong&gt; This is what keeps notes alive when containers are removed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Creating the Compose file:&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%2F4vpnzb6cwdvazjbxqgju.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%2F4vpnzb6cwdvazjbxqgju.png" alt=" " width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Compose script in the editor:&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%2Fwkgi2lsv0dnflmsd2dw0.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%2Fwkgi2lsv0dnflmsd2dw0.png" alt=" " width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Error #1: &lt;code&gt;Cannot find module 'pg'&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;I ran &lt;code&gt;npm init -y&lt;/code&gt; and &lt;code&gt;npm install express&lt;/code&gt; to generate &lt;code&gt;package.json&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%2F9ajfpueg0ko51eciw3ok.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%2F9ajfpueg0ko51eciw3ok.png" alt=" " width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I built and started everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose 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;&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%2Fcnef3gz92d6yx539ax7n.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%2Fcnef3gz92d6yx539ax7n.png" alt=" " width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The build succeeded and the containers started. But the app wasn't reachable. &lt;code&gt;docker compose ps&lt;/code&gt; showed the app container as &lt;strong&gt;Restarting&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
docker compose logs app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logs gave the reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Cannot find module 'pg'
Require stack:
- /app/server.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fnf2wmob8s8bhirgvo0rt.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%2Fnf2wmob8s8bhirgvo0rt.png" alt=" " width="800" height="621"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; &lt;code&gt;server.js&lt;/code&gt; requires the &lt;code&gt;pg&lt;/code&gt; PostgreSQL driver, but I had only installed &lt;code&gt;express&lt;/code&gt;. &lt;code&gt;package.json&lt;/code&gt; didn't list &lt;code&gt;pg&lt;/code&gt;, so &lt;code&gt;RUN npm install&lt;/code&gt; inside the image never installed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;express pg
docker compose 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;&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%2F34jvidhcdhtlnqisp9i7.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%2F34jvidhcdhtlnqisp9i7.png" alt=" " width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--build&lt;/code&gt; flag forces a rebuild so the image picks up the updated &lt;code&gt;package.json&lt;/code&gt;. This time the app came up.&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%2F2k4g2vd7fkmpqcn6m98d.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%2F2k4g2vd7fkmpqcn6m98d.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; the container only knows what's in &lt;code&gt;package.json&lt;/code&gt;. If it works on your machine because of a global install, it will break in Docker. Also, &lt;code&gt;docker compose logs &amp;lt;service&amp;gt;&lt;/code&gt; should be the first thing you reach for when a container keeps restarting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Proving the data survives
&lt;/h3&gt;

&lt;p&gt;At &lt;code&gt;http://localhost:3000&lt;/code&gt; the app loaded:&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%2F6jchwj4s43bwyom6mj4q.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%2F6jchwj4s43bwyom6mj4q.png" alt=" " width="799" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I added a note ("Hello" / "This is David test running the notes-app"), then typed a second one:&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%2Fzdg1di6t2kpv7fcguns7.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%2Fzdg1di6t2kpv7fcguns7.png" alt=" " width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I tore everything down:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&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%2Fmh6xlcug3w9y8qf259lm.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%2Fmh6xlcug3w9y8qf259lm.png" alt=" " width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;localhost:3000&lt;/code&gt; now showed &lt;code&gt;ERR_CONNECTION_REFUSED&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%2Fwntfj4xqdbcuchc65wke.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%2Fwntfj4xqdbcuchc65wke.png" alt=" " width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I brought it back:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&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%2Fb3fceoge3shyoq4ewlqm.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%2Fb3fceoge3shyoq4ewlqm.png" alt=" " width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both notes were still there:&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%2F3tg50g84sdw20ogzwjlr.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%2F3tg50g84sdw20ogzwjlr.png" alt=" " width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker compose down&lt;/code&gt; removes containers and the network, but not named volumes, so Postgres picked up its data again. If I had run &lt;code&gt;docker compose down -v&lt;/code&gt;, the notes would have been wiped.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Quick note: Compose printed a warning that the &lt;code&gt;version&lt;/code&gt; attribute is obsolete. It's harmless, since modern Compose ignores it, but you can delete that line.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 2: Push the image to Docker Hub
&lt;/h2&gt;

&lt;p&gt;Every cloud deployment below pulls the image from a registry, so this step connects local to cloud.&lt;/p&gt;

&lt;p&gt;First, a &lt;code&gt;.dockerignore&lt;/code&gt; so junk doesn't end up inside the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node_modules
.git
.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without it, &lt;code&gt;COPY . .&lt;/code&gt; would copy your local &lt;code&gt;node_modules&lt;/code&gt; into the image and overwrite the clean ones that &lt;code&gt;npm install&lt;/code&gt; just built. It also keeps &lt;code&gt;.env&lt;/code&gt; secrets out.&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%2Famozgvqsoo4a8e3xalxo.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%2Famozgvqsoo4a8e3xalxo.png" alt=" " width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I logged in and confirmed the image existed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down
docker login
docker image &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Ftktupng7kxwa3xqki3s9.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%2Ftktupng7kxwa3xqki3s9.png" alt=" " width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And pushed it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker push 4thman/notes-app:V1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might notice the push output says "Mounted from 4thman/hagital" for several layers. Those layers (the Node base image) were already in my account from a previous project, so Docker Hub didn't need them uploaded again. Only the new layers were pushed.&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%2F0tqhuvfwqgx440x43h9t.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%2F0tqhuvfwqgx440x43h9t.png" alt=" " width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The repository now shows up at &lt;a href="https://hub.docker.com/r/4thman/notes-app" rel="noopener noreferrer"&gt;hub.docker.com/r/4thman/notes-app&lt;/a&gt; with the &lt;code&gt;V1&lt;/code&gt; tag.&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%2Figr9kyhdt2p5aiyf1mk1.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%2Figr9kyhdt2p5aiyf1mk1.png" alt=" " width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3 (the warm-up): Azure Container Instances
&lt;/h2&gt;

&lt;p&gt;Before App Service, I tried the simplest Azure option: ACI runs containers with no VM or orchestrator to manage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Register the resource providers
&lt;/h3&gt;

&lt;p&gt;On a fresh subscription, services often fail until their resource provider is registered. I registered the ones I'd need up front:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az provider register &lt;span class="nt"&gt;--namespace&lt;/span&gt; Microsoft.ContainerInstance
az provider register &lt;span class="nt"&gt;--namespace&lt;/span&gt; Microsoft.DBforPostgreSQL
az provider register &lt;span class="nt"&gt;--namespace&lt;/span&gt; Microsoft.Web

az provider show &lt;span class="nt"&gt;--namespace&lt;/span&gt; Microsoft.Web &lt;span class="nt"&gt;--query&lt;/span&gt; registrationState &lt;span class="nt"&gt;--output&lt;/span&gt; tsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last command printed &lt;code&gt;Registering&lt;/code&gt; and later &lt;code&gt;Registered&lt;/code&gt;. Registration takes a minute or two, so check the state before moving on.&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%2Fzr43k6z5zzt51lf0n43p.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%2Fzr43k6z5zzt51lf0n43p.png" alt=" " width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Variables and a resource group
&lt;/h3&gt;

&lt;p&gt;To avoid retyping, I set shell variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DOCKER_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"4thman"&lt;/span&gt;
&lt;span class="nv"&gt;SUFFIX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your-unique-suffix&amp;gt;"&lt;/span&gt;
&lt;span class="nv"&gt;LOCATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"eastus"&lt;/span&gt;
&lt;span class="nv"&gt;DB_PASS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;a-strong-password&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fjo5hh158aaq0vy1scxik.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%2Fjo5hh158aaq0vy1scxik.png" alt=" " width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Checking that they were set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$DOCKER_USER&lt;/span&gt; &lt;span class="nv"&gt;$SUFFIX&lt;/span&gt; &lt;span class="nv"&gt;$LOCATION&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fnt5rqa0669izlyt50fm5.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%2Fnt5rqa0669izlyt50fm5.png" alt=" " width="800" height="263"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then the resource group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group create &lt;span class="nt"&gt;--name&lt;/span&gt; notes-app-rg &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nv"&gt;$LOCATION&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;provisioningState: Succeeded&lt;/code&gt; in the output confirmed the group existed.&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%2Fsl63lc2yybzurmaz8k9q.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%2Fsl63lc2yybzurmaz8k9q.png" alt=" " width="799" height="242"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The YAML deployment file
&lt;/h3&gt;

&lt;p&gt;ACI can run several containers in one &lt;strong&gt;container group&lt;/strong&gt;, and containers in a group share a network. That means the app can reach Postgres on &lt;code&gt;localhost&lt;/code&gt;. I described the group in &lt;code&gt;aci-notes.yaml&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%2Fw6cge3933r1vv28nvyrb.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%2Fw6cge3933r1vv28nvyrb.png" alt=" " width="799" height="406"&gt;&lt;/a&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2021-10-01&lt;/span&gt;
&lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eastus&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;notes-app-group&lt;/span&gt;
&lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&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;app&lt;/span&gt;
      &lt;span class="na"&gt;properties&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;4thman/notes-app:V1&lt;/span&gt;
        &lt;span class="na"&gt;command&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;/bin/sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sleep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;20&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;server.js"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;environmentVariables&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;DB_HOST&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost&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;DB_PORT&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432"&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;DB_USER&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appuser&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;DB_PASSWORD&lt;/span&gt;
            &lt;span class="na"&gt;secureValue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;your-password&amp;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;DB_NAME&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;notesdb&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="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&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;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.0&lt;/span&gt;
            &lt;span class="na"&gt;memoryInGb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.0&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;db&lt;/span&gt;
      &lt;span class="na"&gt;properties&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;postgres:16-alpine&lt;/span&gt;
        &lt;span class="na"&gt;environmentVariables&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;POSTGRES_USER&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appuser&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;POSTGRES_PASSWORD&lt;/span&gt;
            &lt;span class="na"&gt;secureValue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;your-password&amp;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;POSTGRES_DB&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;notesdb&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="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&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;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.0&lt;/span&gt;
            &lt;span class="na"&gt;memoryInGb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.0&lt;/span&gt;
  &lt;span class="na"&gt;osType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Linux&lt;/span&gt;
  &lt;span class="na"&gt;ipAddress&lt;/span&gt;&lt;span class="pi"&gt;:&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;Public&lt;/span&gt;
    &lt;span class="na"&gt;dnsNameLabel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;notes-app-&amp;lt;your-suffix&amp;gt;&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="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tcp&lt;/span&gt;
        &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
  &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Always&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;Microsoft.ContainerInstance/containerGroups&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first half of the file (the app and db containers):&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%2Fm5vv7w8w16pvbd5twlfh.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%2Fm5vv7w8w16pvbd5twlfh.png" alt=" " width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The second half (public IP, DNS label, port and restart policy):&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%2Fnfkej7zq189rqo5rjgmi.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%2Fnfkej7zq189rqo5rjgmi.png" alt=" " width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two things I want to explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sleep 20 &amp;amp;&amp;amp; node server.js&lt;/code&gt;.&lt;/strong&gt; ACI has no equivalent of Compose's &lt;code&gt;depends_on&lt;/code&gt;. Both containers start at the same time, and Postgres needs a few seconds before it accepts connections. The delay gives the database time to be ready before the app tries to create its table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;secureValue&lt;/code&gt;&lt;/strong&gt; instead of &lt;code&gt;value&lt;/code&gt; for passwords, so they aren't shown in the container group's properties.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az container create &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-app-rg &lt;span class="nt"&gt;--file&lt;/span&gt; aci-notes.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;provisioningState: Succeeded&lt;/code&gt; came back:&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%2Fezc2041wveb5l6uor26v.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%2Fezc2041wveb5l6uor26v.png" alt=" " width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I grabbed the address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az container show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-app-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; notes-app-group &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; ipAddress.fqdn &lt;span class="nt"&gt;--output&lt;/span&gt; tsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fj7fh0j5s7m10raqveg1n.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%2Fj7fh0j5s7m10raqveg1n.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And checked the logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az container logs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-app-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; notes-app-group &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--container-name&lt;/span&gt; app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logs showed exactly what I wanted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Notes app running on port 3000
Database initialized (notes table ready)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fwdmyvjtu05snh2r073ma.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%2Fwdmyvjtu05snh2r073ma.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I opened &lt;code&gt;http://notes-app-&amp;lt;suffix&amp;gt;.eastus.azurecontainer.io:3000&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%2Fkxkgj5gsqugnpl24vvqc.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%2Fkxkgj5gsqugnpl24vvqc.png" alt=" " width="800" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I added a few notes to test it, and they were saved:&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%2Fbccxnxtw2oa5dr18m70d.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%2Fbccxnxtw2oa5dr18m70d.png" alt=" " width="799" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I moved on
&lt;/h3&gt;

&lt;p&gt;ACI is great for quick demos, but this setup has no volume for Postgres. If the group restarts, the database starts empty. Also, a database in a container is the wrong model for anything you care about. So I deleted it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az container delete &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-app-rg &lt;span class="nt"&gt;--name&lt;/span&gt; notes-app-group &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F3k1xa327e0ihmf9xd76p.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%2F3k1xa327e0ihmf9xd76p.png" alt=" " width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Reloading the URL now gave &lt;code&gt;DNS_PROBE_FINISHED_NXDOMAIN&lt;/code&gt;, which confirms the DNS name went away with the container:&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%2Fk8lviienm4khbp8ecxmk.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%2Fk8lviienm4khbp8ecxmk.png" alt=" " width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I removed the whole resource group and created a fresh one for the next attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group delete &lt;span class="nt"&gt;--name&lt;/span&gt; notes-app-rg &lt;span class="nt"&gt;--yes&lt;/span&gt; &lt;span class="nt"&gt;--no-wait&lt;/span&gt;
az group create &lt;span class="nt"&gt;--name&lt;/span&gt; notes-prod-rg &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nv"&gt;$LOCATION&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fvkjfseds9o94rjp4k048.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%2Fvkjfseds9o94rjp4k048.png" alt=" " width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4: Azure App Service + Azure Database for PostgreSQL
&lt;/h2&gt;

&lt;p&gt;This is the "proper" cloud setup. Azure runs the database and the web hosting, and I only supply the image and the configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create the managed PostgreSQL server
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az postgres flexible-server create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-prod-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; notes-db-&lt;span class="nv"&gt;$SUFFIX&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt; eastus2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--admin-user&lt;/span&gt; appuser &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--admin-password&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB_PASS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sku-name&lt;/span&gt; Standard_B1ms &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tier&lt;/span&gt; Burstable &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; 16 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--public-access&lt;/span&gt; 0.0.0.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What those flags mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Standard_B1ms&lt;/code&gt; and &lt;code&gt;Burstable&lt;/code&gt; is the cheap tier, fine for a demo.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--public-access 0.0.0.0&lt;/code&gt; creates a firewall rule that allows connections from other Azure services. That's how App Service will reach the database.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--version 16&lt;/code&gt; matches the Postgres version I used locally.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CLI also warns that the output includes your password in plain text. Treat that output as sensitive and never paste it into a blog post.&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%2Fx4rz8kneiiyo2mn462fs.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%2Fx4rz8kneiiyo2mn462fs.png" alt=" " width="799" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I created the application database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az postgres flexible-server db create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-prod-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--server-name&lt;/span&gt; notes-db-&lt;span class="nv"&gt;$SUFFIX&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; notesdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F3pyqm1xbh3di8jvn23fg.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%2F3pyqm1xbh3di8jvn23fg.png" alt=" " width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And captured the server address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DB_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az postgres flexible-server show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-prod-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; notes-db-&lt;span class="nv"&gt;$SUFFIX&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; fullyQualifiedDomainName &lt;span class="nt"&gt;--output&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$DB_HOST&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server showed state &lt;strong&gt;Ready&lt;/strong&gt; with a host like &lt;code&gt;notes-db-&amp;lt;suffix&amp;gt;.postgres.database.azure.com&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%2Fm7i21afxz9uio8rwsoni.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%2Fm7i21afxz9uio8rwsoni.png" alt=" " width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Create the App Service plan
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Error #2: free-tier quota in East US
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az appservice plan create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; notes-app-plan &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-prod-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--is-linux&lt;/span&gt; &lt;span class="nt"&gt;--sku&lt;/span&gt; F1 &lt;span class="nt"&gt;--location&lt;/span&gt; eastus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Current Limit (F1 VMs): 0
Amount required for this deployment (F1 VMs): 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; my subscription had zero free-tier (F1) capacity in East US.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; I tried another region instead of fighting for a quota increase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az appservice plan create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; notes-app-plan &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-prod-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--is-linux&lt;/span&gt; &lt;span class="nt"&gt;--sku&lt;/span&gt; F1 &lt;span class="nt"&gt;--location&lt;/span&gt; centralus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That worked (&lt;code&gt;provisioningState: Succeeded&lt;/code&gt;). The trade-off is that my app (Central US) and database (East US 2) are now in different regions, which adds a little latency. For production, you'd want them in the same region.&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%2Foqxw4uwns5fc0op96vp5.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%2Foqxw4uwns5fc0op96vp5.png" alt=" " width="800" height="598"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Create the web app from the Docker Hub image
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az webapp create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-prod-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--plan&lt;/span&gt; notes-app-plan &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; notes-app-&lt;span class="nv"&gt;$SUFFIX&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--deployment-container-image-name&lt;/span&gt; 4thman/notes-app:V1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI warned me that &lt;code&gt;--deployment-container-image-name&lt;/code&gt; is deprecated and will be removed in a future release, so check &lt;code&gt;az webapp create --help&lt;/code&gt; for the current flag name.&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%2F17oucvt2sr727mbn6kap.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%2F17oucvt2sr727mbn6kap.png" alt=" " width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Configure the app
&lt;/h3&gt;

&lt;p&gt;The container reads its database settings from environment variables, so I set them as App Service application settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az webapp config appsettings &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-prod-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; notes-app-&lt;span class="nv"&gt;$SUFFIX&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--settings&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;DB_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB_HOST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;DB_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"5432"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;DB_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"appuser"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB_PASS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;DB_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"notesdb"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;DB_SSL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;PGSSLMODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"require"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;WEBSITES_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"3000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three of these settings matter more than they look:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DB_SSL=true&lt;/code&gt;&lt;/strong&gt; turns on the SSL option in my &lt;code&gt;pg&lt;/code&gt; pool. Azure's managed Postgres rejects unencrypted connections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WEBSITES_PORT=3000&lt;/code&gt;&lt;/strong&gt; tells App Service which port my container listens on. Without it, App Service assumes port 80 and can't find the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DB_HOST&lt;/code&gt;&lt;/strong&gt; is now the managed server's full address instead of &lt;code&gt;db&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fze23mkvar18lk8fdlx0s.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%2Fze23mkvar18lk8fdlx0s.png" alt=" " width="800" height="598"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;)&lt;/p&gt;

&lt;p&gt;Then I restarted the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az webapp restart &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-prod-rg &lt;span class="nt"&gt;--name&lt;/span&gt; notes-app-&lt;span class="nv"&gt;$SUFFIX&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F58u7of58n3gfzzwshdo5.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%2F58u7of58n3gfzzwshdo5.png" alt=" " width="800" height="171"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And printed the site URL to open it:&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%2F1ptmezu4so0e50jxomta.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%2F1ptmezu4so0e50jxomta.png" alt=" " width="799" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Error #3: 503 Service Unavailable
&lt;/h3&gt;

&lt;p&gt;I opened &lt;code&gt;https://notes-app-&amp;lt;suffix&amp;gt;.azurewebsites.net&lt;/code&gt; and got:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;503 Service Unavailable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Finve8o7ifkw666d1ocdv.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%2Finve8o7ifkw666d1ocdv.png" alt=" " width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is where I did some actual debugging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First suspect: the database firewall.&lt;/strong&gt; I added an explicit rule allowing Azure services and restarted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az postgres flexible-server firewall-rule create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-prod-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--server-name&lt;/span&gt; notes-db-&lt;span class="nv"&gt;$SUFFIX&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; AllowAllAzureIPs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--start-ip-address&lt;/span&gt; 0.0.0.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--end-ip-address&lt;/span&gt; 0.0.0.0

az webapp restart &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-prod-rg &lt;span class="nt"&gt;--name&lt;/span&gt; notes-app-&lt;span class="nv"&gt;$SUFFIX&lt;/span&gt;
az webapp start &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-prod-rg &lt;span class="nt"&gt;--name&lt;/span&gt; notes-app-&lt;span class="nv"&gt;$SUFFIX&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looking back, this rule was redundant. The &lt;code&gt;--public-access 0.0.0.0&lt;/code&gt; flag at server creation had already created an equivalent one. Still, ruling out the firewall was worth the minute it took.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second suspect: the app's actual state.&lt;/strong&gt; Instead of guessing, I asked Azure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az webapp show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-prod-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; notes-app-&lt;span class="nv"&gt;$SUFFIX&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"state"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The answer was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"QuotaExceeded"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; the Free (F1) tier has a daily compute quota. The app had used it up, and Azure stopped it. The 503 was only a symptom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; move the plan up one tier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az appservice plan update &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; notes-app-plan &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-prod-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sku&lt;/span&gt; B1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;B1 is a paid tier with no daily CPU quota. For a free-tier demo, remember that F1 can quietly stop your app.&lt;/p&gt;

&lt;p&gt;The firewall rule, the restart and start attempts, the &lt;code&gt;QuotaExceeded&lt;/code&gt; state and the plan upgrade:&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%2Fagvg705lz9uuwulcmyxi.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%2Fagvg705lz9uuwulcmyxi.png" alt=" " width="800" height="616"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Along the way the browser showed a different message, because Azure had stopped the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error 403 - This web app is stopped.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fjevnpvne6nqwbhor0drb.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%2Fjevnpvne6nqwbhor0drb.png" alt=" " width="799" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Re-point the container and restart
&lt;/h3&gt;

&lt;p&gt;With the plan upgraded, I re-applied the container configuration explicitly, pointing at the exact image tag, and restarted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az webapp config container &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-prod-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; notes-app-&lt;span class="nv"&gt;$SUFFIX&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--docker-custom-image-name&lt;/span&gt; 4thman/notes-app:V1

az webapp restart &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-prod-rg &lt;span class="nt"&gt;--name&lt;/span&gt; notes-app-&lt;span class="nv"&gt;$SUFFIX&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the tag: &lt;code&gt;V1&lt;/code&gt; with a capital V. Docker tags are case-sensitive, and some of my earlier commands used lowercase &lt;code&gt;v1&lt;/code&gt; while the tag I actually pushed is &lt;code&gt;V1&lt;/code&gt;. I corrected these as I went, but it's a classic way to get "image not found" errors, so copy the tag exactly.&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%2Ffzc0gq1hjds4hh3k4jwd.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%2Ffzc0gq1hjds4hh3k4jwd.png" alt=" " width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This time the site came up on &lt;code&gt;https://notes-app-&amp;lt;suffix&amp;gt;.azurewebsites.net&lt;/code&gt;, with HTTPS provided by Azure for free:&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%2F3dlnubh8o66vcwbi7v9e.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%2F3dlnubh8o66vcwbi7v9e.png" alt=" " width="799" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I added a note ("my name" / "My name is David"), refreshed, and it persisted in Azure Database for PostgreSQL:&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%2Fbzo9le2o54plgjlho85t.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%2Fbzo9le2o54plgjlho85t.png" alt=" " width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleanup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group delete &lt;span class="nt"&gt;--name&lt;/span&gt; notes-prod-rg &lt;span class="nt"&gt;--yes&lt;/span&gt; &lt;span class="nt"&gt;--no-wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deleting the resource group removes the plan, web app and database together, so nothing keeps billing.&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%2F8qlcyqi3jtglypqo33mh.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%2F8qlcyqi3jtglypqo33mh.png" alt=" " width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5: Azure Virtual Machine + Docker Compose
&lt;/h2&gt;

&lt;p&gt;The third approach gives you the most control and the most responsibility. It's just a Linux server, and I install everything myself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create the VM
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group create &lt;span class="nt"&gt;--name&lt;/span&gt; notes-vm-rg &lt;span class="nt"&gt;--location&lt;/span&gt; eastus

az vm create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-vm-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; notes-vm &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; Ubuntu2204 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--admin-username&lt;/span&gt; azureuser &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--generate-ssh-keys&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--size&lt;/span&gt; Standard_B1s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--generate-ssh-keys&lt;/code&gt; creates an SSH key pair, so there's no password login to worry about. The output shows the VM running with a private and public IP.&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%2Fyzm438f6cfn3e1na56iw.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%2Fyzm438f6cfn3e1na56iw.png" alt=" " width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Open port 3000
&lt;/h3&gt;

&lt;p&gt;A new VM blocks inbound traffic by default. The app listens on 3000, so I opened that port in the network security group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az vm open-port &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-vm-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; notes-vm &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output lists a new inbound rule called &lt;code&gt;open-port-3000&lt;/code&gt; that allows TCP traffic to port 3000.&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%2Fehe9f6l3sxh268dw5bvg.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%2Fehe9f6l3sxh268dw5bvg.png" alt=" " width="799" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect with SSH
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;VM_IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az vm show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; notes-vm-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; notes-vm &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--show-details&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; publicIps &lt;span class="nt"&gt;--output&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

ssh azureuser@&lt;span class="nv"&gt;$VM_IP&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt changed to &lt;code&gt;azureuser@notes-vm:~$&lt;/code&gt;, which means I was now inside the VM.&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%2Fumcyecup2ze0rpyv32ro.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%2Fumcyecup2ze0rpyv32ro.png" alt=" " width="800" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Docker
&lt;/h3&gt;

&lt;p&gt;I followed Docker's official apt repository instructions. First, update the package index:&lt;br&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;apt-get update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F46uutvg6ka9jxvamhfv1.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%2F46uutvg6ka9jxvamhfv1.png" alt=" " width="800" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then the prerequisites:&lt;br&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;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ca-certificates curl gnupg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Feq7n82lkxr2c19ghu73p.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%2Feq7n82lkxr2c19ghu73p.png" alt=" " width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, Docker's signing key and repository:&lt;br&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 install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/apt/keyrings
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/ubuntu/gpg | &lt;span class="nb"&gt;sudo &lt;/span&gt;gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/docker.gpg
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;a+r /etc/apt/keyrings/docker.gpg

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"deb [arch=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; stable"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/docker.list &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key lets apt verify that packages are genuine, and the &lt;code&gt;echo&lt;/code&gt; adds Docker's repository to apt's sources.&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%2Fqwo1b9m3ii1sm06gkn6b.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%2Fqwo1b9m3ii1sm06gkn6b.png" alt=" " width="800" height="198"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, install the engine, the CLI, containerd and the Compose plugin:&lt;br&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;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker-ce docker-ce-cli containerd.io docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fc3gq3r153exb1xhdhush.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%2Fc3gq3r153exb1xhdhush.png" alt=" " width="800" height="491"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Error #4: Docker permission denied (the group problem)
&lt;/h3&gt;

&lt;p&gt;By default only root can talk to the Docker daemon, which means every command needs &lt;code&gt;sudo&lt;/code&gt;. The fix is adding your user to the &lt;code&gt;docker&lt;/code&gt; group:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Group changes normally apply on your next login. &lt;code&gt;newgrp docker&lt;/code&gt; applies it to the current shell immediately, so you don't have to log out and back in. Then I verified everything:&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="nt"&gt;--version&lt;/span&gt;          &lt;span class="c"&gt;# Docker version 29.8.2&lt;/span&gt;
docker compose version    &lt;span class="c"&gt;# Docker Compose version v5.5.1&lt;/span&gt;
docker ps                 &lt;span class="c"&gt;# empty list, no permission errors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Ff6235bn0lhtntie8wto7.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%2Ff6235bn0lhtntie8wto7.png" alt=" " width="800" height="184"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy with Compose
&lt;/h3&gt;

&lt;p&gt;On the VM I created a folder and a Compose file that pulls my image from Docker Hub, with no build step needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;notes-app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;notes-app
nano docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&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;4thman/notes-app:V1&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;sh -c "sleep 15 &amp;amp;&amp;amp; node server.js"&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;3000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="na"&gt;DB_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
      &lt;span class="na"&gt;DB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appuser&lt;/span&gt;
      &lt;span class="na"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;your-password&amp;gt;&lt;/span&gt;
      &lt;span class="na"&gt;DB_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;notesdb&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&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;db&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;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appuser&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;your-password&amp;gt;&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;notesdb&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db_data:/var/lib/postgresql/data&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;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fkgkh4pi7unomtx7g7l3d.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%2Fkgkh4pi7unomtx7g7l3d.png" alt=" " width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice the VM runs its own Postgres container, not Azure's managed database. The extra &lt;code&gt;sleep 15&lt;/code&gt; gives Postgres time to initialise on first start, since &lt;code&gt;depends_on&lt;/code&gt; only waits for the container to &lt;em&gt;start&lt;/em&gt;, not for the database to be &lt;em&gt;ready&lt;/em&gt;.&lt;/p&gt;

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

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

&lt;/div&gt;



&lt;p&gt;Compose pulled both images (&lt;code&gt;postgres:16-alpine&lt;/code&gt; and &lt;code&gt;4thman/notes-app:V1&lt;/code&gt;), created the network and volume, and started both containers. &lt;code&gt;docker compose ps&lt;/code&gt; showed the app mapped to &lt;code&gt;0.0.0.0:3000-&amp;gt;3000/tcp&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%2Fekow5ofjnd5ti7b1po8y.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%2Fekow5ofjnd5ti7b1po8y.png" alt=" " width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I opened &lt;code&gt;http://&amp;lt;VM-IP&amp;gt;:3000&lt;/code&gt; and there was the Notes app, running on a server I had built from scratch. I filled in a note titled "hello from the vm" to try it out.&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%2F4fn4gonujiow09a67fnw.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%2F4fn4gonujiow09a67fnw.png" alt=" " width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleanup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group delete &lt;span class="nt"&gt;--name&lt;/span&gt; notes-vm-rg &lt;span class="nt"&gt;--yes&lt;/span&gt; &lt;span class="nt"&gt;--no-wait&lt;/span&gt;
az group list &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the group showed &lt;code&gt;Deleting&lt;/code&gt;, I also noticed a &lt;code&gt;NetworkWatcherRG&lt;/code&gt; group that Azure creates automatically when you build networks. I deleted that too and re-listed groups until both were gone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group delete &lt;span class="nt"&gt;--name&lt;/span&gt; NetworkWatcherRG &lt;span class="nt"&gt;--yes&lt;/span&gt; &lt;span class="nt"&gt;--no-wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fcbt6vmiiw3mx6paxkrgx.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%2Fcbt6vmiiw3mx6paxkrgx.png" alt=" " width="800" height="662"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Pushing the project to GitHub
&lt;/h2&gt;

&lt;p&gt;Finally, I put everything under version control. I created a &lt;code&gt;.gitignore&lt;/code&gt;, initialised the repository and made the first commit:&lt;br&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;touch&lt;/span&gt; .gitignore
git init
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"saving the files for the first time"&lt;/span&gt;
git branch main
git switch main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fa1utqrfu6b0nwhn48avq.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%2Fa1utqrfu6b0nwhn48avq.png" alt=" " width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I created an empty repository on GitHub:&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%2F3koulp1dfcufypus61zq.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%2F3koulp1dfcufypus61zq.png" alt=" " width="799" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And connected and pushed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote add origin https://github.com/4thman/4thman-dockerised-notes-web-application.git
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F93kbilqunwaruluqww4v.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%2F93kbilqunwaruluqww4v.png" alt=" " width="800" height="593"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Errors at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;What I saw&lt;/th&gt;
&lt;th&gt;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;1&lt;/td&gt;
&lt;td&gt;Container restarting, &lt;code&gt;Cannot find module 'pg'&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Driver missing from &lt;code&gt;package.json&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npm install express pg&lt;/code&gt;, then &lt;code&gt;docker compose up -d --build&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;F1 quota limit 0 in East US&lt;/td&gt;
&lt;td&gt;No free-tier capacity in that region&lt;/td&gt;
&lt;td&gt;Created the plan in Central US&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;503 Service Unavailable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;App had been stopped (&lt;code&gt;QuotaExceeded&lt;/code&gt; on the F1 tier)&lt;/td&gt;
&lt;td&gt;Upgraded the plan to B1, re-set the container, restarted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Docker commands need &lt;code&gt;sudo&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;User not in the &lt;code&gt;docker&lt;/code&gt; group&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sudo usermod -aG docker $USER&lt;/code&gt; and &lt;code&gt;newgrp docker&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Risk of "image not found"&lt;/td&gt;
&lt;td&gt;Tag case mismatch (&lt;code&gt;v1&lt;/code&gt; vs &lt;code&gt;V1&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Used the exact tag &lt;code&gt;V1&lt;/code&gt; everywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;App crashing before the DB was ready&lt;/td&gt;
&lt;td&gt;Containers start at the same time&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sleep&lt;/code&gt; before &lt;code&gt;node server.js&lt;/code&gt; (ACI and VM)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Configuration through environment variables is the whole game.&lt;/strong&gt; One image ran in three environments because nothing environment-specific was baked into it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the logs before you change anything.&lt;/strong&gt; &lt;code&gt;docker compose logs&lt;/code&gt;, &lt;code&gt;az container logs&lt;/code&gt; and &lt;code&gt;az webapp show --query state&lt;/code&gt; each pointed straight at the problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A symptom isn't a cause.&lt;/strong&gt; The 503 looked like a database or networking issue. It was a billing-tier quota.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed services save work but add settings.&lt;/strong&gt; App Service handled HTTPS and the OS, but I had to learn about &lt;code&gt;WEBSITES_PORT&lt;/code&gt;, SSL and plan tiers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VMs give control and take your time.&lt;/strong&gt; Docker installation, permissions, firewall rules and updates were all mine to handle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean up after yourself.&lt;/strong&gt; Deleting resource groups is the easiest way to avoid surprise bills.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Your Turn
&lt;/h3&gt;

&lt;p&gt;That’s where I’ll leave this one.&lt;/p&gt;

&lt;p&gt;I started with a Notes app running on my machine and ended up deploying the same Docker image through ACI, Azure App Service with managed PostgreSQL, and an Azure VM. Along the way, I broke things, read the logs, fixed them, and learned something new from every failure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/4thman/4thman-dockerised-notes-web-application.git" rel="noopener noreferrer"&gt;github.com/4thman/4thman-dockerised-notes-web-application&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image:&lt;/strong&gt; &lt;code&gt;docker pull 4thman/notes-app:V1&lt;/code&gt; (&lt;a href="https://hub.docker.com/r/4thman/notes-app" rel="noopener noreferrer"&gt;Docker Hub&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Hit one of these errors while deploying your own Docker app? Or did you find a smarter fix that I missed? Drop your experience, workaround, or “I’ve been there” story in the comments. I’d love to compare notes and learn from you.&lt;/p&gt;

&lt;p&gt;Make sure you kindly, &lt;strong&gt;give it a like, share it with someone learning Docker or Azure, and let me know what you’d like to see me build or break next.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Until the next deployment, &lt;strong&gt;keep building, keep testing, and don’t be afraid of the errors. They’re often where the real learning happens.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>postgres</category>
      <category>devops</category>
      <category>azure</category>
    </item>
    <item>
      <title>Dockerizing a Node App &amp; Shipping It to Azure: Build, Break, Fix &amp; Ship (3 Errors, Zero Regrets) 🐳</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Sat, 26 Sep 2026 00:33:05 +0000</pubDate>
      <link>https://dev.to/4thman/-catching-container-vibes-dockerizing-a-node-app-shipping-it-to-azure-3-errors-zero-regrets-p02</link>
      <guid>https://dev.to/4thman/-catching-container-vibes-dockerizing-a-node-app-shipping-it-to-azure-3-errors-zero-regrets-p02</guid>
      <description>&lt;p&gt;I wanted a hands-on project to actually cement what I'd been learning in Docker and Azure, build something small, break it, fix it, and ship it for real. So I built &lt;strong&gt;Container Vibes&lt;/strong&gt;: a tiny Express app that hands you a random "vibe" (a color theme + a little quote) every time you click a button. It's deliberately simple. The point was never the app it was everything around it: containerizing it properly, pushing it to a registry, and getting it running live on Azure Container Instances (ACI).&lt;/p&gt;

&lt;p&gt;This post walks through the entire build, step by step, in the exact order I actually did it including the three errors I ran into along the way and exactly how I diagnosed and fixed each one. &lt;/p&gt;

&lt;h2&gt;
  
  
  What we're building
&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%2Funx1yjo84gyadnjoik9f.jpg" 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%2Funx1yjo84gyadnjoik9f.jpg" alt=" " width="642" height="648"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A small Node.js/Express app with three routes: a homepage, a &lt;code&gt;/health&lt;/code&gt; check, and a &lt;code&gt;/api/vibe&lt;/code&gt; endpoint that returns a random theme&lt;/li&gt;
&lt;li&gt;A Docker image for it, tested locally&lt;/li&gt;
&lt;li&gt;The image pushed up to Docker Hub as the deployable artifact&lt;/li&gt;
&lt;li&gt;The container running live on Azure Container Instances, reachable at a real URL&lt;/li&gt;
&lt;li&gt;The source code version-controlled and pushed to GitHub&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1 — Laying the groundwork
&lt;/h2&gt;

&lt;p&gt;First thing, I set up a clean folder structure so everything had a home:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;docker-project-1 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;docker-project-1
code &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That opens the empty folder straight into VS Code. From there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;hagital &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;hagital
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;touch &lt;/span&gt;Dockerfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;npm init -y&lt;/code&gt; scaffolds a &lt;code&gt;package.json&lt;/code&gt; with all the defaults accepted automatically no need to answer the usual prompts one by one. This file is what tells both Node and, later, Docker's &lt;code&gt;npm install&lt;/code&gt; step exactly which dependencies the project needs, so getting it created early matters. I also touched an empty &lt;code&gt;Dockerfile&lt;/code&gt; right away, just as a placeholder for what was coming.&lt;/p&gt;

&lt;p&gt;Then the actual app files:&lt;br&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;touch &lt;/span&gt;server.js .dockerignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fapcrxefzdqblfy35dnw2.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%2Fapcrxefzdqblfy35dnw2.png" alt=" " width="786" height="452"&gt;&lt;/a&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%2Fbfggh8tsoww7htx1xerg.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%2Fbfggh8tsoww7htx1xerg.png" alt=" " width="800" height="435"&gt;&lt;/a&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%2Fvgfogp9j8bohin8haigm.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%2Fvgfogp9j8bohin8haigm.png" alt=" " width="799" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Writing the app
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;server.js&lt;/code&gt; is where all the actual logic lives. It's a small Express server holding a roster of "vibes" each one a name, a pair of gradient colors, and a short quote plus two routes: one that picks a vibe at random, and a health check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// A little roster of "container vibes" — color themes + quotes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VIBES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sunset Deploy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#FF6B6B&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#FFD93D&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Life is good and could be challenging — ship it anyway.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ocean Rollout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#4ECDC4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#556270&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Every container has a bad day. Restart policy: always.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Neon Nightshift&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#8E2DE2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#4A00E0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Logs don't lie. Neither do good tests.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Forest Uptime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#11998e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#38ef7d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Healthy pods, happy engineers.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Candy CI/CD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#f857a6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ff5858&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Green pipeline, good vibes.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Golden Hour Git&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#f7971e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ffd200&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Commit small, dream big.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cosmic Cluster&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#0f0c29&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#302b63&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#24243e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Somewhere out there, a pod is scaling for you.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;static&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/vibe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vibe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;VIBES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;VIBES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;vibe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;uptime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uptime&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Life is good and could be challenging. But we are up.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`✨ Server running on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; — go visit http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth pointing out: &lt;code&gt;express.static&lt;/code&gt; serves everything in the &lt;code&gt;public&lt;/code&gt; folder (that's how the HTML page gets delivered), &lt;code&gt;/api/vibe&lt;/code&gt; picks a random entry from the &lt;code&gt;VIBES&lt;/code&gt; array and tacks on a live timestamp and the server's uptime so every response feels fresh, and &lt;code&gt;/health&lt;/code&gt; exists purely as a lightweight endpoint to confirm the server is alive which turned out to be genuinely useful later when checking on the container from Azure's side.&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%2Ff57a1m0ovil61hcxny3o.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%2Ff57a1m0ovil61hcxny3o.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Building the frontend
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;public &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;public
&lt;span class="nb"&gt;touch &lt;/span&gt;index.html
&lt;span class="nb"&gt;cd&lt;/span&gt; ..
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fhfnnlhpaori95qihz3g4.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%2Fhfnnlhpaori95qihz3g4.png" alt=" " width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;public/index.html&lt;/code&gt; is a single self-contained page: a gradient background that slowly animates through the current vibe's colors, a frosted-glass "card" in the center holding the vibe's name and quote, a small uptime readout, and a "New Vibe" button. Clicking that button calls &lt;code&gt;/api/vibe&lt;/code&gt;, gets back a fresh random theme as JSON, and updates the card's colors and text on the page without a reload.&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%2Fd4dl6ihd5apbnad5fodj.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%2Fd4dl6ihd5apbnad5fodj.png" alt=" " width="799" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Writing 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="c"&gt;# pull the official Node.js image from the Docker Hub&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;

&lt;span class="c"&gt;# set the working directory in the container&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# copy package.json and package-lock.json to the working directory&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;

&lt;span class="c"&gt;# install dependencies&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# copy the rest of the application code to the working directory&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# expose the port that the application will run on&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;

&lt;span class="c"&gt;# define the command to run the application&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; [ "node", "server.js" ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walking through it line by line: &lt;code&gt;FROM node:20-alpine&lt;/code&gt; grabs a minimal, lightweight Node 20 base image (the Alpine variant strips out everything not strictly needed, keeping the image small). &lt;code&gt;WORKDIR /app&lt;/code&gt; sets where all the following commands run inside the container. &lt;code&gt;COPY package*.json ./&lt;/code&gt; copies over just &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;package-lock.json&lt;/code&gt; first — deliberately, before the rest of the code so that &lt;code&gt;RUN npm install&lt;/code&gt; becomes its own cached layer. That means if I only change &lt;code&gt;server.js&lt;/code&gt; later and rebuild, Docker can reuse the already-installed &lt;code&gt;node_modules&lt;/code&gt; layer instead of reinstalling everything from scratch every single time. &lt;code&gt;COPY . .&lt;/code&gt; then brings in the rest of the app. &lt;code&gt;EXPOSE 3000&lt;/code&gt; documents which port the app listens on. And &lt;code&gt;CMD [ "node", "server.js" ]&lt;/code&gt; is what actually runs when a container starts from this image.&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%2Fofm3blogx85s6oz5igc9.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%2Fofm3blogx85s6oz5igc9.png" alt=" " width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — First build, first surprise
&lt;/h2&gt;

&lt;p&gt;With the Dockerfile in place, I moved back up to the project root and kicked off the build:&lt;br&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;cd&lt;/span&gt; ..
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; 4thman/hagital:v1 &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fzpmiva1cdzlraffh9dmm.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%2Fzpmiva1cdzlraffh9dmm.png" alt=" " width="800" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It built clean — all 11 steps finished, image tagged as &lt;code&gt;4thman/hagital:v1&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%2Fe2jzrv26eeyy3cbhkrbc.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%2Fe2jzrv26eeyy3cbhkrbc.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feeling good, I ran it and checked the images list first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker images
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 &lt;span class="nt"&gt;--name&lt;/span&gt; hagital-app 4thman/hagital:v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fdp7l9jy91qpxrll0bqxk.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%2Fdp7l9jy91qpxrll0bqxk.png" alt=" " width="799" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then opened &lt;code&gt;localhost:3000&lt;/code&gt; in the browser, fully expecting my little vibes card to show up. Instead: &lt;strong&gt;"This site can't be reached localhost refused to connect."&lt;/strong&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%2Fgf4p3xg1meyes61zhgn9.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%2Fgf4p3xg1meyes61zhgn9.png" alt=" " width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging: the case of the missing module
&lt;/h2&gt;

&lt;p&gt;A clean &lt;code&gt;docker build&lt;/code&gt; doesn't guarantee a working app that's the lesson this error taught me. First move was checking whether the container was even still alive:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;It showed status &lt;code&gt;Exited (1)&lt;/code&gt;, meaning the container had started and then immediately crashed. Time to check why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs hagital-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Cannot find module 'express'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there it was. I had written &lt;code&gt;require('express')&lt;/code&gt; in &lt;code&gt;server.js&lt;/code&gt;, but I had never actually run &lt;code&gt;npm install express&lt;/code&gt; on my own machine so &lt;code&gt;package.json&lt;/code&gt; had zero dependencies listed. When the Dockerfile's &lt;code&gt;RUN npm install&lt;/code&gt; step ran during the build, there was simply nothing in &lt;code&gt;package.json&lt;/code&gt; for it to install. The image built successfully because Docker didn't know anything was missing it only found out at runtime, the moment Node tried to &lt;code&gt;require()&lt;/code&gt; a package that was never there.&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%2F0dlnnyxtzbw5zjgabij8.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%2F0dlnnyxtzbw5zjgabij8.png" alt=" " width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;express
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That updates &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;package-lock.json&lt;/code&gt; with the real dependency. Then it was just a matter of rebuilding the image (this time you could see only the &lt;code&gt;WORKDIR&lt;/code&gt; layer got reused from cache everything from &lt;code&gt;COPY package*.json&lt;/code&gt; onward had to rerun, since the package files had actually changed) and swapping the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; 4thman/hagital:v1 &lt;span class="nb"&gt;.&lt;/span&gt;
docker &lt;span class="nb"&gt;rm &lt;/span&gt;hagital-app
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 &lt;span class="nt"&gt;--name&lt;/span&gt; hagital-app 4thman/hagital:v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fjqia8zblaicih51otiu0.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%2Fjqia8zblaicih51otiu0.png" alt=" " width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Success — locally, at last
&lt;/h2&gt;

&lt;p&gt;Refreshed the browser and there it was: Container Vibes, running from inside a container, gradient background and all.&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%2Fk5vc7hc1v4wz0pkqta2g.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%2Fk5vc7hc1v4wz0pkqta2g.png" alt=" " width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I went through all three routes to be thorough the homepage, the health check, and the vibe API directly:&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%2Fb9x8v384fgu231n67nnv.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%2Fb9x8v384fgu231n67nnv.png" alt=" " width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(homepage) · &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%2Fh6xkcm4h1ejalsradge8.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%2Fh6xkcm4h1ejalsradge8.png" alt=" " width="799" height="505"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(&lt;code&gt;/health&lt;/code&gt; returning &lt;code&gt;{"status":"ok",...}&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%2Ftlp51sfnqxhmrghgab9j.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%2Ftlp51sfnqxhmrghgab9j.png" alt=" " width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(&lt;code&gt;/api/vibe&lt;/code&gt; returning a random theme as JSON)*&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick concept check: EXPOSE vs -p vs --dns-name-label
&lt;/h2&gt;

&lt;p&gt;These three get mixed up a lot, but they each solve a different problem. &lt;code&gt;EXPOSE&lt;/code&gt; in the Dockerfile is really just documentation it tells anyone reading the image which port the app inside expects traffic on, but it doesn't actually open anything to the outside world by itself; that's exactly why the app could build fine and still be unreachable. The &lt;code&gt;-p&lt;/code&gt; flag on &lt;code&gt;docker run&lt;/code&gt; is the one doing the real work locally: it's the bridge that maps a port on your own machine to a port inside the container (&lt;code&gt;-p 3000:3000&lt;/code&gt; means "traffic hitting port 3000 on my laptop gets forwarded into the container's port 3000"), and without it your app can be running perfectly inside the container and you'd still get "connection refused" on the host. &lt;code&gt;--dns-name-label&lt;/code&gt;, which comes in later once we're on Azure, is a different beast entirely since Azure Container Instances hands your container a public IP that's a pain to remember and isn't guaranteed to stay the same, this flag lets you attach a friendly, stable subdomain to it instead (like &lt;code&gt;david-hagital.eastus.azurecontainer.io&lt;/code&gt;), so you're never hunting down a raw IP address to share a link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 — Cleaning up locally
&lt;/h2&gt;

&lt;p&gt;Before moving on, I ran through the basic container lifecycle commands just to get comfortable with them checking status, reading logs, stopping, and removing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps
docker logs hagital-app
docker stop hagital-app
docker &lt;span class="nb"&gt;rm &lt;/span&gt;hagital-app
docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last &lt;code&gt;docker ps&lt;/code&gt; came back empty, confirming the container was fully gone.&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%2Ficer6qyh7ytvai61n1im.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%2Ficer6qyh7ytvai61n1im.png" alt=" " width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7 — Logging into Docker Hub
&lt;/h2&gt;

&lt;p&gt;Next was getting the image somewhere Azure could actually reach it. Logged in, and while I was at it, ran &lt;code&gt;git init&lt;/code&gt; on the project too (even though I wouldn't actually commit anything until much later) and double-checked my &lt;code&gt;.dockerignore&lt;/code&gt; set to keep &lt;code&gt;.git&lt;/code&gt; and &lt;code&gt;.env&lt;/code&gt; out of the build context entirely, so neither ever ends up baked into the image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker login
git init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;.&lt;span class="n"&gt;dockerignore&lt;/span&gt;
.&lt;span class="n"&gt;git&lt;/span&gt;
.&lt;span class="n"&gt;env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fqlutecsga1pg11tf3nbo.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%2Fqlutecsga1pg11tf3nbo.png" alt=" " width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8 — Pushing to Docker Hub
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker push 4thman/hagital:v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watched each layer push up one by one until it finished with a digest and total size the image was now sitting on Docker Hub, ready to be pulled from anywhere, including Azure.&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%2Fty5riijepsm58mt83phl.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%2Fty5riijepsm58mt83phl.png" alt=" " width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9 — Logging into Azure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This opens a browser sign-in flow and, once authenticated, lists the subscriptions tied to the account so you can confirm which one the CLI is about to operate on.&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%2Fm6kpttqhtet84ujeq28f.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%2Fm6kpttqhtet84ujeq28f.png" alt=" " width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 10 — Creating the resource group (and hitting error #1)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group create &lt;span class="nt"&gt;--name&lt;/span&gt; david-docker-rg &lt;span class="nt"&gt;--location&lt;/span&gt; eastus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That part succeeded instantly — a resource group is just a logical folder in Azure to keep everything for this project together. Then I tried to actually create the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az container create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; david-docker-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; david-app-container &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; 4thman/hagital:v1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 3000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dns-name-label&lt;/span&gt; david-hagital &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpu&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--memory&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And hit the first Azure error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(MissingSubscriptionRegistration) The subscription is not registered to use namespace 'Microsoft.ContainerInstance'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F89svqvmx5whc4gexvhif.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%2F89svqvmx5whc4gexvhif.png" alt=" " width="799" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing error #1: the provider that wasn't switched on
&lt;/h2&gt;

&lt;p&gt;Every Azure subscription has to explicitly "register" the individual resource providers it wants to use before you're allowed to create resources of that type. Mine had simply never touched Container Instances before, so the &lt;code&gt;Microsoft.ContainerInstance&lt;/code&gt; namespace wasn't switched on yet. The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az provider register &lt;span class="nt"&gt;--namespace&lt;/span&gt; Microsoft.ContainerInstance
az provider show &lt;span class="nt"&gt;-n&lt;/span&gt; Microsoft.ContainerInstance &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"registrationState"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran that last command a couple of times, watching the state move from &lt;code&gt;"Registering"&lt;/code&gt; to &lt;code&gt;"Registered"&lt;/code&gt; before trying again.&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%2F02eno0lmwhfpkqyvs7tv.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%2F02eno0lmwhfpkqyvs7tv.png" alt=" " width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Error #2: pick an OS, any OS
&lt;/h2&gt;

&lt;p&gt;With the provider registered, I ran the exact same &lt;code&gt;az container create&lt;/code&gt; command again and hit a second, different error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(InvalidOsType) The 'osType' for container group '&amp;lt;null&amp;gt;' is invalid. The value must be one of 'Windows,Linux'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It turns out &lt;code&gt;az container create&lt;/code&gt; doesn't assume an operating system for you, you have to say so explicitly, every time. Adding &lt;code&gt;--os-type Linux&lt;/code&gt; to the command fixed it immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az container create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; david-docker-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; david-app-container &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; 4thman/hagital:v1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 3000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dns-name-label&lt;/span&gt; david-hagital &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--os-type&lt;/span&gt; Linux &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpu&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--memory&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fmij3ye5onfqsp7rkfm7t.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%2Fmij3ye5onfqsp7rkfm7t.png" alt=" " width="799" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  It worked — container created
&lt;/h2&gt;

&lt;p&gt;This time the command ran all the way through, returning the full container group JSON with &lt;code&gt;"provisioningState": "Succeeded"&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%2Fr1sz2rxhzoi45zy1bqvg.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%2Fr1sz2rxhzoi45zy1bqvg.png" alt=" " width="799" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 11 — Grabbing the live URL
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az container show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; david-docker-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; david-app-container &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; ipAddress.fqdn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; tsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returned &lt;code&gt;david-hagital.eastus.azurecontainer.io&lt;/code&gt; — the friendly subdomain the &lt;code&gt;--dns-name-label&lt;/code&gt; flag from earlier had reserved.&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%2Ftrat1fjco7gw86bjl7s8.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%2Ftrat1fjco7gw86bjl7s8.png" alt=" " width="799" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  It's alive on the internet 🎉
&lt;/h2&gt;

&lt;p&gt;Visited &lt;code&gt;david-hagital.eastus.azurecontainer.io:3000&lt;/code&gt; in the browser, and there it was Container Vibes, live, running the Forest Uptime theme.&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%2F0wg2svjqbuag2h8dt2gy.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%2F0wg2svjqbuag2h8dt2gy.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clicked "New Vibe" a few times to make sure the randomization actually worked over a real network call, not just locally and it did, cycling through themes like Golden Hour Git exactly as expected.&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%2Fnrwwwx2zeq2108lgjb6i.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%2Fnrwwwx2zeq2108lgjb6i.png" alt=" " width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 12 — Checking logs straight from Azure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az container logs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; david-docker-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; david-app-container
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pulled back the exact same startup message the app prints locally, confirming the container had booted cleanly on Azure's side too.&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%2Fb9sjqtho78ssif55vvbl.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%2Fb9sjqtho78ssif55vvbl.png" alt=" " width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 13 — Tearing it down
&lt;/h2&gt;

&lt;p&gt;Azure Container Instances bills for the time a container is actually running, so once I'd confirmed everything worked, I cleaned up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az container delete &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; david-docker-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; david-app-container &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fioovmcx8to4wyxwmmfh6.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%2Fioovmcx8to4wyxwmmfh6.png" alt=" " width="800" height="354"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group delete &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; david-docker-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--yes&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--no-wait&lt;/code&gt; just means the CLI hands control back to me immediately instead of sitting there until Azure finishes deleting everything in the background.&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%2F9toh9icrbqbnkj22nzsb.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%2F9toh9icrbqbnkj22nzsb.png" alt=" " width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 14 — Finally, version control
&lt;/h2&gt;

&lt;p&gt;With the app built, tested, containerized, pushed to Docker Hub, and successfully deployed and torn down on Azure, I went back and properly set up Git since none of the actual deployment steps above needed GitHub at all (Azure was pulling the image straight from Docker Hub), but I still wanted the source code backed up and version-controlled.&lt;/p&gt;

&lt;p&gt;Added a proper &lt;code&gt;.gitignore&lt;/code&gt; to keep &lt;code&gt;node_modules&lt;/code&gt; and &lt;code&gt;.env&lt;/code&gt; out of the repo, then checked status and staged everything:&lt;br&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;touch&lt;/span&gt; .gitignore
git status
git add &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fioylw4fqefl1if1vbag2.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%2Fioylw4fqefl1if1vbag2.png" alt=" " width="799" height="414"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Hagital Docker App"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven files went into that first commit: the &lt;code&gt;.dockerignore&lt;/code&gt;, &lt;code&gt;.gitignore&lt;/code&gt;, &lt;code&gt;Dockerfile&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;package-lock.json&lt;/code&gt;, &lt;code&gt;public/index.html&lt;/code&gt;, and &lt;code&gt;server.js&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%2F1j7ldrk4rl2xw964uod2.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%2F1j7ldrk4rl2xw964uod2.png" alt=" " width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Created a new private repository on GitHub named &lt;strong&gt;Hagital-Docker-App&lt;/strong&gt;, with a short description.&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%2Fg738qkuir8t5mqro68bw.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%2Fg738qkuir8t5mqro68bw.png" alt=" " width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then connected the local repo to it and pushed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git branch main
git switch main
git remote add origin https://github.com/4thman/Hagital-Docker-App.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Frdca1j0hpsff0mxldrw2.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%2Frdca1j0hpsff0mxldrw2.png" alt=" " width="800" height="418"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fdl0rl67lv5nc1y89sl57.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%2Fdl0rl67lv5nc1y89sl57.png" alt=" " width="799" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And confirmed everything landed correctly by checking the repo on GitHub,  all seven files present, matching exactly what was committed locally.&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%2Fd9v6ncodh7t9a90xxd5f.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%2Fd9v6ncodh7t9a90xxd5f.png" alt=" " width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past-me
&lt;/h2&gt;

&lt;p&gt;None of the three errors here were Docker or Azure being difficult — they were small gaps in my own checklist. Forgetting to run &lt;code&gt;npm install&lt;/code&gt; before building meant a "successful" build that shipped a broken app, and the fix was realizing that a clean build log means nothing if the container crashes the moment it actually starts. Forgetting that a fresh Azure subscription needs its resource providers registered cost me a re-run, and forgetting to specify an OS type cost me another. None of it was complicated once I actually read the error message properly — which, honestly, is most of DevOps in a nutshell: read the log, trust what it's telling you, fix that one specific thing, and try again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub repo:&lt;/strong&gt; &lt;a href="https://github.com/4thman/Hagital-Docker-App" rel="noopener noreferrer"&gt;github.com/4thman/Hagital-Docker-App&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Hub image:&lt;/strong&gt; &lt;a href="https://hub.docker.com/repository/docker/4thman/hagital/general" rel="noopener noreferrer"&gt;hub.docker.com/repository/docker/4thman/hagital&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you enjoyed following along, drop a ❤️, leave a comment, and share this with someone. I'd also love to know: what would you add or change in this project if you were building it yourself?&lt;/p&gt;

&lt;p&gt;Until the next build keep learning, keep breaking things, keep fixing them, and keep shipping. &lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>containers</category>
      <category>github</category>
    </item>
    <item>
      <title>I Built a One-Click Azure CI/CD Pipeline with Terraform &amp; GitHub Actions From git push to a Live Website</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Sun, 20 Sep 2026 01:33:32 +0000</pubDate>
      <link>https://dev.to/4thman/i-built-a-one-click-azure-cicd-pipeline-with-terraform-github-actions-from-git-push-to-a-live-1pj9</link>
      <guid>https://dev.to/4thman/i-built-a-one-click-azure-cicd-pipeline-with-terraform-github-actions-from-git-push-to-a-live-1pj9</guid>
      <description>&lt;p&gt;A few weeks ago I built an Azure VM with Terraform from my laptop. It worked, but I still had to SSH in, install nginx and copy my website over by hand.&lt;/p&gt;

&lt;p&gt;This time I wanted all of that to happen on its own. I run &lt;code&gt;git push&lt;/code&gt; from Git Bash, and a live website shows up on a brand new VM. The website is a copy of a public GitHub project called Omnifood, and the workflow clones it onto the server for me. No portal clicking, no SSH from me.&lt;/p&gt;

&lt;p&gt;It took me a few hours and a lot of red error messages. In this post I'll show you what worked, what broke, and how I destroyed everything at the end so Azure doesn't charge me for a VM I forgot about.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(The name "Hagital" comes from my school, Hagital Tech Institute. Use your own names when you follow along.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're building
&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%2F3k2wmhse081bhnwz9vpp.jpg" 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%2F3k2wmhse081bhnwz9vpp.jpg" alt=" " width="800" height="248"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git push from my laptop
   -&amp;gt; GitHub Actions wakes up
   -&amp;gt; Terraform builds the VM on Azure
   -&amp;gt; GitHub logs into the VM over SSH
   -&amp;gt; nginx is installed, then the website is cloned from GitHub over nginx's default page
   -&amp;gt; You open the IP address and see a live site
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/4thman/Hagital-project.git" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fbadge%2FMy%2520GitHub%2520Repo-181717%3Fstyle%3Dflat%26logo%3Dgithub%26logoColor%3Dwhite" alt="My GitHub repo" width="117" height="20"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And there's a second path for cleanup: click &lt;strong&gt;Run workflow&lt;/strong&gt;, and a destroy job deletes everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Words I'll use
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Terraform&lt;/strong&gt;: a tool where you describe your cloud stuff in files, and it builds it for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State file&lt;/strong&gt;: Terraform's notebook of what it has built so far.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service principal&lt;/strong&gt;: a robot account. GitHub uses it to log into Azure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secret&lt;/strong&gt;: a hidden value in GitHub (passwords, keys) that your workflow can use but nobody can read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow&lt;/strong&gt;: a YAML file that tells GitHub Actions what to run and when.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An Azure account and the Azure CLI&lt;/li&gt;
&lt;li&gt;Terraform installed&lt;/li&gt;
&lt;li&gt;VS Code and Git Bash (I'm on Windows)&lt;/li&gt;
&lt;li&gt;A GitHub account&lt;/li&gt;
&lt;li&gt;An SSH key pair&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My GitHub repo&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: Build the Terraform files on my laptop
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Make a folder and open it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;Hagital-Project1
&lt;span class="nb"&gt;cd &lt;/span&gt;Hagital-Project1
code &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F5xssj4k5y9aqpgtmgloq.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%2F5xssj4k5y9aqpgtmgloq.png" alt=" " width="800" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I made an empty &lt;code&gt;main.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch &lt;/span&gt;main.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F74cj60seuwvi2sju7sgb.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%2F74cj60seuwvi2sju7sgb.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Give Terraform a safe place for its notebook
&lt;/h3&gt;

&lt;p&gt;If the state file sits on my laptop, GitHub's computer can't see it. Then it wouldn't know what already exists and couldn't delete it later. So the state lives in an Azure storage account. I made three things with the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; terraform-state-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt; eastus

az storage account create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; hagitaldavid2026 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; terraform-state-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sku&lt;/span&gt; Standard_LRS

az storage container create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; tfstate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--account-name&lt;/span&gt; hagitaldavid2026 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--auth-mode&lt;/span&gt; login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Storage account names must be unique across all of Azure, so yours can't be &lt;code&gt;hagitaldavid2026&lt;/code&gt;. When you see &lt;code&gt;"created": true&lt;/code&gt;, you're good.&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%2F5g0loc7gyyw5xul5vq2g.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%2F5g0loc7gyyw5xul5vq2g.png" alt=" " width="799" height="270"&gt;&lt;/a&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%2Fsxa0hkfarcliomygwh0z.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%2Fsxa0hkfarcliomygwh0z.png" alt=" " width="799" height="263"&gt;&lt;/a&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%2Fy9xi6s9jph6ytmn2vujz.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%2Fy9xi6s9jph6ytmn2vujz.png" alt=" " width="799" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Provider and backend
&lt;/h3&gt;

&lt;p&gt;This goes at the top of &lt;code&gt;main.tf&lt;/code&gt;. The backend block tells Terraform where the notebook lives.&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;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;azurerm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/azurerm"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 5.4.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"azurerm"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;features&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="nx"&gt;subscription_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"YOUR-SUBSCRIPTION-ID"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"azurerm"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;resource_group_name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-state-rg"&lt;/span&gt;
    &lt;span class="nx"&gt;storage_account_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hagitaldavid2026"&lt;/span&gt;
    &lt;span class="nx"&gt;container_name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-project.tfstate"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&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%2F9vru7g3xioeki79l71et.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%2F9vru7g3xioeki79l71et.png" alt=" " width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The actual resources
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Resource group, network, subnet and public IP:&lt;/strong&gt;&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;"hagital_rg"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hagital-rg"&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"East US"&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_virtual_network"&lt;/span&gt; &lt;span class="s2"&gt;"hagital_vnet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hagital-vnet"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;address_space&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.0.0.0/16"&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_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"hagital_subnet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hagital-subnet"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;virtual_network_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_virtual_network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_vnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;address_prefixes&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.0.0.0/24"&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_public_ip"&lt;/span&gt; &lt;span class="s2"&gt;"hagital_ip"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hagital-ip"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;allocation_method&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Static"&lt;/span&gt;
  &lt;span class="nx"&gt;sku&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Standard"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F0vk5accxje1ixssieu25.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%2F0vk5accxje1ixssieu25.png" alt=" " width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network security group.&lt;/strong&gt; Think of it as a door guard. I opened port 22 for SSH and port 80 for the website:&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_network_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"hagital_nsg"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hagital-nsg"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;

  &lt;span class="nx"&gt;security_rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;                       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow-SSH"&lt;/span&gt;
    &lt;span class="nx"&gt;priority&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
    &lt;span class="nx"&gt;direction&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Inbound"&lt;/span&gt;
    &lt;span class="nx"&gt;access&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;source_port_range&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
    &lt;span class="nx"&gt;destination_port_range&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"22"&lt;/span&gt;
    &lt;span class="nx"&gt;source_address_prefix&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
    &lt;span class="nx"&gt;destination_address_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;security_rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;                       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow-HTTP"&lt;/span&gt;
    &lt;span class="nx"&gt;priority&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;
    &lt;span class="nx"&gt;direction&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Inbound"&lt;/span&gt;
    &lt;span class="nx"&gt;access&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;source_port_range&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
    &lt;span class="nx"&gt;destination_port_range&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"80"&lt;/span&gt;
    &lt;span class="nx"&gt;source_address_prefix&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
    &lt;span class="nx"&gt;destination_address_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&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%2F37aa300vq160fyl7jb13.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%2F37aa300vq160fyl7jb13.png" alt=" " width="799" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network card, tied to the NSG:&lt;/strong&gt;&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_network_interface"&lt;/span&gt; &lt;span class="s2"&gt;"hagital_nic"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hagital-nic"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;

  &lt;span class="nx"&gt;ip_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;                          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"internal"&lt;/span&gt;
    &lt;span class="nx"&gt;subnet_id&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_subnet&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;private_ip_address_allocation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Dynamic"&lt;/span&gt;
    &lt;span class="nx"&gt;public_ip_address_id&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_public_ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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_network_interface_security_group_association"&lt;/span&gt; &lt;span class="s2"&gt;"hagital_nic_nsg"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;network_interface_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_network_interface&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_nic&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;network_security_group_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_network_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_nsg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fio2gbmw9ef99e9jwifr3.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%2Fio2gbmw9ef99e9jwifr3.png" alt=" " width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Linux VM&lt;/strong&gt; (Ubuntu 22.04, small and cheap):&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_virtual_machine"&lt;/span&gt; &lt;span class="s2"&gt;"hagital_vm"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hagital-vm"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;size&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Standard_B1s"&lt;/span&gt;
  &lt;span class="nx"&gt;admin_username&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"adminuser"&lt;/span&gt;
  &lt;span class="nx"&gt;network_interface_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;azurerm_network_interface&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_nic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;os_disk&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;caching&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ReadWrite"&lt;/span&gt;
    &lt;span class="nx"&gt;storage_account_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Standard_LRS"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;source_image_reference&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;publisher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Canonical"&lt;/span&gt;
    &lt;span class="nx"&gt;offer&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0001-com-ubuntu-server-jammy"&lt;/span&gt;
    &lt;span class="nx"&gt;sku&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"22_04-LTS"&lt;/span&gt;
    &lt;span class="nx"&gt;version&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"latest"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;admin_ssh_key&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;username&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"adminuser"&lt;/span&gt;
    &lt;span class="nx"&gt;public_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"~/.ssh/id_rsa.pub"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&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%2Ficb0d61ub12nhioxlwgp.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%2Ficb0d61ub12nhioxlwgp.png" alt=" " width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Look at that last line, &lt;code&gt;file("~/.ssh/id_rsa.pub")&lt;/code&gt;. It reads the key from a path on my laptop. Remember it, because it causes trouble later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Init and plan
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init
terraform plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init&lt;/code&gt; set up the Azure backend and downloaded the provider. It also created a &lt;code&gt;.terraform&lt;/code&gt; folder and a &lt;code&gt;.terraform.lock.hcl&lt;/code&gt; file. The plan said &lt;strong&gt;8 to add, 0 to change, 0 to destroy&lt;/strong&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%2F59408te2v00txuu2lgrw.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%2F59408te2v00txuu2lgrw.png" alt=" " width="800" height="429"&gt;&lt;/a&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%2Fcgny854osz6uoa0kxf38.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%2Fcgny854osz6uoa0kxf38.png" alt=" " width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Variables and outputs
&lt;/h3&gt;

&lt;p&gt;Hard-coded names are fine for a first try, but I wanted them in a &lt;code&gt;variables.tf&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch &lt;/span&gt;variables.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fjwuj17yqg85kefu4sakr.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%2Fjwuj17yqg85kefu4sakr.png" alt=" " width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every value got its own variable block, like this:&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;variable&lt;/span&gt; &lt;span class="s2"&gt;"resource_group_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hagital-rg"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hagital-rg"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I did the same for location, network name, subnet name, public IP name, NSG name, NIC name and VM name. Two variables have no default on purpose, so they must be given a value when Terraform runs:&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;variable&lt;/span&gt; &lt;span class="s2"&gt;"subscription_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Azure subscription ID"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"public_key"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"admin user ssh public key"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F2yyytl7twa1i0p4vi77k.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%2F2yyytl7twa1i0p4vi77k.png" alt=" " width="800" height="465"&gt;&lt;/a&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%2Fefnw921cvsrme9d2j0dq.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%2Fefnw921cvsrme9d2j0dq.png" alt=" " width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I changed &lt;code&gt;main.tf&lt;/code&gt; to use them, for example &lt;code&gt;subscription_id = var.subscription_id&lt;/code&gt; and &lt;code&gt;name = var.resource_group_name&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%2Fgz795yw5f0vy1tcsuqfn.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%2Fgz795yw5f0vy1tcsuqfn.png" alt=" " width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, &lt;code&gt;outputs.tf&lt;/code&gt;. It prints the VM's public IP and username after the build:&lt;br&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;touch &lt;/span&gt;outputs.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"hagital_vm_public_ip"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Public IP address of the Hagital virtual machine"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_public_ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip_address&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"hagital_vm_username"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Username of the Hagital virtual machine"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_linux_virtual_machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital_vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;admin_username&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran &lt;code&gt;terraform fmt&lt;/code&gt; to tidy the files. It listed the two files it fixed.&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%2F8bn5f8358td2tszsef5r.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%2F8bn5f8358td2tszsef5r.png" alt=" " width="800" height="302"&gt;&lt;/a&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%2Fq2jbw3f1e1t8f2quhjmc.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%2Fq2jbw3f1e1t8f2quhjmc.png" alt=" " width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I ran &lt;code&gt;terraform plan&lt;/code&gt; again. Because &lt;code&gt;subscription_id&lt;/code&gt; and &lt;code&gt;public_key&lt;/code&gt; have no defaults, Terraform stopped and &lt;strong&gt;asked me to type them in&lt;/strong&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%2Fxevu20ca93ksjps3cvbt.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%2Fxevu20ca93ksjps3cvbt.png" alt=" " width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I typed the subscription ID and got another clean plan: 8 to add, plus my two outputs.&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%2Fewrjnve2873fidsd36wb.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%2Fewrjnve2873fidsd36wb.png" alt=" " width="800" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep that typing prompt in mind. It comes back later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: Let GitHub talk to Azure
&lt;/h2&gt;

&lt;p&gt;GitHub's computer needs permission to build things in my Azure account. I made a service principal (the robot account) with the Contributor role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create-for-rbac&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hagital-cicd"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;contributor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--scopes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/subscriptions/YOUR-SUBSCRIPTION-ID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--sdk-auth&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It printed a JSON block with the login details. Azure warned me that &lt;code&gt;--sdk-auth&lt;/code&gt; is deprecated. It still worked for me.&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%2Fv03c14yhnrj7kkjnys2w.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%2Fv03c14yhnrj7kkjnys2w.png" alt=" " width="799" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I made a &lt;strong&gt;private&lt;/strong&gt; repo on GitHub with a README:&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%2Fef8u37ss3foe430zukod.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%2Fef8u37ss3foe430zukod.png" alt=" " width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding the secrets
&lt;/h3&gt;

&lt;p&gt;In the repo I went to &lt;strong&gt;Settings &amp;gt; Secrets and variables &amp;gt; Actions &amp;gt; New repository secret&lt;/strong&gt; and added four secrets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret name&lt;/th&gt;
&lt;th&gt;What I pasted in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HAGITAL_AZURE_CREDENTIALS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The whole JSON from the command above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HAGITAL_SUBSCRIPTION_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;My subscription ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HAGITAL_SSH_PUBLIC_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Output of &lt;code&gt;cat ~/.ssh/id_rsa.pub&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HAGITAL_SSH_PRIVATE_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Output of &lt;code&gt;cat ~/.ssh/id_rsa&lt;/code&gt;, including the BEGIN and END lines&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you don't have a key pair yet, &lt;code&gt;ssh-keygen -t rsa -b 4096&lt;/code&gt; makes one.&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%2Fndxqhna50my62mzew58b.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%2Fndxqhna50my62mzew58b.png" alt=" " width="799" height="380"&gt;&lt;/a&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%2Fdp6p6pfs7y5afrizr6oa.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%2Fdp6p6pfs7y5afrizr6oa.png" alt=" " width="800" height="385"&gt;&lt;/a&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%2Fdsoowfig8iwz4brf2buw.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%2Fdsoowfig8iwz4brf2buw.png" alt=" " width="800" height="382"&gt;&lt;/a&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%2Fc3xw9ahd5yhve7if7nx2.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%2Fc3xw9ahd5yhve7if7nx2.png" alt=" " width="800" height="387"&gt;&lt;/a&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%2Fz0l2y2hv11thrgr2e1vo.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%2Fz0l2y2hv11thrgr2e1vo.png" alt=" " width="800" height="414"&gt;&lt;/a&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%2Fgcn8hwp192cqe54k7er3.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%2Fgcn8hwp192cqe54k7er3.png" alt=" " width="799" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Last, I made an environment for the workflow to deploy into:&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%2Ffnp2ja3oukk2y4yob6pk.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%2Ffnp2ja3oukk2y4yob6pk.png" alt=" " width="799" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One small thing I noticed later. I named it "Hagital Production" here, but my YAML says &lt;code&gt;hagital-production&lt;/code&gt;. GitHub doesn't complain, it just makes a new environment from the YAML. Keep the names identical so you don't end up with a stray one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: Git and GitHub (where the fun started)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Error 1: "not a valid object name: 'master'"
&lt;/h3&gt;

&lt;p&gt;I ran &lt;code&gt;git init&lt;/code&gt;, and Git put me on a branch called &lt;code&gt;master&lt;/code&gt;. I wanted &lt;code&gt;main&lt;/code&gt;, so I typed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git branch main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git said no:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;fatal: not a valid object name: 'master'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason is simple. A branch is a pointer to a commit, and I had zero commits. There was nothing to point at. So I did &lt;code&gt;git status&lt;/code&gt;, &lt;code&gt;git add .&lt;/code&gt; and &lt;code&gt;git commit&lt;/code&gt; first.&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%2Ff27x7wn5hk9jm3gdrwj0.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%2Ff27x7wn5hk9jm3gdrwj0.png" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that commit, the same commands worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git branch main
git switch main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fv41dtxczfc0808f3iqgn.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%2Fv41dtxczfc0808f3iqgn.png" alt=" " width="800" height="163"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Notice what was inside that first commit. It matters in Part 5.)&lt;/p&gt;

&lt;h3&gt;
  
  
  .gitignore
&lt;/h3&gt;

&lt;p&gt;I made a &lt;code&gt;.gitignore&lt;/code&gt; and added the &lt;code&gt;.terraform/&lt;/code&gt; folder to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch&lt;/span&gt; .gitignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fooutzfvijc50c6ndvd1u.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%2Fooutzfvijc50c6ndvd1u.png" alt=" " width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the workflow file on GitHub
&lt;/h3&gt;

&lt;p&gt;To make the workflow file, I used the GitHub website. &lt;strong&gt;Add file &amp;gt; Create new file&lt;/strong&gt;. When I typed &lt;code&gt;.github/workflows/deploy.yaml&lt;/code&gt; in the name box, GitHub made both folders for me.&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%2Ft19x78kgb0jxtlp8nw84.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%2Ft19x78kgb0jxtlp8nw84.png" alt=" " width="800" height="184"&gt;&lt;/a&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%2F2fxljc3ky9ipbmcq029e.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%2F2fxljc3ky9ipbmcq029e.png" alt=" " width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I copied the repo's HTTPS link:&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%2Fa4b3e1pu4qi3y4c96th0.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%2Fa4b3e1pu4qi3y4c96th0.png" alt=" " width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And connected my laptop folder to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote add origin https://github.com/4thman/Hagital-project.git
git pull origin main &lt;span class="nt"&gt;--allow-unrelated-histories&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--allow-unrelated-histories&lt;/code&gt; part is there because GitHub already had its own first commit (the README and my new YAML file), and my laptop had a totally separate history. Git refuses to join two histories that don't share a parent unless you say it's okay.&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%2Fgjj00z2rzrcapkl1c0sg.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%2Fgjj00z2rzrcapkl1c0sg.png" alt=" " width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4: Write the workflow
&lt;/h2&gt;

&lt;p&gt;The workflow has two jobs. &lt;code&gt;provision&lt;/code&gt; builds the VM. &lt;code&gt;configure&lt;/code&gt; waits for &lt;code&gt;provision&lt;/code&gt;, then sets up the website.&lt;/p&gt;

&lt;h3&gt;
  
  
  Job 1: provision
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Hagital VM Deploy&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&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;main"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;provision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hagital-production&lt;/span&gt;

    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;vm_ip&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.tf_output.outputs.vm_ip }}&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/login@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;creds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.HAGITAL_AZURE_CREDENTIALS }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hashicorp/setup-terraform@v3&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;Prepare SSH Public Key for Terraform&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;mkdir -p ~/.ssh&lt;/span&gt;
          &lt;span class="s"&gt;echo "${{ secrets.HAGITAL_SSH_PUBLIC_KEY }}" &amp;gt; ~/.ssh/id_rsa.pub&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;Terraform Init&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform init&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;Terraform Plan&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;terraform plan \&lt;/span&gt;
            &lt;span class="s"&gt;-var="subscription_id=${{ secrets.HAGITAL_SUBSCRIPTION_ID }}" \&lt;/span&gt;
            &lt;span class="s"&gt;-out=hagital.tfplan&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;Terraform Apply&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform apply hagital.tfplan&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;Get VM Public IP&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tf_output&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "vm_ip=$(terraform output -raw hagital_vm_public_ip)" &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;on:&lt;/code&gt; says run when I push to &lt;code&gt;main&lt;/code&gt;, or when I click the button myself (&lt;code&gt;workflow_dispatch&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;azure/login&lt;/code&gt; uses my robot account secret.&lt;/li&gt;
&lt;li&gt;The last step reads the VM's IP from Terraform and saves it as a job output. That's how the second job learns where the VM is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmoefj8532b7widrzkxmb.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%2Fmoefj8532b7widrzkxmb.png" alt=" " width="800" height="608"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Job 2: configure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;configure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;provision&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prepare SSH Private Key&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;mkdir -p ~/.ssh&lt;/span&gt;
          &lt;span class="s"&gt;echo "${{ secrets.HAGITAL_SSH_PRIVATE_KEY }}" &amp;gt; ~/.ssh/id_rsa&lt;/span&gt;
          &lt;span class="s"&gt;chmod 600 ~/.ssh/id_rsa&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;Wait for SSH to become Available&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;for i in $(seq 1 10); do&lt;/span&gt;
            &lt;span class="s"&gt;ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa \&lt;/span&gt;
              &lt;span class="s"&gt;adminuser@${{ needs.provision.outputs.vm_ip }} echo ready &amp;amp;&amp;amp; break&lt;/span&gt;
            &lt;span class="s"&gt;echo "VM not Ready Yet, Waiting 15 Seconds..."&lt;/span&gt;
            &lt;span class="s"&gt;sleep 15&lt;/span&gt;
          &lt;span class="s"&gt;done&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;Install and configure the website&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa adminuser@${{ needs.provision.outputs.vm_ip }} '&lt;/span&gt;
            &lt;span class="s"&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y&lt;/span&gt;
            &lt;span class="s"&gt;sudo apt install nginx git -y&lt;/span&gt;
            &lt;span class="s"&gt;cd /var/www/html&lt;/span&gt;
            &lt;span class="s"&gt;sudo rm -f index.nginx-debian.html&lt;/span&gt;
            &lt;span class="s"&gt;sudo git clone https://github.com/MettaSurendhar/omnifood.git .&lt;/span&gt;
            &lt;span class="s"&gt;sudo chown -R www-data:www-data /var/www/html&lt;/span&gt;
            &lt;span class="s"&gt;sudo systemctl restart nginx&lt;/span&gt;
          &lt;span class="s"&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;Show the live Website URL&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "Hagital Website is Live and was deployed using CI/CD." &amp;gt;&amp;gt; "$GITHUB_STEP_SUMMARY"&lt;/span&gt;
          &lt;span class="s"&gt;echo "Visit: http://${{ needs.provision.outputs.vm_ip }}" &amp;gt;&amp;gt; "$GITHUB_STEP_SUMMARY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In plain words:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;needs: provision&lt;/code&gt; means "don't start until the VM exists."&lt;/li&gt;
&lt;li&gt;A brand new VM isn't ready for SSH right away. So the wait step tries to connect up to 10 times, 15 seconds apart.&lt;/li&gt;
&lt;li&gt;Once in, the script installs nginx and git, then swaps nginx's default page for the website. More on that just below.&lt;/li&gt;
&lt;li&gt;The last step writes the site's URL into the run summary, so I don't have to dig for the IP.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Where the website comes from
&lt;/h3&gt;

&lt;p&gt;I didn't build the website myself. I used a public GitHub project called Omnifood (&lt;code&gt;https://github.com/MettaSurendhar/omnifood&lt;/code&gt;), and the workflow copies it onto the VM. Here's how that works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;nginx is a web server. It shows whatever files sit in &lt;code&gt;/var/www/html&lt;/code&gt;. Right after you install it, that folder only holds nginx's default "Welcome to nginx" page (&lt;code&gt;index.nginx-debian.html&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The script deletes that page with &lt;code&gt;sudo rm -f index.nginx-debian.html&lt;/code&gt;. Git can only clone into an empty folder, so the default page has to go first.&lt;/li&gt;
&lt;li&gt;Then &lt;code&gt;sudo git clone https://github.com/MettaSurendhar/omnifood.git .&lt;/code&gt; downloads the Omnifood files straight into that folder. The dot at the end means "put the files here, don't make a new folder."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chown&lt;/code&gt; gives nginx permission to read the files, and &lt;code&gt;systemctl restart nginx&lt;/code&gt; makes it pick them up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So when someone opens the VM's IP address, they see Omnifood, not the nginx welcome page. Want to show your own site? Change that one clone URL.&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%2Ff7rv9vteuaosudrn9kmu.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%2Ff7rv9vteuaosudrn9kmu.png" alt=" " width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Back in VS Code, I saved the file and committed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"wrote the Hagital deploy &amp;amp; configure script on the deploy.yaml file"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fb8c5umlo8qdejq7r5v88.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%2Fb8c5umlo8qdejq7r5v88.png" alt=" " width="799" height="319"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5: Push day (three errors in a row)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Error 2: GitHub rejected my push
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;remote: error: File .terraform/providers/.../terraform-provider-azurerm_v5.4.0_x5.exe is 219.00 MB;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;this exceeds GitHub's file size limit of 100.00 MB
remote: error: GH001: Large files detected.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provider file that Terraform downloads is 219 MB, and GitHub's limit is 100 MB. I had put &lt;code&gt;.terraform/&lt;/code&gt; in &lt;code&gt;.gitignore&lt;/code&gt;, so what happened?&lt;/p&gt;

&lt;p&gt;Remember my very first commit in Part 3? I ran &lt;code&gt;git add .&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; I made the &lt;code&gt;.gitignore&lt;/code&gt;. Git had already saved that giant file. A &lt;code&gt;.gitignore&lt;/code&gt; only stops Git from picking up new files. It does nothing about files that were already committed.&lt;/p&gt;

&lt;p&gt;I added the state files to &lt;code&gt;.gitignore&lt;/code&gt; too, then committed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.terraform/
*.tfstate
*tfstate.*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F81us3itxvygw21asuvgt.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%2F81us3itxvygw21asuvgt.png" alt=" " width="800" height="472"&gt;&lt;/a&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%2F3zgq4r8pnjhztovt2oqk.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%2F3zgq4r8pnjhztovt2oqk.png" alt=" " width="799" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The push failed again, with the same error. Deleting the file in a new commit doesn't help, because the old commit still carries it. The big file lives in my history.&lt;/p&gt;

&lt;p&gt;While I was fixing things, I also changed the VM's key line in &lt;code&gt;main.tf&lt;/code&gt;. The path &lt;code&gt;~/.ssh/id_rsa.pub&lt;/code&gt; only exists on my laptop, so I switched to the variable:&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;public_key&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Faw9nthdz8lfslimd9tjq.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%2Faw9nthdz8lfslimd9tjq.png" alt=" " width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix that worked:&lt;/strong&gt; this repo was brand new and had nothing worth saving, so I deleted Git's history and started over:&lt;br&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;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; .git
git init
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"removed git and readded git then re staged an commit"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time &lt;code&gt;.terraform&lt;/code&gt; wasn't in the commit. Seven small files went in.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Don't do this on a real project.&lt;/strong&gt; &lt;code&gt;rm -rf .git&lt;/code&gt; wipes your whole history. I could do it because I had exactly two commits and both were disposable.&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%2Fhkya3nbav2b4c0j67xqi.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%2Fhkya3nbav2b4c0j67xqi.png" alt=" " width="799" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Error 3: the merge conflict
&lt;/h3&gt;

&lt;p&gt;Same trick as before: &lt;code&gt;git branch main&lt;/code&gt; failed until after the first commit. Once I had a commit, I made &lt;code&gt;main&lt;/code&gt; and tried to join my history with GitHub's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git merge origin/main &lt;span class="nt"&gt;--allow-unrelated-histories&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;Automatic merge failed;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fix conflicts and &lt;span class="k"&gt;then &lt;/span&gt;commit the result.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both sides had a &lt;code&gt;deploy.yaml&lt;/code&gt;, and Git couldn't pick one. My prompt changed to &lt;code&gt;(main|MERGING)&lt;/code&gt;. I wanted my local version, so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;--ours&lt;/span&gt; .github/workflows/deploy.yaml
git add .github/workflows/deploy.yaml
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Merge GitHub main into local main"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt went back to &lt;code&gt;(main)&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%2F62n0e50cbw2rrxmuy4o0.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%2F62n0e50cbw2rrxmuy4o0.png" alt=" " width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;)&lt;/p&gt;

&lt;p&gt;Then the push finally went through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fbn7g5f87qvs9lrgfv6iq.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%2Fbn7g5f87qvs9lrgfv6iq.png" alt=" " width="799" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Your turn:&lt;/em&gt; look at your own repo's file list right now. Is anything in there that shouldn't be? Check before you push, not after.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 6: Watching GitHub Actions do the work
&lt;/h2&gt;

&lt;p&gt;I opened the &lt;strong&gt;Actions&lt;/strong&gt; tab and my workflow was already running.&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%2F2fnzw6q07gs9655ony7t.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%2F2fnzw6q07gs9655ony7t.png" alt=" " width="800" height="412"&gt;&lt;/a&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%2Fp1aip9uktn6azxwem1jl.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%2Fp1aip9uktn6azxwem1jl.png" alt=" " width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Error 4: the workflow that waited forever
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;provision&lt;/code&gt; job sat on &lt;strong&gt;Terraform Plan&lt;/strong&gt;. Two minutes. Five minutes. Twenty minutes.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Quick guess before you scroll: what do you think it was waiting for?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Remember the prompt from Part 1, where Terraform asked me to type the public key? That's what happened. My &lt;code&gt;main.tf&lt;/code&gt; now used &lt;code&gt;var.public_key&lt;/code&gt;, but my workflow only passed &lt;code&gt;subscription_id&lt;/code&gt;. So Terraform asked for the key and waited for someone to type it. Nobody types in GitHub Actions. It just sat there until the run gave up after 19 minutes and 50 seconds.&lt;/p&gt;

&lt;p&gt;The fix is one extra line in the plan step:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Plan&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;terraform plan \&lt;/span&gt;
            &lt;span class="s"&gt;-var="subscription_id=${{ secrets.HAGITAL_SUBSCRIPTION_ID }}" \&lt;/span&gt;
            &lt;span class="s"&gt;-var="public_key=${{ secrets.HAGITAL_SSH_PUBLIC_KEY }}" \&lt;/span&gt;
            &lt;span class="s"&gt;-out=hagital.tfplan&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also changed the apply line to &lt;code&gt;terraform apply -auto-approve hagital.tfplan&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error 5: my commit message broke Bash
&lt;/h3&gt;

&lt;p&gt;When I committed the fix, I wrote a commit message that had &lt;code&gt;${{ secrets... }}&lt;/code&gt; inside double quotes. Bash tried to read it as a command and said:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash: ... : bad substitution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing got committed. I wrote a normal message instead (&lt;code&gt;Add public key variable to Terraform plan&lt;/code&gt;) and it worked. Keep commit messages plain.&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%2Fuvydv7faqxiu3qi9zl6q.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%2Fuvydv7faqxiu3qi9zl6q.png" alt=" " width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I pushed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F72jjnfcdev3xq5zohe7c.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%2F72jjnfcdev3xq5zohe7c.png" alt=" " width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  It works
&lt;/h3&gt;

&lt;p&gt;That &lt;code&gt;git push&lt;/code&gt; is the trigger. The &lt;code&gt;on: push&lt;/code&gt; lines in the YAML tell GitHub to start the workflow every time new code lands on &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A new run started. &lt;code&gt;provision&lt;/code&gt; finished in 35 seconds. &lt;code&gt;configure&lt;/code&gt; began.&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%2Fy159c627ukgoegl5zeca.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%2Fy159c627ukgoegl5zeca.png" alt=" " width="799" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The "Install and configure the website" step took a while, because apt was updating a whole Ubuntu machine.&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%2Fwwcmeequezlinny5p8s7.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%2Fwwcmeequezlinny5p8s7.png" alt=" " width="799" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The log gets long and ends with warnings about a pending kernel upgrade and services to restart. That's normal. Ubuntu is saying "a reboot would be nice." The job still finished green.&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%2Fs4x6o25y90pjrf01tiho.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%2Fs4x6o25y90pjrf01tiho.png" alt=" " width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the run list. Run #4 is green (4 minutes 19 seconds). Run #2 is the stuck one from before.&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%2F2uq4zlcwfbl42rx2ujeg.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%2F2uq4zlcwfbl42rx2ujeg.png" alt=" " width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And here's my favourite part, the summary page, with the link my last step wrote:&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%2Fzfzbknbfh21bz17z365v.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%2Fzfzbknbfh21bz17z365v.png" alt=" " width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I clicked the IP address. No "Welcome to nginx" page. It was the Omnifood website, cloned from GitHub onto a server that didn't exist 5 minutes earlier:&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%2F57xmjfiln4l19fks10en.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%2F57xmjfiln4l19fks10en.png" alt=" " width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I did nothing by hand except push code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 7: Delete everything with one click
&lt;/h2&gt;

&lt;p&gt;A VM that runs forever keeps costing money. I wanted a way to tear it all down from GitHub, so I added a third job:&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;destroy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'workflow_dispatch'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hagital-production&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&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;Azure Login&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/login@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;creds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.HAGITAL_AZURE_CREDENTIALS }}&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;Setup Terraform&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hashicorp/setup-terraform@v3&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;Terraform Init&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform init&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;Terraform Destroy&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;terraform destroy \&lt;/span&gt;
            &lt;span class="s"&gt;-auto-approve \&lt;/span&gt;
            &lt;span class="s"&gt;-var="subscription_id=${{ secrets.HAGITAL_SUBSCRIPTION_ID }}" \&lt;/span&gt;
            &lt;span class="s"&gt;-var="public_key=${{ secrets.HAGITAL_SSH_PUBLIC_KEY }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;if&lt;/code&gt; line is the switch. This job only runs when I click the button (&lt;code&gt;workflow_dispatch&lt;/code&gt;), never on a normal push. So my code pushes can't accidentally delete my server.&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%2Fkoiiqpua14xz32fb8qsi.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%2Fkoiiqpua14xz32fb8qsi.png" alt=" " width="799" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But then the other two jobs needed the opposite switch. Without it, clicking the button would try to build the VM and delete it in the same run. So I added this line to both &lt;code&gt;provision&lt;/code&gt; and &lt;code&gt;configure&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;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'push'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fjspsinbp9n4xk42ixgfl.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%2Fjspsinbp9n4xk42ixgfl.png" alt=" " width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then the usual routine, with commit message "Added Manual Terraform Destroy Workflow":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status
git add .github/workflows/deploy.yaml
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Added Manual Terraform Destroy Workflow"&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F19hjhqw605zgzqc5gm36.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%2F19hjhqw605zgzqc5gm36.png" alt=" " width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That push counts as a normal push, so &lt;code&gt;provision&lt;/code&gt; and &lt;code&gt;configure&lt;/code&gt; ran again and &lt;code&gt;destroy&lt;/code&gt; was skipped (the grey circle). The site stayed up.&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%2F5g8veh0cpjli9zf1o81b.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%2F5g8veh0cpjli9zf1o81b.png" alt=" " width="799" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Proof the site was still alive:&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%2Fn7f637irpbysztxqwm2g.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%2Fn7f637irpbysztxqwm2g.png" alt=" " width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pulling the plug
&lt;/h3&gt;

&lt;p&gt;Then I went to &lt;strong&gt;Actions &amp;gt; Hagital VM Deploy &amp;gt; Run workflow&lt;/strong&gt;, picked the &lt;code&gt;main&lt;/code&gt; branch, and clicked the green button.&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%2F2w4r0nr9j0dixurewx2a.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%2F2w4r0nr9j0dixurewx2a.png" alt=" " width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This time &lt;code&gt;provision&lt;/code&gt; and &lt;code&gt;configure&lt;/code&gt; were skipped and only &lt;code&gt;destroy&lt;/code&gt; ran. The log ends with the line I wanted to see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Destroy complete! Resources: 8 destroyed.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight things built, eight things gone.&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%2F8cs7qet9m9h6z3zuf0wc.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%2F8cs7qet9m9h6z3zuf0wc.png" alt=" " width="800" height="415"&gt;&lt;/a&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%2Fnb49tw1zwa09tvu9as5b.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%2Fnb49tw1zwa09tvu9as5b.png" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why does this work on a fresh GitHub computer that has never seen my VM? Because of the remote state from Part 1. Terraform reads its notebook from Azure storage, sees what it built, and deletes exactly that.&lt;/p&gt;

&lt;p&gt;One thing is still there. The state storage account (&lt;code&gt;terraform-state-rg&lt;/code&gt;) was made by hand with the CLI, not by this Terraform code. If you want a zero bill, delete it yourself when you're done.&lt;/p&gt;




&lt;h2&gt;
  
  
  Every mistake in one place
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;What went wrong&lt;/th&gt;
&lt;th&gt;Why&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;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git branch main&lt;/code&gt; said "not a valid object name"&lt;/td&gt;
&lt;td&gt;No commits yet, so the branch had nothing to point at&lt;/td&gt;
&lt;td&gt;Make a commit first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;GitHub rejected my push (219 MB file)&lt;/td&gt;
&lt;td&gt;I ran &lt;code&gt;git add .&lt;/code&gt; before making &lt;code&gt;.gitignore&lt;/code&gt;, so the provider file was already in history&lt;/td&gt;
&lt;td&gt;Create &lt;code&gt;.gitignore&lt;/code&gt; first; I restarted the repo with &lt;code&gt;rm -rf .git&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Merge failed on &lt;code&gt;deploy.yaml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;GitHub and my laptop each had a different &lt;code&gt;deploy.yaml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git checkout --ours&lt;/code&gt;, then add and commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Terraform plan hung for 19+ minutes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;public_key&lt;/code&gt; had no value and nobody can type in CI&lt;/td&gt;
&lt;td&gt;Pass it with &lt;code&gt;-var&lt;/code&gt; from a secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bad substitution&lt;/code&gt; on commit&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;${{ }}&lt;/code&gt; inside a double-quoted commit message&lt;/td&gt;
&lt;td&gt;Use a plain message&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Things I'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Make &lt;code&gt;.gitignore&lt;/code&gt; before the first &lt;code&gt;git add .&lt;/code&gt;&lt;/strong&gt; and list &lt;code&gt;.terraform/&lt;/code&gt; in it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't leave SSH open to the world.&lt;/strong&gt; My NSG allows port 22 from anywhere. I tried limiting SSH to GitHub's IP ranges at one point and backed it out, so this lab stays open. That's okay for something I destroy in a day. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;And that’s it from a simple git push to a live website running on Azure, with Terraform and GitHub Actions handling the heavy lifting.&lt;/p&gt;

&lt;p&gt;The biggest lesson for me wasn’t just learning Terraform or GitHub Actions. It was learning how to troubleshoot, fix mistakes, and keep going when things don’t work the first time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git push → GitHub Actions → Terraform → Azure VM → Live Website
                                                          ↓
                                                One-click Destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📦 Full source code at my github repo: github.com/4thman/Hagital-project&lt;/p&gt;

&lt;p&gt;Now I want to hear from you:&lt;/p&gt;

&lt;p&gt;Where should the journey go next Docker containers, secure HTTPS, custom domains, OIDC authentication, or a fully automated production CI/CD pipeline?&lt;/p&gt;

&lt;p&gt;And most importantly, what’s the biggest DevOps mistake you’ve ever made? 😄&lt;/p&gt;

&lt;p&gt;Drop it in the comments. Let’s learn from each other!&lt;/p&gt;

&lt;p&gt;One git push at a time. 🚀&lt;/p&gt;

</description>
      <category>azure</category>
      <category>terraform</category>
      <category>githubactions</category>
      <category>devops</category>
    </item>
    <item>
      <title>Three Days, Four Errors, One Working Azure VM: Deploying with Terraform and Remote State</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Wed, 09 Sep 2026 12:31:36 +0000</pubDate>
      <link>https://dev.to/4thman/three-days-four-errors-one-working-azure-vm-deploying-with-terraform-and-remote-state-2kbp</link>
      <guid>https://dev.to/4thman/three-days-four-errors-one-working-azure-vm-deploying-with-terraform-and-remote-state-2kbp</guid>
      <description>&lt;p&gt;This was my first real hands-on Terraform assignment: deploy a working Azure VM, wire up a remote backend for the state file, and document the whole process. On paper it looked like a weekend task. It took me close to three days, mostly because I kept running into errors I'd never seen before and had to actually understand each one instead of working around it.&lt;/p&gt;

&lt;p&gt;This post walks through what I built, the order I built it in, the four errors that stopped me at different points, how I got past each one, and the proof that it all worked in the end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/4thman/cloud-cohort" rel="noopener noreferrer"&gt;github.com/4thman/cloud-cohort&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;At a glance, the stack looks like this:&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%2Fpd2stn6jfaono69ios1w.jpg" 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%2Fpd2stn6jfaono69ios1w.jpg" alt=" " width="600" height="482"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet
   │
   ▼
Public IP (cohort-pip)
   │
   ▼
NIC (cohort-nic)  ──secured by──  NSG (cohort-nsg: allow 22, allow 80)
   │
   ▼
VM: cohort-vm (Ubuntu 18.04 LTS, Standard_B1s)
   │
   ▼
Subnet (cohort-subnet, 10.0.0.0/24)
   │
   ▼
VNet (cohort-vnet, 10.0.0.0/16)
   │
   ▼
Resource Group: cohort-rg (East US)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything above lives in Terraform. The state file for all of it lives in an Azure Storage Account, not on my laptop. That was the part of the brief I hadn't done before, and it ended up being the most useful thing I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting the stage
&lt;/h2&gt;

&lt;p&gt;Nothing complicated here, just a folder and an editor.&lt;br&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;cd &lt;/span&gt;Downloads
&lt;span class="nb"&gt;mkdir &lt;/span&gt;cloud-cohort
&lt;span class="nb"&gt;cd &lt;/span&gt;cloud-cohort
code &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F0v1qom1f95i0ack3vgsr.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%2F0v1qom1f95i0ack3vgsr.png" alt=" " width="800" height="253"&gt;&lt;/a&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%2Fzzpf6naa9spi84nysc50.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%2Fzzpf6naa9spi84nysc50.png" alt=" " width="799" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First file: &lt;code&gt;main.tf&lt;/code&gt;, empty.&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%2F20ud4x49jq27ev1a1n0o.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%2F20ud4x49jq27ev1a1n0o.png" alt=" " width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring up the provider
&lt;/h2&gt;

&lt;p&gt;Every Terraform project starts with telling it which provider to talk to and which version to pin. I went with &lt;code&gt;azurerm&lt;/code&gt; version &lt;code&gt;~&amp;gt; 5.4.0&lt;/code&gt; since am working on Azure Platform.&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;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;azurerm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/azurerm"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 5.4.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"azurerm"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;features&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="nx"&gt;subscription_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fsyepx2g4pyq3vwnmuk02.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%2Fsyepx2g4pyq3vwnmuk02.png" alt=" " width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then the command that downloads the provider plugin and sets everything up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fku1hnkq53u5nhmo5v47h.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%2Fku1hnkq53u5nhmo5v47h.png" alt=" " width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the network layer
&lt;/h2&gt;

&lt;p&gt;I built this resource by resource, running &lt;code&gt;terraform plan&lt;/code&gt; after almost every block just to see what was about to happen.&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="c1"&gt;# Create a resource group&lt;/span&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;"cohort-rg"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cohort-rg"&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"East US"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Create a virtual network&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_virtual_network"&lt;/span&gt; &lt;span class="s2"&gt;"cohort-vnet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cohort-vnet"&lt;/span&gt;
  &lt;span class="nx"&gt;address_space&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cohort-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cohort-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Create a subnet&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"cohort-subnet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cohort-subnet"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cohort-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;virtual_network_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_virtual_network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cohort-vnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;address_prefixes&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.0.0.0/24"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&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%2F5pm759eb5d9z9ga8ey7r.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%2F5pm759eb5d9z9ga8ey7r.png" alt=" " width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Locking it down with an NSG
&lt;/h2&gt;

&lt;p&gt;I configured a Network Security Group (NSG) to control inbound traffic. I added two essential rules: SSH (port 22) to allow secure remote administration and HTTP (port 80) to enable web-server access for testing and demonstration purposes.&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="c1"&gt;# Create a network security group &amp;amp; NSG rules&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_network_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"cohort-nsg"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cohort-nsg"&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cohort-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cohort-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;

  &lt;span class="nx"&gt;security_rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;                       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowSSH"&lt;/span&gt;
    &lt;span class="nx"&gt;priority&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
    &lt;span class="nx"&gt;direction&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Inbound"&lt;/span&gt;
    &lt;span class="nx"&gt;access&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;source_port_range&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
    &lt;span class="nx"&gt;destination_port_range&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"22"&lt;/span&gt;
    &lt;span class="nx"&gt;source_address_prefix&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
    &lt;span class="nx"&gt;destination_address_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;security_rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;                       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowHTTP"&lt;/span&gt;
    &lt;span class="nx"&gt;priority&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1001&lt;/span&gt;
    &lt;span class="nx"&gt;direction&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Inbound"&lt;/span&gt;
    &lt;span class="nx"&gt;access&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;source_port_range&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
    &lt;span class="nx"&gt;destination_port_range&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"80"&lt;/span&gt;
    &lt;span class="nx"&gt;source_address_prefix&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
    &lt;span class="nx"&gt;destination_address_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&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%2Fga6k79ars60pbtnrk9mm.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%2Fga6k79ars60pbtnrk9mm.png" alt=" " width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The NIC and a formatting habit
&lt;/h2&gt;

&lt;p&gt;The network interface is what actually connects the VM to the subnet and, through the public IP, to the internet. I also started running &lt;code&gt;terraform fmt&lt;/code&gt; before every &lt;code&gt;plan&lt;/code&gt; around here. It costs nothing, and it catches the indentation mess before it becomes a diff you have to think about.&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="c1"&gt;# Create a network interface&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_network_interface"&lt;/span&gt; &lt;span class="s2"&gt;"cohort-nic"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cohort-nic"&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cohort-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cohort-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;

  &lt;span class="nx"&gt;ip_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;                          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"internal"&lt;/span&gt;
    &lt;span class="nx"&gt;subnet_id&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cohort-subnet&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;private_ip_address_allocation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Dynamic"&lt;/span&gt;
    &lt;span class="nx"&gt;public_ip_address_id&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_public_ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cohort-pip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Associate the NIC with the NSG&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_network_interface_security_group_association"&lt;/span&gt; &lt;span class="s2"&gt;"cohort-nic-nsg"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;network_interface_id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_network_interface&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cohort-nic&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;network_security_group_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_network_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cohort-nsg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fdnelr9vod7p9r1283wfg.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%2Fdnelr9vod7p9r1283wfg.png" alt=" " width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual VM
&lt;/h2&gt;

&lt;p&gt;This is the main resource that everything else is built around: an Ubuntu 18.04 LTS virtual machine using the Standard_B1s size and authenticated with an SSH key instead of a password. I chose Ubuntu rather than a Microsoft Windows Server VM because Ubuntu is lightweight, open-source, and commonly used for hosting web servers and working with Linux-based cloud environments. It also provides a straightforward environment for installing and managing web-server software through the command line, making it a suitable choice for this learning project.&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="c1"&gt;# Create a virtual machine&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_linux_virtual_machine"&lt;/span&gt; &lt;span class="s2"&gt;"cohort-vm"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cohort-vm"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cohort-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cohort-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;size&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Standard_B1s"&lt;/span&gt;
  &lt;span class="nx"&gt;admin_username&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cohortuser1"&lt;/span&gt;

  &lt;span class="nx"&gt;network_interface_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;azurerm_network_interface&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cohort-nic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;admin_ssh_key&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;username&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cohortuser1"&lt;/span&gt;
    &lt;span class="nx"&gt;public_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"~/.ssh/id_rsa.pub"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;os_disk&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;caching&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ReadWrite"&lt;/span&gt;
    &lt;span class="nx"&gt;storage_account_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Standard_LRS"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;source_image_reference&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;publisher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Canonical"&lt;/span&gt;
    &lt;span class="nx"&gt;offer&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"UbuntuServer"&lt;/span&gt;
    &lt;span class="nx"&gt;sku&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"18.04-LTS"&lt;/span&gt;
    &lt;span class="nx"&gt;version&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"latest"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&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%2F0a1tsb78ecljsldswthj.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%2F0a1tsb78ecljsldswthj.png" alt=" " width="799" height="542"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And two outputs so I don't have to go hunting through the Azure portal every time I need the IP or the username:&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%2Frqmagzivabz7zoux41pu.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%2Frqmagzivabz7zoux41pu.png" alt=" " width="720" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleaning up: variables and outputs
&lt;/h2&gt;

&lt;p&gt;At this point, everything was hardcoded directly into main.tf. While this works, it makes the configuration harder to maintain, reuse, and modify for different environments. To improve the structure, I moved the repeated and changeable values into variables.tf and placed the two outputs into a separate outputs.tf file.&lt;/p&gt;

&lt;p&gt;I created variables.tf to make the Terraform configuration more flexible and reusable. Instead of changing values directly inside main.tf, I can define them as variables and provide different values when needed. This makes it easier to reuse the same configuration for another environment without modifying the main infrastructure code.&lt;/p&gt;

&lt;p&gt;I created outputs.tf to keep the information Terraform needs to display after deployment separate from the resource configuration. This makes the project more organised and allows important details, such as the VM's public IP address, to be easily accessed after the infrastructure has been created.&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%2Fz631fdi1empvgim49nub.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%2Fz631fdi1empvgim49nub.png" alt=" " width="800" height="417"&gt;&lt;/a&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%2Fm5dr1hkpjx4rnh6jbq6s.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%2Fm5dr1hkpjx4rnh6jbq6s.png" alt=" " width="800" height="248"&gt;&lt;/a&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%2Fg026ttzt6lbrmd36e8ux.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%2Fg026ttzt6lbrmd36e8ux.png" alt=" " width="799" height="395"&gt;&lt;/a&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%2F4vb006ksyf3zhie7nrmz.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%2F4vb006ksyf3zhie7nrmz.png" alt=" " width="799" height="417"&gt;&lt;/a&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%2Fzcfjmevcf2abqsqig3hu.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%2Fzcfjmevcf2abqsqig3hu.png" alt=" " width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;main.tf&lt;/code&gt; then got rewritten to reference &lt;code&gt;var.*&lt;/code&gt; instead of literal strings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;location = "East US"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;location = var.location&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name = "cohort-rg"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;name = var.resource_group_name&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;address_prefixes = ["10.0.0.0/24"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;still literal (left this one alone on purpose)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Frtth1ldxhublqpqe2lb5.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%2Frtth1ldxhublqpqe2lb5.png" alt=" " width="799" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One small mistake I made was in my variables.tf file. I used the description field to store the default value instead of explaining what the variable is for. For example, I used description = "cohort-rg" instead of description = "Name of the resource group".&lt;/p&gt;

&lt;p&gt;This did not cause any problems because Terraform still works with it, but it makes the file less clear and harder to understand later. It is a small mistake, but it is something I would correct to keep the code clean and easy to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  A duplicate-output gotcha
&lt;/h2&gt;

&lt;p&gt;Ran &lt;code&gt;terraform fmt&lt;/code&gt; and &lt;code&gt;terraform plan&lt;/code&gt; right after the refactor, expecting a clean run. Instead, Terraform stopped me with this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Error: Duplicate output definition&lt;br&gt;
An output named "vm_username" was already defined at main.tf:134,1-21. Output names must be unique within a module.&lt;/p&gt;
&lt;/blockquote&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%2F8a8cphds4mx53akr54j3.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%2F8a8cphds4mx53akr54j3.png" alt=" " width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once I looked closer it made sense: I'd created &lt;code&gt;outputs.tf&lt;/code&gt; with the two outputs, but never went back and deleted the original output blocks still sitting at the bottom of &lt;code&gt;main.tf&lt;/code&gt;. Terraform was looking at two files both trying to define &lt;code&gt;vm_username&lt;/code&gt;, and correctly refused to guess which one I meant. Deleted the leftover blocks from &lt;code&gt;main.tf&lt;/code&gt;, re-ran &lt;code&gt;plan&lt;/code&gt;, and got a clean 8-to-add.&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%2Fq55ee6fgj5kwrutqcb9e.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%2Fq55ee6fgj5kwrutqcb9e.png" alt=" " width="800" height="424"&gt;&lt;/a&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%2Fd69e4ic4l0ye2y99j6d9.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%2Fd69e4ic4l0ye2y99j6d9.png" alt=" " width="799" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Running terraform apply
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ssh_Public_key&lt;/code&gt; and &lt;code&gt;subscription_id&lt;/code&gt; don't have defaults in &lt;code&gt;variables.tf&lt;/code&gt; on purpose. They're marked &lt;code&gt;sensitive&lt;/code&gt;, and I didn't want them sitting in a file that might end up on GitHub. Terraform prompts for them at apply time instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Ft68qvvw0wh8xhlunmzcc.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%2Ft68qvvw0wh8xhlunmzcc.png" alt=" " width="800" height="230"&gt;&lt;/a&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%2Fa18xmdhwee20hswjncfl.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%2Fa18xmdhwee20hswjncfl.png" alt=" " width="800" height="370"&gt;&lt;/a&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%2F2x3xaub0cj95mmkrn090.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%2F2x3xaub0cj95mmkrn090.png" alt=" " width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring the remote backend
&lt;/h2&gt;

&lt;p&gt;This was the part of the brief I'd never actually done before, so it got its own careful pass.&lt;/p&gt;

&lt;p&gt;The idea: instead of &lt;code&gt;terraform.tfstate&lt;/code&gt; sitting on my machine, where it can get lost, overwritten, or just never seen by anyone else on a team, it lives in an Azure Storage Account, and Terraform reads and writes it there on every command.&lt;/p&gt;

&lt;p&gt;First, log in and create the storage account and a container inside it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Ft5y7e5lhlipvv7vwnoa7.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%2Ft5y7e5lhlipvv7vwnoa7.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cohorttfstate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--resource-group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cohort-rg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;eastus&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--sku&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Standard_LRS&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fek7qm89ziomvaz4ejyur.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%2Fek7qm89ziomvaz4ejyur.png" alt=" " width="799" height="395"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tfstate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--account-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cohorttfstate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--auth-mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fb8x1d6fokgsosuut4140.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%2Fb8x1d6fokgsosuut4140.png" alt=" " width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then tell Terraform where to find it, right inside the &lt;code&gt;terraform {}&lt;/code&gt; block:&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;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;azurerm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/azurerm"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 5.4.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# Backend configuration for storing the Terraform state in Azure Blob Storage&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"azurerm"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;resource_group_name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cohort-rg"&lt;/span&gt;
    &lt;span class="nx"&gt;storage_account_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cohorttfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;container_name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform.tfstate"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&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%2Fw353n4zi6xmomj30p0ud.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%2Fw353n4zi6xmomj30p0ud.png" alt=" " width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;terraform init&lt;/code&gt; again picks up the new backend and asks whether to migrate the state that's currently sitting locally:&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%2Flapcdtnc8548jik5qq9v.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%2Flapcdtnc8548jik5qq9v.png" alt=" " width="800" height="420"&gt;&lt;/a&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%2Fia7m0uowgbxqkza3fatu.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%2Fia7m0uowgbxqkza3fatu.png" alt=" " width="799" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From this point forward, every &lt;code&gt;terraform plan&lt;/code&gt; reads the state from &lt;strong&gt;Azure Blob Storage&lt;/strong&gt; instead of a local file. This also means the Terraform state file, which contains information such as my resource IDs, is no longer stored on my laptop.&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%2Fvy6jvn43qayil9j2bmro.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%2Fvy6jvn43qayil9j2bmro.png" alt=" " width="800" height="498"&gt;&lt;/a&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%2Fbp9sokzoc6h2dmylvg47.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%2Fbp9sokzoc6h2dmylvg47.png" alt=" " width="799" height="396"&gt;&lt;/a&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%2Fbvahgwa1hb8gjv4x78uu.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%2Fbvahgwa1hb8gjv4x78uu.png" alt=" " width="800" height="587"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"No changes" here is the whole point: it means the remote state and the real infrastructure agree with each other, from somewhere that isn't tied to my one laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting onto the box
&lt;/h2&gt;

&lt;p&gt;Time to prove there's an actual, usable server behind all this config.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh cohortuser1@4.157.245.198
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fopxi50plqgr8c0py0l15.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%2Fopxi50plqgr8c0py0l15.png" alt=" " width="800" height="601"&gt;&lt;/a&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%2F3yepk1thcgl9tli7dcbt.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%2F3yepk1thcgl9tli7dcbt.png" alt=" " width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I tried to update and upgrade in one line and typed &lt;code&gt;&amp;amp;&lt;/code&gt; where I meant &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade &amp;amp; update &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fh5p6s2vyklzkbk2e37bb.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%2Fh5p6s2vyklzkbk2e37bb.png" alt=" " width="799" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;amp;&lt;/code&gt; backgrounds the first command and immediately tries to run &lt;code&gt;update&lt;/code&gt; as its own program, which doesn't exist as a standalone command, hence &lt;code&gt;Command 'update' not found&lt;/code&gt;. &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; chains two commands so the second only runs after the first succeeds: one character, very different behaviour. Ran it properly afterward:&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%2Fqwzu2wj4pkinpzqzdq6u.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%2Fqwzu2wj4pkinpzqzdq6u.png" alt=" " width="800" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then installed the two things I actually needed: nginx to prove a web server can be reached from outside, and git to push the code.&lt;br&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;apt &lt;span class="nb"&gt;install &lt;/span&gt;nginx &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git &lt;span class="nt"&gt;-y&lt;/span&gt;
git &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fcv18y5c45uupvki0ttru.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%2Fcv18y5c45uupvki0ttru.png" alt=" " width="800" height="121"&gt;&lt;/a&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%2F3qbzrssg6040lzks47n9.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%2F3qbzrssg6040lzks47n9.png" alt=" " width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And in a browser, hitting the VM's public IP directly:&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%2Fa86swsrwimk2l8ldgvbv.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%2Fa86swsrwimk2l8ldgvbv.png" alt=" " width="799" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof in the portal
&lt;/h2&gt;

&lt;p&gt;Terraform reported that it successfully created &lt;strong&gt;eight resources&lt;/strong&gt;, and I verified this in the &lt;strong&gt;Azure portal&lt;/strong&gt;, which also shows the same resources. This confirmed that the Terraform configuration was applied successfully and that the infrastructure was created as expected.&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%2Fkzw59vyk4jv9umse7x5a.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%2Fkzw59vyk4jv9umse7x5a.png" alt=" " width="800" height="362"&gt;&lt;/a&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%2Ftzppzfc4a0qxz5yln5wk.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%2Ftzppzfc4a0qxz5yln5wk.png" alt=" " width="799" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tearing it down
&lt;/h2&gt;

&lt;p&gt;Once everything was verified and screenshotted, I destroyed the whole stack. It's a learning VM, not a production workload, and there's no reason to let a &lt;code&gt;Standard_B1s&lt;/code&gt; keep billing for the rest of the month for something I've already proven works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F3fyq605vtgopihdi27hi.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%2F3fyq605vtgopihdi27hi.png" alt=" " width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because the state lived remotely, destroying it was as clean as creating it. Terraform read the real state from Blob Storage, worked out exactly what it had made, and took it all back down in one pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting this into GitHub
&lt;/h2&gt;

&lt;p&gt;This is the section where I learned more about Git than about Terraform.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fl05347427oaptglui4ew.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%2Fl05347427oaptglui4ew.png" alt=" " width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I tried &lt;code&gt;git branch main&lt;/code&gt; before making a single commit, which fails because there's no commit yet for a branch to point at (&lt;code&gt;fatal: not a valid object name 'master'&lt;/code&gt;). Staged everything instead:&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%2F750c9coc8goqbqdhb6li.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%2F750c9coc8goqbqdhb6li.png" alt=" " width="800" height="409"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"first commit on terraform cloud cohort from master branch"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F763t1nd38boyg7yv540l.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%2F763t1nd38boyg7yv540l.png" alt=" " width="799" height="419"&gt;&lt;/a&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%2Fcbx0rkjvne9dxxgk7owu.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%2Fcbx0rkjvne9dxxgk7owu.png" alt=" " width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Renamed the default branch from &lt;code&gt;master&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt; to match what GitHub expects by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git branch main
git switch main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fbkvve2marllpfva53hym.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%2Fbkvve2marllpfva53hym.png" alt=" " width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Created the repo on GitHub, private, with a README ticked on (a decision that comes back to bite me two steps from now):&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%2F2sf36hx78mrvxo2ildj0.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%2F2sf36hx78mrvxo2ildj0.png" alt=" " width="800" height="404"&gt;&lt;/a&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%2Fp5ameu6qqptoywtsnva4.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%2Fp5ameu6qqptoywtsnva4.png" alt=" " width="799" height="406"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote add origin https://github.com/4thman/cloud-cohort.git
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fm3nku1rd93zjzoo4mc3x.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%2Fm3nku1rd93zjzoo4mc3x.png" alt=" " width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first push got rejected:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;remote: error: File .terraform/providers/registry.terraform.io/hashicorp/azurerm/5.4.0/windows_amd64/terraform-provider-azurerm_v5.4.0_x5.exe is 219.00 MB; this exceeds GitHub's file size limit of 100.00 MB&lt;/p&gt;
&lt;/blockquote&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%2F9moytit1h8vojvlm4qhy.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%2F9moytit1h8vojvlm4qhy.png" alt=" " width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Honestly, I didn't know GitHub even had a 100MB file size limit until this error told me. I'd just run &lt;code&gt;git add .&lt;/code&gt; and pushed everything in the folder without thinking about what was actually sitting inside &lt;code&gt;.terraform/&lt;/code&gt;. That folder is a local cache of provider plugins, genuinely huge (that one provider binary alone was 219MB), and it's something nobody needs in version control anyway since &lt;code&gt;terraform init&lt;/code&gt; rebuilds it from scratch on any machine. So it needed to go in &lt;code&gt;.gitignore&lt;/code&gt; instead of getting committed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;.&lt;span class="n"&gt;terraform&lt;/span&gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F03kqtcq8z0p4iopvjfq0.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%2F03kqtcq8z0p4iopvjfq0.png" alt=" " width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Staged it, committed it, and pushed again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"added .terraform folder to .gitignore due to push error"&lt;/span&gt;
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rejected a second time, this time with &lt;code&gt;[rejected] main -&amp;gt; main (fetch first)&lt;/code&gt;, because the README GitHub generated when I created the repo existed as a commit on the remote that my local history knew nothing about. Two histories, no shared ancestor Git could reconcile on its own.&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%2Fsjn2g0afh6ihluuectmb.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%2Fsjn2g0afh6ihluuectmb.png" alt=" " width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pulled the remote's history in, merged it with mine, and pushed one more time:&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%2Ftcs83tv1nbwa1ywukyzv.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%2Ftcs83tv1nbwa1ywukyzv.png" alt=" " width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the repo, live, with all three commits and the actual project files:&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%2Fe6kgzqr60i5r91rabb6v.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%2Fe6kgzqr60i5r91rabb6v.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/4thman/cloud-cohort" rel="noopener noreferrer"&gt;github.com/4thman/cloud-cohort&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Add &lt;strong&gt;.gitignore&lt;/strong&gt; before the first commit, rather than after the push. When I created the GitHub repository, I left the &lt;strong&gt;ADD README&lt;/strong&gt; option enabled, which meant the local and remote repositories had different histories. I fixed this by running git pull to bring the README into my local repository and merge the changes before pushing my files.&lt;/p&gt;

&lt;p&gt;Write proper descriptions in** variables.tf** instead of putting the default value in the description field. I should also add the &lt;strong&gt;.tfstate&lt;/strong&gt; files to .gitignore, not just &lt;strong&gt;.terraform/&lt;/strong&gt;. My state file ended up in the first commit, and although there was nothing sensitive in this particular file, Terraform state files can contain sensitive information and should not normally be committed to GitHub.&lt;/p&gt;

&lt;p&gt;None of these broke the deployment. All of them are the kind of thing you only really learn by hitting them once.&lt;/p&gt;




&lt;p&gt;If you made it this far, thanks for reading! 🙌 I’d love to hear from you what’s one Terraform, Azure, or Git mistake you’ve made while learning, and what did it teach you? Drop it in the comments; your experience might save someone else a few hours of frustration. If you found this walkthrough useful, like ❤️, comment 💬, and share 🔄 it with someone else learning Terraform or Azure. Let’s learn from the mistakes, not hide them! &lt;/p&gt;

</description>
      <category>terraform</category>
      <category>azure</category>
      <category>git</category>
      <category>cloudcomputing</category>
    </item>
    <item>
      <title>From Zero to Auto-Scaling Hero: Deploying a VMSS on Azure 🚀</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Wed, 08 Jul 2026 00:03:12 +0000</pubDate>
      <link>https://dev.to/4thman/from-zero-to-auto-scaling-hero-deploying-a-vmss-on-azure-48g5</link>
      <guid>https://dev.to/4thman/from-zero-to-auto-scaling-hero-deploying-a-vmss-on-azure-48g5</guid>
      <description>&lt;h1&gt;
  
  
  From Zero to Auto-Scaling Hero: Deploying a VMSS on Azure 🚀
&lt;/h1&gt;

&lt;p&gt;Ever wondered how big websites survive a sudden rush of visitors without crashing? The secret is &lt;strong&gt;auto-scaling&lt;/strong&gt; — the server army quietly grows when traffic spikes, and shrinks back down when things calm down, so nobody pays for computers they don't need.&lt;/p&gt;

&lt;p&gt;In this post, I'm walking you through &lt;strong&gt;exactly&lt;/strong&gt; how I built that on Microsoft Azure — from a completely empty subscription to a live webpage served by &lt;strong&gt;5 auto-scaling virtual machines&lt;/strong&gt; sitting behind a load balancer. &lt;/p&gt;

&lt;p&gt;By the end, you'll have your own mini traffic-proof web app running in the cloud, and you'll know how to tear it all down without leaving a single unpaid bill behind.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;This is part of a series!&lt;/strong&gt; I've already covered the foundational pieces in earlier posts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔗 &lt;a href="https://dev.to/4thman/inside-azure-networking-how-i-created-a-virtual-network-with-custom-subnets-35g4"&gt;https://dev.to/4thman/inside-azure-networking-how-i-created-a-virtual-network-with-custom-subnets-35g4&lt;/a&gt; — The "land and street" this project is built on&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔗 &lt;a href="https://dev.to/4thman/i-built-my-first-azure-vm-from-scratch-and-hosted-a-live-website-1ob0"&gt;https://dev.to/4thman/i-built-my-first-azure-vm-from-scratch-and-hosted-a-live-website-1ob0&lt;/a&gt; — The basics of a single server, before we multiply it into a scale set&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of the early steps below feel unfamiliar, those posts break them down in full detail. Here, I'll recap them quickly and then dive deep into the part that's new: &lt;strong&gt;auto-scaling with VMSS&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🧩 What We're Actually Building
&lt;/h2&gt;

&lt;p&gt;Think of it like building a house, street by street:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Resource Group&lt;/strong&gt; – an empty folder that holds everything we create (so we can delete it all in one click later)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Virtual Network (VNet) + Subnet&lt;/strong&gt; – the private street our servers will live on&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Virtual Machine Scale Set (VMSS)&lt;/strong&gt; – a &lt;em&gt;group&lt;/em&gt; of identical servers that can grow or shrink automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load Balancer&lt;/strong&gt; – the traffic cop that spreads visitors evenly across all the servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A tiny Apache web page&lt;/strong&gt; – so we have something to actually see working in the browser&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the flow in one picture (in your head, for now): &lt;strong&gt;Resource Group → Network → Subnet → Scale Set → Load Balancer → 🌍 Live Website&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Before You Start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An active Azure subscription (Pay-As-You-Go or free trial both work)&lt;/li&gt;
&lt;li&gt;Nothing else! Azure gives you everything else through the portal — no local software needed for this tutorial&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;(Optional but helpful)&lt;/em&gt; A quick read of my earlier posts on &lt;a href="https://dev.to/4thman/inside-azure-networking-how-i-created-a-virtual-network-with-custom-subnets-35g4"&gt;https://dev.to/4thman/inside-azure-networking-how-i-created-a-virtual-network-with-custom-subnets-35g4&lt;/a&gt; — This post builds directly on top of those concepts&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: Say Hello to the Azure Portal 👋
&lt;/h2&gt;

&lt;p&gt;Everything starts here — &lt;a href="https://portal.azure.com" rel="noopener noreferrer"&gt;portal.azure.com&lt;/a&gt;. This is mission control for every Azure resource you'll ever create.&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%2Fepsdl1idimlczddzxsoq.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%2Fepsdl1idimlczddzxsoq.png" alt=" " width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Azure homepage — our home base for the whole build.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Quick Recap — Resource Group, VNet &amp;amp; Subnet 📁🛣️🏘️
&lt;/h2&gt;

&lt;p&gt;I've already walked through this part step-by-step in an earlier post, so here's the short version — &lt;strong&gt;check out Setting up a Resource Group, Virtual Network &amp;amp; Subnet on Azure for the full click-by-click guide.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what happened, in a nutshell:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Created a Resource Group&lt;/strong&gt; called &lt;strong&gt;Smyles-RG&lt;/strong&gt; — the folder that holds everything in this project, so it can all be deleted in one click later.&lt;/li&gt;
&lt;/ol&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%2Fk1lz4devqnd8px7uof9m.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%2Fk1lz4devqnd8px7uof9m.png" alt=" " width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Built a Virtual Network&lt;/strong&gt; called &lt;strong&gt;Smyles-VN&lt;/strong&gt; inside that resource group — a private, isolated network, like a gated estate for our resources.&lt;/li&gt;
&lt;/ol&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%2Frqfnh0jb0jd7nw5tztl3.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%2Frqfnh0jb0jd7nw5tztl3.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Carved out a Subnet&lt;/strong&gt; called &lt;strong&gt;Smyles-Subnet&lt;/strong&gt; inside the VNet — a smaller slice of that estate where our servers will actually live.&lt;/li&gt;
&lt;/ol&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%2F9gzgscrhb2nfn2itwy88.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%2F9gzgscrhb2nfn2itwy88.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Simple English tip:&lt;/strong&gt; Resource Group = the folder. VNet = the private estate. Subnet = the specific street inside it. Our servers move in next.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With the land prepped, we're ready for the main event.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Create the Virtual Machine Scale Set (VMSS) 🖥️🖥️🖥️
&lt;/h2&gt;

&lt;p&gt;This is the star of the show. A &lt;strong&gt;VMSS&lt;/strong&gt; doesn't create just &lt;em&gt;one&lt;/em&gt; server — it creates a &lt;em&gt;template&lt;/em&gt; for identical servers that Azure can multiply or shrink automatically based on demand.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 — Find and open VMSS
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Search &lt;strong&gt;"vmss"&lt;/strong&gt; in the top search bar and select &lt;strong&gt;Virtual machine scale sets&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&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%2Fa6lt9dixkw5ex1x9v1iv.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%2Fa6lt9dixkw5ex1x9v1iv.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&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%2Fg0ydcaieagisn1pqtf0q.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%2Fg0ydcaieagisn1pqtf0q.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 — Basics: name, region, and zones
&lt;/h3&gt;

&lt;p&gt;Fill in the resource group (&lt;strong&gt;Smyles-RG&lt;/strong&gt;), give the scale set a name (&lt;strong&gt;Smyles-vmms&lt;/strong&gt;), pick your region, and choose &lt;strong&gt;Zones 1, 2, 3&lt;/strong&gt; for high availability (this spreads your servers across different physical data centers, so one outage doesn't take everything down).&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%2F2rohzlhhmogtzpplfxzv.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%2F2rohzlhhmogtzpplfxzv.png" alt=" " width="799" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 — Orchestration and scaling mode
&lt;/h3&gt;

&lt;p&gt;Choose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Orchestration mode: Flexible&lt;/strong&gt; — the modern, more flexible way Azure manages your servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaling mode: Autoscaling&lt;/strong&gt; — this is the whole point! Let Azure decide how many servers we need based on CPU usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxj1mqps2mq8nef0g3tmj.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%2Fxj1mqps2mq8nef0g3tmj.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.4 — Configure the scaling rules
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;Configure&lt;/strong&gt; to fine-tune exactly &lt;em&gt;how&lt;/em&gt; the auto-scaling should behave.&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%2F1tl1psbywp1g8ldg0964.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%2F1tl1psbywp1g8ldg0964.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Add a scaling condition&lt;/strong&gt;, name it (&lt;strong&gt;Smyles-scaling-condition&lt;/strong&gt;), and set the instance limits — I chose a &lt;strong&gt;minimum of 2&lt;/strong&gt; and a &lt;strong&gt;maximum of 5&lt;/strong&gt; servers.&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%2F5jajvv2ly3vummf9254z.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%2F5jajvv2ly3vummf9254z.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then define the actual trigger rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scale out&lt;/strong&gt; (add a server) when CPU usage goes &lt;strong&gt;above 80%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale in&lt;/strong&gt; (remove a server) when CPU usage drops &lt;strong&gt;below 20%&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy70dj9mxlncf8k4snibp.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%2Fy70dj9mxlncf8k4snibp.png" alt=" " width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Save it, and you'll see both the &lt;strong&gt;Default condition&lt;/strong&gt; and our new &lt;strong&gt;Smyles-scaling-condition&lt;/strong&gt; sitting side by side.&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%2Fa4nwri5mows6tlm4z1rn.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%2Fa4nwri5mows6tlm4z1rn.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;In plain English:&lt;/strong&gt; "If the servers are working too hard (above 80% CPU), add a helper. If they're bored (below 20% CPU), send a helper home. Never go below 2 or above 5 helpers."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3.5 — Instance details: OS, size, and count
&lt;/h3&gt;

&lt;p&gt;Set the instance count to &lt;strong&gt;5&lt;/strong&gt;, choose the image — I picked &lt;strong&gt;Ubuntu Server 24.04 LTS&lt;/strong&gt; — and select a size. I went with the budget-friendly &lt;strong&gt;Standard B1ms&lt;/strong&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%2F93wnmlk4ucb9pn7gefqj.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%2F93wnmlk4ucb9pn7gefqj.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.6 — Admin account
&lt;/h3&gt;

&lt;p&gt;Create a login for the servers. I used the username &lt;strong&gt;smyles&lt;/strong&gt; with a password (SSH keys work too, if you prefer).&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%2Fvioc4yg6a09rcghb9ctp.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%2Fvioc4yg6a09rcghb9ctp.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.7 — Networking: plug into our VNet
&lt;/h3&gt;

&lt;p&gt;Head to the &lt;strong&gt;Networking&lt;/strong&gt; tab. Select  &lt;strong&gt;Smyles-VN&lt;/strong&gt; and &lt;strong&gt;Smyles-Subnet&lt;/strong&gt;. Click &lt;strong&gt;Create new nic&lt;/strong&gt; to set up the network card for our servers.&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%2F22cmgrsj6hqnzojugwe4.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%2F22cmgrsj6hqnzojugwe4.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Name the NIC (&lt;strong&gt;Smyles-Networkinterface&lt;/strong&gt;), keep the network security group on &lt;strong&gt;Basic&lt;/strong&gt;, and allow &lt;strong&gt;HTTP (port 80)&lt;/strong&gt; so the outside world can actually reach our website.&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%2Fiionqivpm76cnyimgs3l.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%2Fiionqivpm76cnyimgs3l.png" alt=" " width="799" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.8 — Add a Load Balancer
&lt;/h3&gt;

&lt;p&gt;Still on the Networking tab, choose &lt;strong&gt;Azure load balancer&lt;/strong&gt;, then click &lt;strong&gt;Create a load balancer&lt;/strong&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%2F36rmi0crjhcu2thucc64.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%2F36rmi0crjhcu2thucc64.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Name it &lt;strong&gt;Smyles-loadbalancer&lt;/strong&gt;, set the type to &lt;strong&gt;Public&lt;/strong&gt; (so it has an internet-facing address), and the protocol to &lt;strong&gt;TCP&lt;/strong&gt;. Click &lt;strong&gt;Create&lt;/strong&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%2F7b3t43ifunc2jlcv9nce.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%2F7b3t43ifunc2jlcv9nce.png" alt=" " width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Simple English tip:&lt;/strong&gt; A load balancer is like a receptionist. Visitors don't talk to individual servers — they talk to the receptionist, who quietly sends them to whichever server is free.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3.9 — Health monitoring
&lt;/h3&gt;

&lt;p&gt;Switch to the &lt;strong&gt;Health&lt;/strong&gt; tab and enable &lt;strong&gt;Application health monitoring&lt;/strong&gt;. This lets Azure automatically detect and replace any server that stops responding.&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%2Fwlbyuefgbc8cwhx1rzke.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%2Fwlbyuefgbc8cwhx1rzke.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Configure&lt;/strong&gt; and set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Protocol: &lt;strong&gt;HTTP&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Port: &lt;strong&gt;80&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Request path: &lt;strong&gt;/&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then &lt;strong&gt;Save&lt;/strong&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%2Fqb8614u5fa19rusd8flb.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%2Fqb8614u5fa19rusd8flb.png" alt=" " width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.10 — Custom Data (the magic script)
&lt;/h3&gt;

&lt;p&gt;This is where the servers get their personality. In the &lt;strong&gt;Advanced&lt;/strong&gt; tab, under &lt;strong&gt;Custom data and cloud-init&lt;/strong&gt;, I pasted a small Bash script that automatically installs a web server and creates a webpage — the moment each server boots up, it's already ready to greet visitors.&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%2Fnglb3v7cpybe6z0j1fxv.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%2Fnglb3v7cpybe6z0j1fxv.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the exact script I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;apache2 &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;apache2
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start apache2
&lt;span class="nv"&gt;HOSTNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | sudo tee /var/www/html/index.html
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;body style="font-family:Arial;text-align:center;padding-top:100px;background:#f4f4f4"&amp;gt;
&amp;lt;h1 style="color:#0078D4"&amp;gt;Hello from &lt;/span&gt;&lt;span class="nv"&gt;$HOSTNAME&lt;/span&gt;&lt;span class="sh"&gt;&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;Azure VMSS Instance&amp;lt;/p&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart apache2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this script does, in plain English:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apt-get update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Refreshes the list of available software&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apt-get install apache2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Installs Apache — a popular web server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemctl enable/start apache2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Turns the web server on, and keeps it on after reboots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HOSTNAME=$(hostname)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Grabs the unique name of &lt;em&gt;this particular&lt;/em&gt; server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cat &amp;lt;&amp;lt;EOF ... EOF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Writes a simple HTML webpage that greets visitors with the server's own name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemctl restart apache2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Restarts the web server so the new page shows up&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cool part? Because this script runs on &lt;strong&gt;every single instance&lt;/strong&gt; in the scale set, each server will proudly display its &lt;strong&gt;own unique name&lt;/strong&gt; — so when we later refresh the load balancer's page, we can literally watch traffic bounce between different servers!&lt;/p&gt;

&lt;h3&gt;
  
  
  3.11 — Review and create
&lt;/h3&gt;

&lt;p&gt;With everything configured, click &lt;strong&gt;Review + create&lt;/strong&gt;. Wait for the green &lt;strong&gt;"Validation passed"&lt;/strong&gt; banner, then click &lt;strong&gt;Create&lt;/strong&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%2Fe3nbvu7zbljzhxxf5hv7.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%2Fe3nbvu7zbljzhxxf5hv7.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sit back and relax while Azure spins up &lt;strong&gt;5 identical virtual machines&lt;/strong&gt; for you. ☕&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%2Ffsb821tsgmxfqmvxzuov.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%2Ffsb821tsgmxfqmvxzuov.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: See It Come Alive 🌍
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 — Grab the load balancer's address
&lt;/h3&gt;

&lt;p&gt;Open your new scale set (&lt;strong&gt;Smyles-vmss&lt;/strong&gt;) and go to &lt;strong&gt;Networking → Load balancing&lt;/strong&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%2F209evmauux6qtv5au63g.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%2F209evmauux6qtv5au63g.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Copy the &lt;strong&gt;Frontend IP address&lt;/strong&gt; — this is the single public address that represents &lt;em&gt;all five&lt;/em&gt; of your servers.&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%2Fudke3gwffdu390vayoqi.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%2Fudke3gwffdu390vayoqi.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 — Paste it in your browser
&lt;/h3&gt;

&lt;p&gt;Paste that IP address into a new browser tab and hit enter...&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%2Fewnif5d0xa4rx12bonny.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%2Fewnif5d0xa4rx12bonny.png" alt=" " width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Hello from smyles-vmR4TV8G"&lt;/strong&gt; — a real, working, auto-scaling web app, built entirely from the ground up! Refresh the page a few times and watch the hostname change as the load balancer sends you to different servers behind the scenes. That's auto-scaling infrastructure in action. 🎉&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Clean Up — Don't Leave the Lights On! 💡
&lt;/h2&gt;

&lt;p&gt;Cloud resources cost money for every hour they exist, even if nobody's visiting your website. Once you're done experimenting, tear it all down — and thanks to our Resource Group trick from Step 2 (or see the full explanation in my earlier Resource Group post), this takes seconds.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go back to &lt;strong&gt;Resource groups&lt;/strong&gt; and open &lt;strong&gt;Smyles-RG&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&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%2F4gxazdjd6d4lzfj0dwsf.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%2F4gxazdjd6d4lzfj0dwsf.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Delete resource group&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&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%2Fnninyxawcuexe3i5vsfm.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%2Fnninyxawcuexe3i5vsfm.png" alt=" " width="799" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Type the resource group name to confirm (Azure double-checks so you don't delete anything by accident), then click &lt;strong&gt;Delete&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&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%2Fionxvzgalnjdl26k2ys0.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%2Fionxvzgalnjdl26k2ys0.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And just like that — the VMs, load balancer, network, subnet, and everything else vanish, and your billing stops. One folder in, one folder out. Also do the same for &lt;strong&gt;&lt;em&gt;NetworkWatcherRG&lt;/em&gt;&lt;/strong&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%2Fjf6emgmz1rnaiuptl9dg.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%2Fjf6emgmz1rnaiuptl9dg.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 What We Actually Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource Groups&lt;/strong&gt; keep your projects tidy and easy to delete&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Virtual Networks &amp;amp; Subnets&lt;/strong&gt; give your servers a private, controlled space to live in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VMSS with Autoscaling&lt;/strong&gt; means your infrastructure grows and shrinks with real demand — you stop paying for idle servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load Balancers&lt;/strong&gt; spread traffic so no single server gets overwhelmed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Data / cloud-init&lt;/strong&gt; lets you automate server setup completely — no manual SSH-ing into five different machines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health monitoring&lt;/strong&gt; keeps your app resilient by auto-replacing unhealthy instances&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the entire journey — from an empty Azure subscription to a self-healing, self-scaling website, torn down responsibly at the end. If you followed along, you didn't just read about cloud infrastructure — you built it. 🙌&lt;/p&gt;

&lt;p&gt;Got questions or stuck somewhere? &lt;strong&gt;Drop a comment below&lt;/strong&gt; — happy to help troubleshoot!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Follow me for more hands-on Azure and cloud engineering walkthroughs, straight from my own learning journey.&lt;/em&gt; ☁️&lt;/p&gt;

</description>
      <category>azure</category>
      <category>cloud</category>
      <category>developer</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I Built My First Azure Virtual Machine From Scratch and Hosted a Website Using Apache2 🌐</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Sat, 20 Jun 2026 15:15:00 +0000</pubDate>
      <link>https://dev.to/4thman/i-built-my-first-azure-vm-from-scratch-and-hosted-a-live-website-1ob0</link>
      <guid>https://dev.to/4thman/i-built-my-first-azure-vm-from-scratch-and-hosted-a-live-website-1ob0</guid>
      <description>&lt;h1&gt;
  
  
  Building My First Azure Virtual Machine From the Ground Up 🚀
&lt;/h1&gt;

&lt;p&gt;In this post, I'll walk you through &lt;strong&gt;exactly&lt;/strong&gt; how I went from an empty Azure subscription to a live Ubuntu VM running Apache, complete with every screen I clicked through. No skipped steps, no "and then magic happens."&lt;/p&gt;

&lt;p&gt;By the end, you'll be able to do this yourself in under 20 minutes. Let's go! 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💬 &lt;strong&gt;Heads up:&lt;/strong&gt; This is a follow-along tutorial. Grab a coffee, open a second tab with your Azure portal, and build alongside me.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  ✅ What You'll Build
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[x] A &lt;strong&gt;Resource Group&lt;/strong&gt; to hold everything together&lt;/li&gt;
&lt;li&gt;[x] A &lt;strong&gt;Virtual Network (VNet)&lt;/strong&gt; with a custom address space&lt;/li&gt;
&lt;li&gt;[x] A &lt;strong&gt;Subnet&lt;/strong&gt; inside that VNet&lt;/li&gt;
&lt;li&gt;[x] A &lt;strong&gt;Virtual Machine&lt;/strong&gt; (Ubuntu 24.04 LTS) deployed into that subnet&lt;/li&gt;
&lt;li&gt;[x] SSH access into the VM from VS Code's terminal&lt;/li&gt;
&lt;li&gt;[x] A live &lt;strong&gt;Apache2 web server&lt;/strong&gt; you can visit in your browser&lt;/li&gt;
&lt;li&gt;[x] A clean teardown so you don't get billed for forgetting it 💸&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;An Azure account (free tier works fine)&lt;/li&gt;
&lt;li&gt;A terminal — I used &lt;strong&gt;VS Code's integrated terminal (bash/Git Bash)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;15–20 minutes and a bit of curiosity&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🗺️ The Architecture (in plain English)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Resource Group  (the folder that holds everything)
   └── Virtual Network   (10.0.0.0/16)
         └── Subnet      (10.0.0.0/24)
               └── Virtual Machine  (Ubuntu 24.04, public IP, SSH + HTTP open)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resource Group first, then network, then subnet, then the actual VM goes &lt;em&gt;into&lt;/em&gt; that subnet. Let's build it top-down.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Create a Resource Group 📁
&lt;/h2&gt;

&lt;p&gt;A Resource Group is basically a folder in Azure — it keeps every resource for this project (network, VM, disks, IP) organized together so you can manage or delete them as one unit.&lt;/p&gt;

&lt;p&gt;Firstly I login into the Azure Homepage via &lt;a href="https://portal.azure.com/" rel="noopener noreferrer"&gt;https://portal.azure.com/&lt;/a&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%2Fep8vflamlqtcb9czzj7w.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%2Fep8vflamlqtcb9czzj7w.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From the Azure homepage, I searched for &lt;strong&gt;Resource Groups&lt;/strong&gt; and hit &lt;strong&gt;Create&lt;/strong&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%2Fnurg5w70jjv925g5nmw9.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%2Fnurg5w70jjv925g5nmw9.png" alt=" " width="800" height="361"&gt;&lt;/a&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%2Ff0iznoswc7nlth37lza3.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%2Ff0iznoswc7nlth37lza3.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I named mine &lt;code&gt;VM-RG-Test&lt;/code&gt; and picked &lt;strong&gt;France Central&lt;/strong&gt; as my region — pick whatever region is closest to you.&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%2Fe97gel88sr93jk6weh5x.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%2Fe97gel88sr93jk6weh5x.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While on the Review + Create page, I clicked Create after reviewing the details of the Resource Group.&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%2Flehbeipofkmukowhc7ys.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%2Flehbeipofkmukowhc7ys.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few seconds later — done! ✅&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%2Fx3xpi6kc7nwxl062mbfh.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%2Fx3xpi6kc7nwxl062mbfh.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Whatever region you pick here is the one you should use for &lt;em&gt;every&lt;/em&gt; resource in this tutorial. Mixing regions adds latency and (sometimes) extra cost.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 2: Create a Virtual Network (VNet) 🌐
&lt;/h2&gt;

&lt;p&gt;Next, your VM needs a network to live in. I searched for &lt;strong&gt;Virtual networks&lt;/strong&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%2Fw0iiaukk7xivjpqkwz0a.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%2Fw0iiaukk7xivjpqkwz0a.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and clicked &lt;strong&gt;Create&lt;/strong&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%2Fmkheezg9c1b38woo79cc.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%2Fmkheezg9c1b38woo79cc.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I assigned it to my &lt;code&gt;VM-RG-Test&lt;/code&gt; resource group and named it &lt;code&gt;VM-Virtual-Network&lt;/code&gt;, same region as before.&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%2F6aiup8jtz8rjaad1ckgv.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%2F6aiup8jtz8rjaad1ckgv.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the &lt;strong&gt;Address space&lt;/strong&gt; tab, Azure gives you a default &lt;code&gt;10.0.0.0/16&lt;/code&gt; range with a default subnet already attached. I deleted that default subnet — I want to create my own with a name that actually makes sense — then hit &lt;strong&gt;Review + create&lt;/strong&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%2Ficrin7j0g7b75uz8qhe1.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%2Ficrin7j0g7b75uz8qhe1.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Validation passed, so I clicked &lt;strong&gt;Create&lt;/strong&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%2F8h9hqlbxf36zn7x9528g.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%2F8h9hqlbxf36zn7x9528g.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the deployment finished, I clicked &lt;strong&gt;Go to resource&lt;/strong&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%2Flir6qyndnjxtm7fjvgkb.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%2Flir6qyndnjxtm7fjvgkb.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Add a Subnet 🧩
&lt;/h2&gt;

&lt;p&gt;A VNet without a subnet is just an empty container — the subnet is where your VM's network card actually attaches.&lt;/p&gt;

&lt;p&gt;Inside the VNet, I went to &lt;strong&gt;Settings → Subnets&lt;/strong&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%2Fy7cojm2kntabspkarozj.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%2Fy7cojm2kntabspkarozj.png" alt=" " width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And clicked &lt;strong&gt;Create&lt;/strong&gt; on the &lt;strong&gt;subnet&lt;/strong&gt; 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%2F9262iz41eis9ukzyyc2b.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%2F9262iz41eis9ukzyyc2b.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I named it &lt;code&gt;VM-Sub-Net&lt;/code&gt;, left the default &lt;code&gt;10.0.0.0/24&lt;/code&gt; range (251 usable IPs — plenty for one VM), and clicked &lt;strong&gt;Add&lt;/strong&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%2Fb33sw282vps8ln6zhtjt.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%2Fb33sw282vps8ln6zhtjt.png" alt=" " width="799" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And there it is — one ready-to-use subnet.&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%2Fr6wvthba96v5rsr7uq38.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%2Fr6wvthba96v5rsr7uq38.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Create the Virtual Machine 🖥️
&lt;/h2&gt;

&lt;p&gt;This is the main event. I searched for &lt;strong&gt;Virtual machines&lt;/strong&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%2F6prl1bjaoh81x5zxyx1j.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%2F6prl1bjaoh81x5zxyx1j.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And clicked &lt;strong&gt;Create&lt;/strong&gt;, then selected the first option, which is &lt;strong&gt;Virtual Machine.&lt;/strong&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%2Fuqep9q160cppi5gyx2gi.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%2Fuqep9q160cppi5gyx2gi.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 — Basics: name, region, resource group
&lt;/h3&gt;

&lt;p&gt;I dropped this VM into my existing &lt;code&gt;VM-RG-Test&lt;/code&gt; resource group and named it &lt;code&gt;VM-Test&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%2Fijk8tmusoatqtlgme7d2.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%2Fijk8tmusoatqtlgme7d2.png" alt=" " width="799" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 — Image, size, and authentication
&lt;/h3&gt;

&lt;p&gt;For the image, I went with &lt;strong&gt;Ubuntu Server 24.04 LTS - x64 Gen2&lt;/strong&gt;, kept the size small (&lt;code&gt;Standard_B1ls&lt;/code&gt;) to keep costs minimal, and chose &lt;strong&gt;SSH public key&lt;/strong&gt; as the authentication method instead of a password — much safer.&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%2F7bo4rqgc73dsci37lozq.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%2F7bo4rqgc73dsci37lozq.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 — Username, SSH key, and inbound ports
&lt;/h3&gt;

&lt;p&gt;I set my username to &lt;code&gt;VM-David&lt;/code&gt;, let Azure &lt;strong&gt;generate a new SSH key pair&lt;/strong&gt; for me, named the key &lt;code&gt;VM-Test_key&lt;/code&gt;, and allowed inbound traffic on &lt;strong&gt;HTTP (80)&lt;/strong&gt; and &lt;strong&gt;SSH (22)&lt;/strong&gt; — HTTP so I can later view my website, SSH so I can connect and manage the server.&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%2Fwa7aq1q2u45rzamqkexk.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%2Fwa7aq1q2u45rzamqkexk.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Allowing SSH from "any IP" is fine for a quick test like this, but for production you'd lock it down to your own IP address using the Advanced networking controls.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4.4 — Disks
&lt;/h3&gt;

&lt;p&gt;I kept the default &lt;strong&gt;Standard SSD&lt;/strong&gt; for the OS disk — good enough for a test server, no need for Premium SSD here.&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%2Fw2jw0aoekzm6ln0nzdw2.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%2Fw2jw0aoekzm6ln0nzdw2.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.5 — Networking: plugging it into our VNet/Subnet
&lt;/h3&gt;

&lt;p&gt;This is where everything we built earlier comes together — I selected my &lt;code&gt;VM-Virtual-Network&lt;/code&gt; and &lt;code&gt;VM-Sub-Net&lt;/code&gt;, let Azure create a new public IP, and chose &lt;strong&gt;HTTP (80), SSH (22)&lt;/strong&gt; as the allowed inbound ports.&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%2F6udoeuttrnrzylynfnd2.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%2F6udoeuttrnrzylynfnd2.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also checked &lt;strong&gt;"Delete public IP and NIC when VM is deleted"&lt;/strong&gt; — a small checkbox that saves you from leaving orphaned (and billable!) resources behind later.&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%2Fv2q77sxkc7zg5i4ub0xi.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%2Fv2q77sxkc7zg5i4ub0xi.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.6 — Review + Create
&lt;/h3&gt;

&lt;p&gt;Validation passed (estimated cost: under $8/month for this tiny config 🙌), so I hit &lt;strong&gt;Create&lt;/strong&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%2F3mjejuptlor8s94g4hcp.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%2F3mjejuptlor8s94g4hcp.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Azure immediately prompted me to &lt;strong&gt;download the private key&lt;/strong&gt; — this is your &lt;em&gt;only&lt;/em&gt; chance to grab it, since Azure never stores the private half of the key pair.&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%2Fghiloy51kbgxam4sbxm8.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%2Fghiloy51kbgxam4sbxm8.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And there's my &lt;code&gt;.pem&lt;/code&gt; key, safely sitting in my Downloads folder.&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%2Fvwkztrs6w2it0tdpcp3d.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%2Fvwkztrs6w2it0tdpcp3d.png" alt=" " width="800" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔐 &lt;strong&gt;Don't lose this file.&lt;/strong&gt; Without it, you can't SSH into your VM. Treat it like a password.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A minute or two later, the deployment finished.&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%2Fs9tl6fej0xwubshhprva.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%2Fs9tl6fej0xwubshhprva.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Confirm the VM Is Alive ✅
&lt;/h2&gt;

&lt;p&gt;On the VM's overview page, I could see its public IP, private IP, region, and size — and the status read &lt;strong&gt;Running&lt;/strong&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%2F3incap25ckwustqjklb9.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%2F3incap25ckwustqjklb9.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: SSH In and Install Apache 🔧
&lt;/h2&gt;

&lt;p&gt;Time to actually log into the server. I opened VS Code's terminal then swicthed to Bash from Powershell(you can use Git-bash also), navigated to my Downloads folder (where the key lives), locked down the key's permissions, and connected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Go to where the key was downloaded&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;Downloads

&lt;span class="c"&gt;# 2. Restrict permissions on the key (required by SSH)&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;400 VM-Test_key.pem
&lt;span class="c"&gt;# (chmod 600 also works on most systems)&lt;/span&gt;

&lt;span class="c"&gt;# 3. Connect to the VM using the key, username, and public IP&lt;/span&gt;
ssh &lt;span class="nt"&gt;-i&lt;/span&gt; VM-Test_key.pem VM-David@98.66.137.84

&lt;span class="c"&gt;# 4. Update package lists&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update

&lt;span class="c"&gt;# 5. Install Apache2 web server&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;apache2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first time you connect, SSH will ask if you trust the host's fingerprint — type &lt;code&gt;yes&lt;/code&gt; to continue. Running &lt;code&gt;whoami&lt;/code&gt; afterward confirmed I was logged in successfully.&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%2F2x8488jtvd5664iie0vs.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%2F2x8488jtvd5664iie0vs.png" alt=" " width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After updating, I installed Apache2:&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%2Fqxyp8v31ufynmwyp59m3.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%2Fqxyp8v31ufynmwyp59m3.png" alt=" " width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: See It Live in the Browser 🌍
&lt;/h2&gt;

&lt;p&gt;With Apache installed and running, I opened a browser and typed in the VM's &lt;strong&gt;public IP address&lt;/strong&gt;. And there it was — the Apache2 default "It works!" page, served straight from my own Azure VM.&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%2F9jhwg2cl2cxdbq12g2ax.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%2F9jhwg2cl2cxdbq12g2ax.png" alt=" " width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This was honestly the most satisfying part — seeing a page load from infrastructure I built myself, end to end. 🎉&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 8: Clean Up — Delete the Resource Group 🧹
&lt;/h2&gt;

&lt;p&gt;Azure bills you for running resources, so once I was done testing, I deleted the whole resource group. Because everything (VM, disk, NIC, public IP, SSH key, VNet) was inside &lt;code&gt;VM-RG-Test&lt;/code&gt;, &lt;strong&gt;one deletion removes it all&lt;/strong&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%2F45tyub1lmha8yhad6rvt.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%2F45tyub1lmha8yhad6rvt.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;👀 Spot the extra one? There's also a NetworkWatcherRG sitting in that list. Azure creates this automatically the first time you use any network diagnostics feature in a region — I didn't make it on purpose. It doesn't cost anything by itself, but since I was cleaning house, I deleted it too.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Azure shows you exactly what's about to be deleted — 7 resources in my case — and makes you type the resource group's name to confirm. No accidental deletions here.&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%2Ff0s7zrzp93jimmv5jupw.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%2Ff0s7zrzp93jimmv5jupw.png" alt=" " width="799" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A couple minutes later, it was gone — clean slate, zero ongoing cost.&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%2Fbk6fu4jhv0lwe327ef5g.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%2Fbk6fu4jhv0lwe327ef5g.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Recap
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What Happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Created a Resource Group (&lt;code&gt;VM-RG-Test&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Created a Virtual Network (&lt;code&gt;VM-Virtual-Network&lt;/code&gt;, &lt;code&gt;10.0.0.0/16&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Added a Subnet (&lt;code&gt;VM-Sub-Net&lt;/code&gt;, &lt;code&gt;10.0.0.0/24&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Deployed an Ubuntu VM into that subnet with an SSH key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Connected via SSH from VS Code's terminal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Installed and ran Apache2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Visited the live page in a browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Deleted everything to avoid surprise charges&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  🗣️ Over to you
&lt;/h3&gt;

&lt;p&gt;Drop a comment below 👇 — and if this helped you, a like/follow goes a long way! 🙌&lt;/p&gt;

</description>
      <category>azure</category>
      <category>cloud</category>
      <category>devops</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Inside Azure Networking: How I Created a Virtual Network with Custom Subnets</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Thu, 04 Jun 2026 23:54:09 +0000</pubDate>
      <link>https://dev.to/4thman/inside-azure-networking-how-i-created-a-virtual-network-with-custom-subnets-35g4</link>
      <guid>https://dev.to/4thman/inside-azure-networking-how-i-created-a-virtual-network-with-custom-subnets-35g4</guid>
      <description>&lt;p&gt;If you've ever stared at the Azure portal wondering where to even begin with networking, you're not alone. I was right there a few weeks ago. In this post, I'm walking you through exactly what I did — from creating a resource group to building a virtual network with three custom subnets. No fluff, just real steps with real screenshots.&lt;br&gt;
Let's get into it.&lt;/p&gt;

&lt;p&gt;Head over to portal.azure.com&lt;br&gt;
Once logged in,&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7hq3gbn9sc4ccouc6ddd.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%2F7hq3gbn9sc4ccouc6ddd.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Search for Resource Groups&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first thing you want to do is head to the Azure portal and use the search bar at the top to look up "resource group". You'll see it pop up under Services — click on Resource groups.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ehdhzvsvwh82om5ixty.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%2F0ehdhzvsvwh82om5ixty.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💡 Quick note: &lt;em&gt;I covered resource group creation in a previous blog too, but I'm including the steps here again so this post is fully self-contained. No jumping around needed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — Create a New Resource Group&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once you're on the Resource Groups page, you'll see a list of any existing groups or an empty list with a create button visible on the page. To create a new one, click the + Create button at the top left.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fugt25ehdlmhw5ni9zesc.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%2Fugt25ehdlmhw5ni9zesc.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — Fill in the Resource Group Details&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On the creation form, you need to fill in three things:&lt;/p&gt;

&lt;p&gt;Subscription — select your active subscription (mine is Azure subscription 1)&lt;br&gt;
Resource group name — I named mine Netaccess&lt;br&gt;
Region — I chose (Africa) South Africa North since that's closest to me&lt;/p&gt;

&lt;p&gt;Once filled in, click Review + create.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft3nt44ao99c0qvbzvcrd.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%2Ft3nt44ao99c0qvbzvcrd.png" alt=" " width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — Review and Create the Resource Group&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You'll be taken to a summary screen. Confirm the details look right — subscription, name, and region. Then hit the Create button at the bottom.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmbvxamym9bwrtp5qdps6.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%2Fmbvxamym9bwrtp5qdps6.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 — Resource Group Created!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once Azure finishes provisioning (it's pretty fast — usually a few seconds), you'll see a notification on the right confirming that the resource group was created successfully. Your new Netaccess group will now appear in the list.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxconcxza0wj7kfmjtbw6.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%2Fxconcxza0wj7kfmjtbw6.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6 — Search for Virtual Networks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now that our resource group is ready, it's time to create a Virtual Network inside it. Use the top search bar again and type "virtual networks". Click on Virtual networks from the Services dropdown.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm3i1chajv1rvyu86qhpo.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%2Fm3i1chajv1rvyu86qhpo.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 7 — Create a Virtual Network&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You'll land on the Virtual Networks overview page. Since we don't have any yet, it'll say "No virtual networks to display." Click the + Create button (either at the top or the one in the centre of the 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.amazonaws.com%2Fuploads%2Farticles%2F2f48yti6ivsqp8z4c57g.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%2F2f48yti6ivsqp8z4c57g.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 8 — Fill in the Virtual Network Basics&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On the Basics tab of the creation wizard, configure the following:&lt;/p&gt;

&lt;p&gt;Subscription — Azure subscription 1&lt;br&gt;
Resource group — Select the Netaccess group we just created&lt;br&gt;
Virtual network name — I named it Netaccess-VM&lt;br&gt;
Region — (Africa) South Africa North&lt;/p&gt;

&lt;p&gt;Then click Next to move to the next tab.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftxhndetaowch5f2i18ab.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%2Ftxhndetaowch5f2i18ab.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 9 — Configure Address Space and Delete the Default Subnet&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Skip past the Security tab (leave everything as default) and head to the Address space tab. Here Azure has already set up a default address space of 10.0.0.0/16 — that gives us 65,536 IP addresses, which is way more than enough.&lt;br&gt;
Now here's the important part: Azure also pre-creates a default subnet with the range 10.0.0.0 - 10.0.0.255 (/24). We're going to delete this because we want to create our own subnets with custom names and IP ranges.&lt;br&gt;
Click the trash/delete icon on the right side of the default subnet row to remove it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffvm925y995eb51lox0e2.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%2Ffvm925y995eb51lox0e2.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Why delete the default subnet? Because accepting defaults means Azure names and organises things on your behalf. When you create your own subnets from scratch, you control the naming convention, IP ranges, and how traffic is logically segmented. Clean slate = cleaner architecture.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 10 — Review and Create the Virtual Network&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click through to the Review + create tab. You'll see a green "Validation passed" banner at the top — Azure confirming everything looks good. Take a moment to review the summary:&lt;/p&gt;

&lt;p&gt;Subscription: Azure subscription 1 ✅&lt;br&gt;
Resource Group: Netaccess ✅&lt;br&gt;
Name: Netaccess-VM ✅&lt;br&gt;
Region: South Africa North ✅&lt;br&gt;
Security (Bastion, Firewall, DDoS): all Disabled ✅&lt;/p&gt;

&lt;p&gt;Hit the Create button at the bottom.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbbp18o2z0lgi7efks97g.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%2Fbbp18o2z0lgi7efks97g.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 11 — Deployment Complete — Click "Go to Resource"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Azure will now provision your virtual network. It usually takes just a minute or less. Once done, you'll land on the deployment overview showing "Your deployment is complete" with a green checkmark.&lt;br&gt;
Click Go to resource to open your newly created Netaccess-VM virtual network.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi07578ekvm5g13vqer3z.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%2Fi07578ekvm5g13vqer3z.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 12 — Explore the VNet Overview and Navigate to Subnets&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You're now inside the Netaccess-VM virtual network overview page. You can see all the essentials — resource group, location, address space (10.0.0.0/16), and notice it currently shows 0 subnets (because we deleted the default one earlier).&lt;br&gt;
&lt;em&gt;&lt;strong&gt;In the left sidebar under Settings, click on Subnets.&lt;/strong&gt;&lt;/em&gt; This is where we'll build all three of our custom subnets.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3t3f7vlyqxidbjyh3kep.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%2F3t3f7vlyqxidbjyh3kep.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 13 — Click "+ Subnet" to Start Adding Subnets&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once you're on the Subnets page, you'll see "No items found" — exactly as expected. Click the + Subnet button at the top of the page to open the "Add a subnet" panel on the right side.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx7dh1mpss08bfzbs5ixc.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%2Fx7dh1mpss08bfzbs5ixc.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 14 — Add Subnet 1 (Netaccess-subnet1)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The "Add a subnet" panel slides in from the right. Fill in the following:&lt;/p&gt;

&lt;p&gt;Subnet purpose — Default&lt;br&gt;
Name — Netaccess-subnet1&lt;br&gt;
IPv4 address range — 10.0.0.0/16 (this is auto-populated from your VNet address space)&lt;br&gt;
Starting address — 10.0.0.0&lt;br&gt;
Size — /24 (256 addresses)&lt;/p&gt;

&lt;p&gt;This creates a subnet range of 10.0.0.0 - 10.0.0.255. Leave IPv6 unchecked and scroll down to click Add.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpr7j5kn32anjacasfkb0.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%2Fpr7j5kn32anjacasfkb0.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 15 — Add Subnet 2 (Netaccess-subnet2)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click + Subnet again to add the second subnet. You'll notice the panel now shows the existing subnet1 listed on the left — that's just confirmation it was saved. Fill in the new details:&lt;/p&gt;

&lt;p&gt;Name — Netaccess-subnet2&lt;br&gt;
Starting address — 10.0.1.0&lt;br&gt;
Size — /24 (256 addresses)&lt;/p&gt;

&lt;p&gt;Subnet range: 10.0.1.0 - 10.0.1.255. Click Save.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpno3tel2g0g36fe213op.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%2Fpno3tel2g0g36fe213op.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice how we moved the third octet from .0.0 to .1.0 for the second subnet — that's how we keep each subnet in its own separate IP block within the same /16 space. No overlap, no conflict, no headaches so we do the same to create Netaccess-subnet3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 16 — All Three Subnets Created Successfully&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Repeat the same process one final time for the third subnet:&lt;/p&gt;

&lt;p&gt;Name — Netaccess-subnet3&lt;br&gt;
Starting address — 10.0.2.0&lt;br&gt;
Size — /24&lt;/p&gt;

&lt;p&gt;After saving, your Subnets page will now show all three subnets neatly listed with their IPv4 ranges highlighted:&lt;br&gt;
NameIPv4 RangeAvailable IPsNetaccess-subnet110.0.0.0/24251Netaccess-subnet210.0.1.0/24251Netaccess-subnet310.0.2.0/24251&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9mwalupb6xdkqo6o1ycc.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%2F9mwalupb6xdkqo6o1ycc.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each subnet has 251 usable IP addresses — When a resource is created, it uses one IP address from its subnet, and that IP is no longer available in the virtual network's address range. However, IP usage is managed at the subnet level, so a subnet can run out of addresses while the virtual network still has many unused IPs available in other subnets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cleanup — Deleting the Resources&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once I was done with the lab, I cleaned everything up to avoid unnecessary charges. Here's how:&lt;br&gt;
Delete the Netaccess Resource Group&lt;br&gt;
Go back to Resource groups, select Netaccess, click Delete resource group, type the name to confirm, and hit Delete.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0gozx30n2bfk9q1k86gz.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%2F0gozx30n2bfk9q1k86gz.png" alt=" " width="800" height="361"&gt;&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2Fubgvipa7gzrln8cszmh4.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%2Fubgvipa7gzrln8cszmh4.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delete the NetworkWatcherRG&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Azure automatically creates a NetworkWatcherRG resource group when you deploy networking resources. Delete this one too — same process: select it, click Delete resource group, type the name, confirm.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy41turp9hcn1y9hst16q.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%2Fy41turp9hcn1y9hst16q.png" alt=" " width="800" height="362"&gt;&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2Fgm67fswtldp47qtg5soo.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%2Fgm67fswtldp47qtg5soo.png" alt=" " width="799" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All Resource Groups Deleted&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once both are gone, your resource groups list is clean again — back to just the ones you had before (in my case, only Je-mega-soft remains).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5eu0jx82kaqiyup9390c.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%2F5eu0jx82kaqiyup9390c.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the full recap of what we did:&lt;/p&gt;

&lt;p&gt;✅ Searched for and opened Resource Groups&lt;br&gt;
✅ Created the Netaccess resource group in South Africa North&lt;br&gt;
✅ Searched for and navigated to Virtual Networks&lt;br&gt;
✅ Filled in VNet Basics — name Netaccess-VM, resource group Netaccess&lt;br&gt;
✅ On the Address space tab, deleted the auto-generated default subnet&lt;br&gt;
✅ Reviewed and created the Virtual Network (validation passed)&lt;br&gt;
✅ Deployment completed — clicked Go to resource&lt;br&gt;
✅ Navigated to the Subnets section inside the VNet&lt;br&gt;
✅ Clicked + Subnet to start adding custom subnets&lt;br&gt;
✅ Created Netaccess-subnet1 → 10.0.0.0/24&lt;br&gt;
✅ Created Netaccess-subnet2 → 10.0.1.0/24&lt;br&gt;
✅ Created Netaccess-subnet3 → 10.0.2.0/24&lt;br&gt;
✅ Cleaned up both Netaccess and NetworkWatcherRG resource groups&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why This Matters&lt;/strong&gt;&lt;br&gt;
Understanding how to manually structure your subnets gives you real control over how traffic flows in your cloud environment. Instead of accepting defaults, you're deliberately choosing IP ranges — which becomes crucial when you start doing things like:&lt;/p&gt;

&lt;p&gt;Separating frontend and backend application tiers&lt;br&gt;
Connecting on-premises networks via VPN Gateway or ExpressRoute&lt;br&gt;
Setting up Network Security Groups (NSGs) with rules per subnet&lt;br&gt;
Deploying services that require dedicated subnets (like Azure Bastion or App Gateway)&lt;/p&gt;

&lt;p&gt;This is foundational Azure networking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If this helped you, drop a ❤️ or leave a comment — especially if you ran into something different in your own setup. I'd love to hear about it.&lt;br&gt;
Until next time — keep building! 🚀&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>cloud</category>
      <category>devops</category>
      <category>networking</category>
    </item>
    <item>
      <title>How I Hosted a Free Website on Azure Blob Storage (No Server Needed!)</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Wed, 03 Jun 2026 07:33:07 +0000</pubDate>
      <link>https://dev.to/4thman/how-i-hosted-a-free-website-on-azure-blob-storage-no-server-needed-188</link>
      <guid>https://dev.to/4thman/how-i-hosted-a-free-website-on-azure-blob-storage-no-server-needed-188</guid>
      <description>&lt;p&gt;Introduction&lt;/p&gt;

&lt;p&gt;So I wanted to host a website without paying for a web server or dealing with complex configurations. Turns out, Azure Blob Storage has a brilliant feature called Static Website Hosting that lets you serve HTML, CSS, and JavaScript files directly from a storage container — kind of like having a mini web host built into your cloud storage.&lt;br&gt;
In this post, I'll walk you through exactly what I did, step by step, with screenshots. The website template I used is called Nexus Brew — a cyberpunk-themed cafe website from Tooplate.com, which offers free HTML templates.&lt;br&gt;
By the end of this guide, your site will be live at a URL like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jewebs.blob.core.windows.net/$web/index.html" rel="noopener noreferrer"&gt;https://jewebs.blob.core.windows.net/$web/index.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's get into it!&lt;/p&gt;

&lt;p&gt;Step 1 — Download the Nexus Brew Template from Tooplate&lt;br&gt;
Before jumping into Azure, the first thing I did was grab the website template.&lt;br&gt;
I went to tooplate.com — a site that offers clean, modern, free HTML templates built with Bootstrap and CSS. They have over 80 templates for startups, small businesses, and portfolios.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9oaul8dvl3lpde1dbvy0.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%2F9oaul8dvl3lpde1dbvy0.png" alt=" " width="799" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I searched for and found the Nexus Brew template.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwo95sy46nf0hytyl7slu.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%2Fwo95sy46nf0hytyl7slu.png" alt=" " width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's a dark, cyberpunk-style cafe website template with features like:&lt;/p&gt;

&lt;p&gt;A hero banner section&lt;br&gt;
A tabbed menu showcase&lt;br&gt;
An about section with content and images&lt;br&gt;
A contact form&lt;br&gt;
Social media links&lt;br&gt;
Smooth scroll navigation and hover effects&lt;/p&gt;

&lt;p&gt;It looks super cool — very futuristic and modern. Perfect for learning!&lt;br&gt;
I clicked the Download button on the template page to grab the zip file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbf2l6t3knaps0uii1c3g.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%2Fbf2l6t3knaps0uii1c3g.png" alt=" " width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The downloaded file was named 2146_nexus_brew.zip and it landed in my Downloads folder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7iqfc0x73sypl5lw9a74.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%2F7iqfc0x73sypl5lw9a74.png" alt=" " width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 2 — Extract the Zip File&lt;br&gt;
After downloading, &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhool87a0qgc4xtub3d19.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%2Fhool87a0qgc4xtub3d19.png" alt=" " width="799" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I extracted the zip file. Inside the 2146_nexus_brew folder, you'll find these files:&lt;/p&gt;

&lt;p&gt;index.html — the main HTML page&lt;br&gt;
tooplate-nexus-brew.css — the stylesheet&lt;br&gt;
tooplate-nexus-scripts.js — the JavaScript file&lt;br&gt;
ABOUT THIS TEMPLATE.txt — info about the template&lt;br&gt;
images/ — a folder containing all the images used on the site&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F63945xkf8uy1nhqbnpml.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%2F63945xkf8uy1nhqbnpml.png" alt=" " width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are all the files I'll need to upload to Azure later.&lt;/p&gt;

&lt;p&gt;Step 3 — Find Resource Groups in the Azure Portal&lt;br&gt;
Now it's time to head over to portal.azure.com.&lt;br&gt;
Once logged in, &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flpdk03oaelvhox71cxx2.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%2Flpdk03oaelvhox71cxx2.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I used the search bar at the top and typed "resource group". In the dropdown that appeared, I clicked on Resource groups (the one listed under Services).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkhkhagnfmrmrxs8dd2tp.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%2Fkhkhagnfmrmrxs8dd2tp.png" alt=" " width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What is a Resource Group?&lt;br&gt;
Think of a Resource Group as a folder in Azure where you keep related things together. It helps you manage, organize, and delete resources (like storage accounts) as a unit.&lt;/p&gt;

&lt;p&gt;Step 4 — View the Existing Resource Group Created in the Last Blog&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3onm6haapiqbvy4rvedd.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%2F3onm6haapiqbvy4rvedd.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After clicking Resource groups, I could see my existing resource group named Je-mega-soft which was created in our last blog, located in South Africa North under Azure subscription 1.&lt;br&gt;
I clicked on it to open it.&lt;/p&gt;

&lt;p&gt;Step 5 — Search for Storage Accounts&lt;br&gt;
Inside the Resource Manager section, I used the search bar again and typed "storage account". From the suggestions, I clicked on Storage accounts (the one listed under Services — not the classic version).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fok5ejeq5n7vtmvs0g41s.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%2Fok5ejeq5n7vtmvs0g41s.png" alt=" " width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This took me to the Storage center where I could see existing storage accounts and create new ones.&lt;/p&gt;

&lt;p&gt;Step 6 — Create a New Storage Account&lt;br&gt;
On the Storage center page, I clicked the + Create button at the top to start creating a new storage account.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff8x69t6kqeoeitvfkrut.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%2Ff8x69t6kqeoeitvfkrut.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the Basics tab of the creation form, I filled in these details:&lt;/p&gt;

&lt;p&gt;Resource group: Je-mega-soft (selected from the dropdown)&lt;br&gt;
Storage account name: jewebs (this has to be globally unique — all lowercase, no special characters)&lt;br&gt;
Region: (Africa) South Africa North (I chose the region closest to me)&lt;br&gt;
Preferred storage type: Azure Blob Storage or Azure Data Lake Storage&lt;br&gt;
Performance: Standard (recommended for most use cases)&lt;br&gt;
Redundancy: Locally Redundant Storage (LRS) (the most affordable option — stores 3 copies of your data within one data center)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0wdjw1nizuxnrcj8n12j.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%2F0wdjw1nizuxnrcj8n12j.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why LRS? For a static website that's just for learning or low-traffic use, LRS is more than enough and the cheapest option. &lt;/p&gt;

&lt;p&gt;Step 7 — Enable Anonymous Access in the Security Tab&lt;br&gt;
After filling in the Basics, I clicked Next to navigate through the tabs until I reached the Security tab.&lt;br&gt;
Here, the most important setting to enable is:&lt;br&gt;
✅ Allow enabling anonymous access on individual containers&lt;br&gt;
I made sure this checkbox was ticked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk266z1ivbyo2u6vqpk4y.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%2Fk266z1ivbyo2u6vqpk4y.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The red arrow in my screenshot points directly to this option — don't miss it!&lt;/p&gt;

&lt;p&gt;Why is this important? Without enabling anonymous access, your static website files would be private by default. No one on the internet would be able to open your website. Enabling this allows the $web container (which Azure creates for static websites) to serve files publicly.&lt;/p&gt;

&lt;p&gt;Once done, I clicked Review + create, reviewed the settings, and clicked Create.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo8vue51we30iws35yd34.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%2Fo8vue51we30iws35yd34.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 8 — Deployment Complete! Go to Resource&lt;br&gt;
After a few seconds, the deployment finished and I saw the "Your deployment is complete" message.&lt;br&gt;
The notification bell at the top right also showed a green checkmark saying "Deployment succeeded" — the deployment was to resource group Je-mega-soft.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy2ok07guzoeccsw49jr6.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%2Fy2ok07guzoeccsw49jr6.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I clicked the Go to resource button (there's one in the main panel and one in the notification panel — either works) to open the newly created storage account.&lt;/p&gt;

&lt;p&gt;Step 9 — Navigate to Static Website Settings&lt;br&gt;
Now I was inside the jewebs storage account dashboard.&lt;br&gt;
On the left sidebar, I scrolled down and expanded the Data management section. Inside it, I clicked on Static website.&lt;/p&gt;

&lt;p&gt;If you don't see Data management, look for it in the left panel — it's usually collapsed. The red arrow in my screenshot points directly to it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvetja4ofyalnp2ef5sfh.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%2Fvetja4ofyalnp2ef5sfh.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 10 — Enable the Static Website Feature&lt;br&gt;
On the Static website page, by default the feature is set to Disabled.&lt;br&gt;
I clicked on Enabled to turn it on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fatqkpysovwuekwubx8dm.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%2Fatqkpysovwuekwubx8dm.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As soon as I enabled it, two input fields appeared:&lt;/p&gt;

&lt;p&gt;Index document name: nexus_brew.html&lt;br&gt;
Error document path: ERROR-404.html&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flgycikyhx0gbbjw9kjns.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%2Flgycikyhx0gbbjw9kjns.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the page that shows when someone visits a URL that doesn't exist. You can create a simple 404 page or just use the default.&lt;/p&gt;

&lt;p&gt;After filling these in, I clicked Save.&lt;/p&gt;

&lt;p&gt;Step 11 — Settings Saved Successfully&lt;br&gt;
A green notification appeared in the top right corner saying "Successfully updated static website settings".&lt;br&gt;
On the same page, I could now see:&lt;/p&gt;

&lt;p&gt;The static website is Enabled&lt;br&gt;
A message saying: "An Azure Storage container has been created to host your static website."&lt;br&gt;
A link to the container: $web&lt;br&gt;
The Primary endpoint URL: &lt;a href="https://jewebs.z1.web.core.windows.net/" rel="noopener noreferrer"&gt;https://jewebs.z1.web.core.windows.net/&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2Fbgob76pa9qnuomvfa3al.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%2Fbgob76pa9qnuomvfa3al.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What is $web? This is a special Azure container that's automatically created when you enable static website hosting. It's where you'll upload all your website files.&lt;/p&gt;

&lt;p&gt;I clicked on $web to open that container.&lt;/p&gt;

&lt;p&gt;Step 12 — Upload Files to the $web Container&lt;br&gt;
Inside the $web container, it was empty — showing "No items found".&lt;br&gt;
I clicked the Upload button at the top to open the Upload blob panel on the right side.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmd1zvn3pbhg336kd9xk6.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%2Fmd1zvn3pbhg336kd9xk6.png" alt=" " width="799" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I then selected all the files from the extracted Nexus Brew folder:&lt;/p&gt;

&lt;p&gt;index.html&lt;br&gt;
tooplate-nexus-brew.css&lt;br&gt;
tooplate-nexus-scripts.js&lt;br&gt;
ABOUT THIS TEMPLATE.txt&lt;br&gt;
The images folder&lt;/p&gt;

&lt;p&gt;The upload panel showed "7 file(s) selected" (the images folder contains multiple image files, so the total count goes up).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzx7yk6j6i55gz02w7pce.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%2Fzx7yk6j6i55gz02w7pce.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I clicked the Upload button in the panel to start the upload.&lt;/p&gt;

&lt;p&gt;Important tip: Make sure to upload the contents of the folder, not the folder itself. Azure Blob Storage needs the files at the root level of $web for the static website to work correctly. If you upload a folder as a subfolder, your index page won't load properly.&lt;/p&gt;

&lt;p&gt;Step 13 — Files Successfully Uploaded&lt;br&gt;
After the upload completed, the $web container now showed all my files:&lt;br&gt;
NameTypeSizeimagesFolder—ABOUT THIS TEMPLATE.txtBlock blob557 Bindex.htmlBlock blob16.8 KiBtooplate-nexus-brew.cssBlock blob24.78 KiBtooplate-nexus-scripts.jsBlock blob3.52 KiB&lt;br&gt;
All 5 items were now visible and available in the container — uploaded on 6/2/2026 at 10:38 PM.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx529ia06dk6tmty6mot5.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%2Fx529ia06dk6tmty6mot5.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 14 — Copy the URL of the Index File&lt;br&gt;
To get the direct URL of the uploaded index.html file, I:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffrubzql0xzz50s5cesza.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%2Ffrubzql0xzz50s5cesza.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clicked the three-dot menu (...) next to index.html&lt;br&gt;
Selected Copy URL from the context menu&lt;/p&gt;

&lt;p&gt;The URL I got was:&lt;br&gt;
&lt;a href="https://jewebs.blob.core.windows.net/$web/index.html" rel="noopener noreferrer"&gt;https://jewebs.blob.core.windows.net/$web/index.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note: There are actually two URLs you can use:&lt;/p&gt;

&lt;p&gt;Blob URL (direct file): &lt;a href="https://jewebs.blob.core.windows.net/$web/index.html" rel="noopener noreferrer"&gt;https://jewebs.blob.core.windows.net/$web/index.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Static website URL (cleaner): &lt;a href="https://jewebs.z1.web.core.windows.net/" rel="noopener noreferrer"&gt;https://jewebs.z1.web.core.windows.net/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Website Is Live! 🎉&lt;/p&gt;

&lt;p&gt;That's it! The Nexus Brew template is now hosted live on Azure Blob Storage as a static website.&lt;br&gt;
You can visit it at:&lt;br&gt;
👉 &lt;a href="https://jewebs.blob.core.windows.net/$web/index.html" rel="noopener noreferrer"&gt;https://jewebs.blob.core.windows.net/$web/index.html&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2F7f0kr6g51hytz5hgpya5.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%2F7f0kr6g51hytz5hgpya5.png" alt=" " width="800" height="407"&gt;&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2Fsoqwtum6shyrtstktf8a.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%2Fsoqwtum6shyrtstktf8a.png" alt=" " width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;           Quick Summary of All Steps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Here's a quick recap of everything we did:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Downloaded the Nexus Brew free HTML template from tooplate.com&lt;/li&gt;
&lt;li&gt;Extracted the zip file and noted the files inside&lt;/li&gt;
&lt;li&gt;Open the Azure Portal and search for Resource groups in the Azure portal&lt;/li&gt;
&lt;li&gt;Opened the existing Je-mega-soft resource group which was previously showed on our last blog how to create.&lt;/li&gt;
&lt;li&gt;Searched for Storage accounts and clicked Create&lt;/li&gt;
&lt;li&gt;Configured the storage account with name jewebs, region South Africa North, Standard performance, and LRS redundancy&lt;/li&gt;
&lt;li&gt;In the Security tab, enabled Allow anonymous access on containers&lt;/li&gt;
&lt;li&gt;Deployed and clicked Go to resource&lt;/li&gt;
&lt;li&gt;Navigated to Data management → Static website&lt;/li&gt;
&lt;li&gt;Enabled static website hosting and set the index document name&lt;/li&gt;
&lt;li&gt;Saved settings and noted the $web container and primary endpoint URL&lt;/li&gt;
&lt;li&gt;Opened the $web container and uploaded all website files&lt;/li&gt;
&lt;li&gt;Confirmed all files were uploaded successfully&lt;/li&gt;
&lt;li&gt;Copied the URL and opened the live websit
e&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why I Loved This Approach&lt;/p&gt;

&lt;p&gt;Honestly, this whole setup took less than 15 minutes and didn't require me to set up a web server, configure Nginx or Apache, deal with SSL certificates manually, or pay for a VM. Azure handles everything for you and even gives you HTTPS for free through the static website endpoint.&lt;br&gt;
For simple websites — portfolios, landing pages, documentation sites, or just practice — this is one of the easiest and cheapest ways to get something live on the internet.&lt;/p&gt;

&lt;p&gt;Resources Used:&lt;/p&gt;

&lt;p&gt;Tooplate.com — Free HTML templates&lt;br&gt;
Azure Static Website Hosting Docs&lt;br&gt;
Azure Account.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Step-by-Step Guide to Azure Blob Storage: Uploading Files, Enabling Public Access, and Secure SAS Link Generation</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Thu, 28 May 2026 22:09:47 +0000</pubDate>
      <link>https://dev.to/4thman/step-by-step-guide-to-azure-blob-storage-uploading-files-enabling-public-access-and-secure-sas-21lm</link>
      <guid>https://dev.to/4thman/step-by-step-guide-to-azure-blob-storage-uploading-files-enabling-public-access-and-secure-sas-21lm</guid>
      <description>&lt;p&gt;In this article, I will walk through how I created an Azure Storage account, configured Blob Storage, uploaded a file, made it publicly accessible, and finally generated a Shared Access Signature (SAS) link for secure access.&lt;/p&gt;

&lt;p&gt;This is part of my hands-on learning journey in Cloud Computing and DevOps using Microsoft Azure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creating the Azure Storage Account&lt;/strong&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.amazonaws.com%2Fuploads%2Farticles%2F292c1x73zol1n02flu2f.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%2F292c1x73zol1n02flu2f.png" alt=" " width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first step was to create a Storage Account in Microsoft Azure. This is the core service that provides a scalable location for storing files such as images, videos, documents, and backups.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv3rqaa8u9tfctb2tztmk.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%2Fv3rqaa8u9tfctb2tztmk.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Configuration used:&lt;br&gt;
Resource Group: je-mega-soft&lt;br&gt;
Storage Account Name: prodjemegastorage1&lt;br&gt;
Region: Africa (South Africa North)&lt;br&gt;
Primary Service: Azure Blob Storage&lt;br&gt;
Performance: Standard&lt;br&gt;
Redundancy: Locally Redundant Storage (LRS)&lt;br&gt;
Access Tier: Hot&lt;br&gt;
Allow Blob Public Access: Enabled (on container level)&lt;br&gt;
Explanation of each choice:&lt;/p&gt;

&lt;p&gt;Resource Group (je-mega-soft):&lt;br&gt;
A resource group is simply a logical container in Azure that holds related resources. I used this to group everything related to my project for easy management and cleanup.&lt;/p&gt;

&lt;p&gt;Storage Account Name (prodjemegastorage1):&lt;br&gt;
This must be globally unique across Azure. I used a structured name to represent a production-style storage setup.&lt;/p&gt;

&lt;p&gt;Region (Africa – South Africa North):&lt;br&gt;
I selected this region because it is geographically closer to my location, which reduces latency and improves performance when accessing resources.&lt;/p&gt;

&lt;p&gt;Primary Service (Blob Storage):&lt;br&gt;
Blob Storage is designed for storing unstructured data like images, videos, logs, and backups. It is the best choice for this type of project.&lt;/p&gt;

&lt;p&gt;Performance (Standard):&lt;br&gt;
Standard performance was chosen because this is a learning/demo project. It is cost-effective and suitable for general-purpose storage. Premium would only be needed for high-speed enterprise workloads.&lt;/p&gt;

&lt;p&gt;Redundancy (Locally Redundant Storage - LRS):&lt;br&gt;
LRS stores data in a single data center but keeps multiple copies within that facility. I chose this because it is the cheapest option and perfect for testing and learning purposes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Finvo9pynj3bcm2fk23ah.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%2Finvo9pynj3bcm2fk23ah.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Access Tier (Hot):&lt;br&gt;
The Hot tier is used for data that is accessed frequently. Since I planned to upload and access the file during testing and sharing, this was the best fit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdxaampdlyienp7lritnw.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%2Fdxaampdlyienp7lritnw.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Public Access Enabled:&lt;br&gt;
I enabled public access at the container level because I wanted the uploaded file to be accessible via a URL without authentication.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F09ooc2xjtlwa1bju3eu3.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%2F09ooc2xjtlwa1bju3eu3.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After reviewing all settings, I clicked Create and waited for deployment to complete.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbzmnpeal1ahvqkoj6c8o.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%2Fbzmnpeal1ahvqkoj6c8o.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creating a Blob Container&lt;/strong&gt;&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%2Fzh83dg3u7smz2lq1ofze.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%2Fzh83dg3u7smz2lq1ofze.png" alt=" " width="800" height="362"&gt;&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2F7kokhduusc70nyfymppj.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%2F7kokhduusc70nyfymppj.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the storage account was successfully created, I moved to the next step which is creating a container.&lt;/p&gt;

&lt;p&gt;Path:&lt;br&gt;
Storage Account → Data Storage → Containers → + Add Container&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4gzu7t4q2ee7027f4unl.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%2F4gzu7t4q2ee7027f4unl.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Container configuration:&lt;br&gt;
Name: jemega-img-vid&lt;br&gt;
Public Access Level: Container (anonymous read access for containers and blobs)&lt;br&gt;
Why I used this container name:&lt;/p&gt;

&lt;p&gt;I chose jemega-img-vid because:&lt;/p&gt;

&lt;p&gt;“jemega” represents my personal/project brand name&lt;br&gt;
“img-vid” clearly indicates the container is meant for images and videos&lt;br&gt;
It is short, readable, and meaningful for future scaling&lt;br&gt;
Public Access Level explanation:&lt;/p&gt;

&lt;p&gt;Setting the access level to Container means:&lt;/p&gt;

&lt;p&gt;Files inside the container can be publicly accessed&lt;br&gt;
Anyone with the link can view blobs without authentication&lt;br&gt;
This is useful for demos, static file hosting, and learning scenarios&lt;br&gt;
About the $logs container:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqp4sw7xu37kylm5zfp0g.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%2Fqp4sw7xu37kylm5zfp0g.png" alt=" " width="799" height="358"&gt;&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2Fqxhg1zuzdlltdjhmb3ii.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%2Fqxhg1zuzdlltdjhmb3ii.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;During setup, Azure automatically created a container called $logs.&lt;/p&gt;

&lt;p&gt;This container is used for system-generated logs such as:&lt;/p&gt;

&lt;p&gt;Storage analytics logs&lt;br&gt;
Request tracking logs&lt;br&gt;
Diagnostic monitoring data&lt;/p&gt;

&lt;p&gt;It is not meant for user files, so I did not modify or use it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uploading the File to Blob Storage&lt;/strong&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.amazonaws.com%2Fuploads%2Farticles%2F8o550f0qqwk6ho6bygma.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%2F8o550f0qqwk6ho6bygma.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, I uploaded a file into the container.&lt;/p&gt;

&lt;p&gt;File uploaded:&lt;br&gt;
just me.png&lt;/p&gt;

&lt;p&gt;Steps:&lt;/p&gt;

&lt;p&gt;Opened the container (jemega-img-vid)&lt;br&gt;
Clicked on Upload&lt;br&gt;
Selected the file from my local system&lt;br&gt;
Clicked Upload to store it in Azure Blob Storage&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2mztiaqqh2zq3gujounx.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%2F2mztiaqqh2zq3gujounx.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once uploaded, Azure automatically generated a unique URL for the file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuy5qck2ho69oaqr3l9h2.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%2Fuy5qck2ho69oaqr3l9h2.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Blob URL generated:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://prodjemegastorage1.blob.core.windows.net/jemega-img-vid/just%20me.png" rel="noopener noreferrer"&gt;https://prodjemegastorage1.blob.core.windows.net/jemega-img-vid/just%20me.png&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Explanation:&lt;/p&gt;

&lt;p&gt;This URL directly points to the file inside the container. Because public access was enabled, the file can be accessed in a browser without authentication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generating a Shared Access Signature (SAS)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To improve security while still allowing temporary access, I generated a Shared Access Signature (SAS).&lt;/p&gt;

&lt;p&gt;What is SAS?&lt;/p&gt;

&lt;p&gt;A SAS token is a secure way to grant limited access to storage resources without exposing your account keys. It allows you to define:&lt;/p&gt;

&lt;p&gt;What can be accessed&lt;br&gt;
How long access is valid&lt;br&gt;
What permissions are allowed&lt;br&gt;
Why I used Key 1:&lt;/p&gt;

&lt;p&gt;I used Key 1 from the storage account because it is one of the primary access keys used to generate SAS tokens. These keys should be protected because they grant full control over the storage account.&lt;/p&gt;

&lt;p&gt;SAS configuration used:&lt;br&gt;
Start Time: Today at 10:30 PM&lt;br&gt;
Expiry Time: 31st of this month at 12:03 PM&lt;br&gt;
Permissions: Read (sp=r)&lt;br&gt;
Protocol: HTTPS only&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frn83pekint3jg5ejxhxj.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%2Frn83pekint3jg5ejxhxj.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why SAS is important:&lt;/p&gt;

&lt;p&gt;SAS provides:&lt;/p&gt;

&lt;p&gt;Temporary access instead of permanent public exposure&lt;br&gt;
Better security control over shared files&lt;br&gt;
Ability to revoke access by changing keys or expiry&lt;br&gt;
Fine-grained permissions (read, write, delete, etc.)&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Final SAS URL (Secure Access Link)&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
Here is the final link generated using SAS:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://prodjemegastorage1.blob.core.windows.net/jemega-img-vid/just%20me.png?sp=r&amp;amp;st=2026-05-28T21:12:08Z&amp;amp;se=2026-05-31T11:03:08Z&amp;amp;spr=https&amp;amp;sv=2026-02-06&amp;amp;sr=b&amp;amp;sig=SseErml92AKi5t60kVuYCN8DNP%2BBkKm4CaXtHwhTqwo%3D" rel="noopener noreferrer"&gt;https://prodjemegastorage1.blob.core.windows.net/jemega-img-vid/just%20me.png?sp=r&amp;amp;st=2026-05-28T21:12:08Z&amp;amp;se=2026-05-31T11:03:08Z&amp;amp;spr=https&amp;amp;sv=2026-02-06&amp;amp;sr=b&amp;amp;sig=SseErml92AKi5t60kVuYCN8DNP%2BBkKm4CaXtHwhTqwo%3D&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This walkthrough helped me understand the basics of Azure Blob Storage, from creating a storage account and container to uploading a file and generating a SAS link for controlled access. I also learned how key settings like redundancy, access tier, and public access affect cost, performance, and security. Overall, it made cloud storage concepts clearer and gave me a solid foundation for working with Azure storage services going forward.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cloud</category>
      <category>cloudcomputing</category>
      <category>devops</category>
    </item>
    <item>
      <title>I Created an Azure User, Gave It Global Admin Access, Revoked It, and Tested Azure IAM — Here’s What I Learned</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Fri, 22 May 2026 01:20:29 +0000</pubDate>
      <link>https://dev.to/4thman/i-created-an-azure-user-gave-it-global-admin-access-revoked-it-and-tested-azure-iam-heres-3p6p</link>
      <guid>https://dev.to/4thman/i-created-an-azure-user-gave-it-global-admin-access-revoked-it-and-tested-azure-iam-heres-3p6p</guid>
      <description>&lt;p&gt;As part of my Cloud Engineering and DevOps learning journey, I recently decided to go beyond theory and start practicing Identity and Access Management (IAM) directly inside Microsoft Azure.&lt;/p&gt;

&lt;p&gt;I wanted to understand how organizations manage users, permissions, administrative access, and security in real cloud environments. Instead of just reading about Microsoft Entra ID and RBAC, I performed a complete hands-on lab from start to finish.&lt;/p&gt;

&lt;p&gt;In this lab, I:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;created a new Azure user&lt;/li&gt;
&lt;li&gt;configured Microsoft Authenticator&lt;/li&gt;
&lt;li&gt;assigned administrative roles&lt;/li&gt;
&lt;li&gt;logged into the new account using an incognito browser&lt;/li&gt;
&lt;li&gt;created another user using delegated access&lt;/li&gt;
&lt;li&gt;revoked the permissions&lt;/li&gt;
&lt;li&gt;tested whether the account could still perform administrative actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This exercise gave me a much deeper understanding of how cloud identity management works in enterprise environments.&lt;/p&gt;

&lt;p&gt;The screenshots attached throughout this post document the entire process step by step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting From the Default Directory
&lt;/h2&gt;

&lt;p&gt;I began by logging into the Azure Portal using my main administrator account inside the default directory.&lt;/p&gt;

&lt;p&gt;From there, I navigated to:&lt;/p&gt;

&lt;p&gt;Microsoft Entra ID → Users → All Users&lt;/p&gt;

&lt;p&gt;This section contains every identity managed inside the Azure environment, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;users&lt;/li&gt;
&lt;li&gt;groups&lt;/li&gt;
&lt;li&gt;applications&lt;/li&gt;
&lt;li&gt;permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, I already had administrative privileges, which allowed me to create and manage other users.&lt;/p&gt;

&lt;p&gt;One thing I’m beginning to understand about cloud platforms is that structure and organization are extremely important. Without proper identity management, large organizations would struggle to control access securely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the “Immortel” User
&lt;/h2&gt;

&lt;p&gt;The next step was creating a new user account named “Immortel.”&lt;/p&gt;

&lt;p&gt;During the setup process, I configured:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;display name&lt;/li&gt;
&lt;li&gt;username&lt;/li&gt;
&lt;li&gt;password settings&lt;/li&gt;
&lt;li&gt;account information&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After creating the account successfully, the new user appeared immediately inside the Azure users list.&lt;/p&gt;

&lt;p&gt;This helped me understand how organizations onboard employees into Microsoft Entra ID environments.&lt;/p&gt;

&lt;p&gt;At this stage, the account existed but had limited permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging Into the Immortel Account Using Incognito Mode
&lt;/h2&gt;

&lt;p&gt;Instead of signing out from my main administrator account, I opened a new incognito browser window and logged into the Immortel account separately.&lt;/p&gt;

&lt;p&gt;This allowed me to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;remain logged into my administrator account&lt;/li&gt;
&lt;li&gt;test the Immortel account independently&lt;/li&gt;
&lt;li&gt;simulate how multiple administrators may work simultaneously&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using incognito mode was actually very useful because it helped me manage both accounts side by side without interrupting my existing Azure session.&lt;/p&gt;

&lt;p&gt;At this stage, the Immortel account still had very limited access because no elevated administrative roles had been assigned yet.&lt;/p&gt;

&lt;p&gt;This demonstrated an important cloud security principle known as:&lt;/p&gt;

&lt;p&gt;Least Privilege Access&lt;/p&gt;

&lt;p&gt;This means users should only receive the minimum permissions necessary for their tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Microsoft Authenticator (MFA)
&lt;/h2&gt;

&lt;p&gt;While logging into the Immortel account, Azure required Multi-Factor Authentication (MFA) setup using Microsoft Authenticator.&lt;/p&gt;

&lt;p&gt;This process involved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scanning a QR code&lt;/li&gt;
&lt;li&gt;linking the account to the Authenticator app&lt;/li&gt;
&lt;li&gt;approving sign-in requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was one of the most practical parts of the exercise because it showed me how organizations secure user accounts beyond just passwords.&lt;/p&gt;

&lt;p&gt;I now understand why MFA is considered one of the most important security practices in cloud computing. Even if a password gets compromised, unauthorized users still cannot access the account without the additional verification step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assigning Global Administrator and Global Reader Roles
&lt;/h2&gt;

&lt;p&gt;After setting up the Immortel account successfully, I returned to my original administrator session inside the default directory and assigned two important roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global Administrator&lt;/li&gt;
&lt;li&gt;Global Reader&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This introduced me to Azure Role-Based Access Control (RBAC).&lt;/p&gt;

&lt;p&gt;Before this exercise, RBAC sounded theoretical while reading documentation. But seeing it work practically made everything much clearer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Global Administrator Role
&lt;/h2&gt;

&lt;p&gt;Global Administrator is one of the highest privileged roles inside Microsoft Entra ID.&lt;/p&gt;

&lt;p&gt;A user with this role can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create and manage users&lt;/li&gt;
&lt;li&gt;assign permissions&lt;/li&gt;
&lt;li&gt;manage identity settings&lt;/li&gt;
&lt;li&gt;configure security&lt;/li&gt;
&lt;li&gt;reset passwords&lt;/li&gt;
&lt;li&gt;control administrative settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In simple terms, this role has broad control over the Azure identity environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Global Reader Role
&lt;/h2&gt;

&lt;p&gt;Global Reader works differently.&lt;/p&gt;

&lt;p&gt;Users with this role can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;view configurations&lt;/li&gt;
&lt;li&gt;monitor environments&lt;/li&gt;
&lt;li&gt;review settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;but they cannot make administrative changes.&lt;/p&gt;

&lt;p&gt;This role is useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;auditors&lt;/li&gt;
&lt;li&gt;compliance teams&lt;/li&gt;
&lt;li&gt;monitoring personnel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;who need visibility without modification access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging Back Into the Immortel Session
&lt;/h2&gt;

&lt;p&gt;After assigning the roles, I refreshed and continued working inside the incognito browser session already logged into the Immortel account.&lt;/p&gt;

&lt;p&gt;The difference was immediately noticeable.&lt;/p&gt;

&lt;p&gt;The account now had elevated privileges and could access administrative capabilities that were unavailable earlier.&lt;/p&gt;

&lt;p&gt;This demonstrated how permissions in Azure are controlled dynamically through assigned roles.&lt;/p&gt;

&lt;p&gt;It also showed me how powerful RBAC is in enterprise cloud environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating Another User Named “Dare”
&lt;/h2&gt;

&lt;p&gt;To confirm the permissions were working correctly, I used the Immortel account to create another user named “Dare.”&lt;/p&gt;

&lt;p&gt;The creation process was successful.&lt;/p&gt;

&lt;p&gt;This confirmed that the Global Administrator privileges assigned earlier were active and functioning properly.&lt;/p&gt;

&lt;p&gt;At this point, I fully understood how delegated administration works in Azure environments.&lt;/p&gt;

&lt;p&gt;Organizations can temporarily assign elevated permissions to specific users without permanently granting full administrative control to everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revoking the Administrative Access
&lt;/h2&gt;

&lt;p&gt;After completing the delegated administration test, I returned to my original administrator session inside the default directory.&lt;/p&gt;

&lt;p&gt;I then removed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global Administrator access&lt;/li&gt;
&lt;li&gt;Global Reader access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;from the Immortel account.&lt;/p&gt;

&lt;p&gt;This part of the exercise taught me another important IAM concept:&lt;/p&gt;

&lt;p&gt;Permissions can be granted and revoked at any time.&lt;/p&gt;

&lt;p&gt;This is critical in real-world environments for situations such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;employee resignation&lt;/li&gt;
&lt;li&gt;temporary contractors&lt;/li&gt;
&lt;li&gt;security incidents&lt;/li&gt;
&lt;li&gt;role changes&lt;/li&gt;
&lt;li&gt;access reviews&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing the Revoked Account
&lt;/h2&gt;

&lt;p&gt;To confirm the permissions were truly removed, I returned to the incognito browser session using the Immortel account and attempted to create another user named “John.”&lt;/p&gt;

&lt;p&gt;This time, the operation failed.&lt;/p&gt;

&lt;p&gt;The account no longer had sufficient privileges to perform administrative tasks.&lt;/p&gt;

&lt;p&gt;This final test clearly demonstrated how Azure immediately enforces permission changes once access is revoked.&lt;/p&gt;

&lt;p&gt;It was satisfying seeing the entire IAM workflow function exactly as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Lab Taught Me
&lt;/h2&gt;

&lt;p&gt;This hands-on exercise helped me better understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Microsoft Entra ID&lt;/li&gt;
&lt;li&gt;Identity and Access Management (IAM)&lt;/li&gt;
&lt;li&gt;RBAC&lt;/li&gt;
&lt;li&gt;Multi-Factor Authentication (MFA)&lt;/li&gt;
&lt;li&gt;delegated administration&lt;/li&gt;
&lt;li&gt;access control&lt;/li&gt;
&lt;li&gt;permission revocation&lt;/li&gt;
&lt;li&gt;cloud security principles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More importantly, it helped me realize that identity management is one of the most critical aspects of cloud security.&lt;/p&gt;

&lt;p&gt;Without proper IAM controls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unauthorized access becomes easier&lt;/li&gt;
&lt;li&gt;security risks increase&lt;/li&gt;
&lt;li&gt;governance becomes difficult&lt;/li&gt;
&lt;li&gt;compliance becomes harder to maintain&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This was one of the most valuable Azure labs I’ve completed so far because it moved beyond theory into practical identity administration and security management.&lt;/p&gt;

&lt;p&gt;As I continue learning Cloud Engineering and DevOps, I’m beginning to appreciate how important hands-on practice is. Reading documentation is useful, but actually performing these tasks inside a real cloud environment creates a completely different level of understanding.&lt;/p&gt;

&lt;p&gt;I still have a long way to go, but each lab continues to build my confidence and understanding step by step.&lt;/p&gt;

&lt;p&gt;I’ll continue documenting my learning journey, projects, and hands-on experiences as I grow in cloud engineering and DevOps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0354s6du370j94aoqq4s.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%2F0354s6du370j94aoqq4s.png" alt=" " width="799" height="351"&gt;&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2Fogrogpzpm4vuwzixs3hz.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%2Fogrogpzpm4vuwzixs3hz.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&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%2Flrfncpv26biyw0vp1few.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%2Flrfncpv26biyw0vp1few.png" alt=" " width="799" height="357"&gt;&lt;/a&gt;&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%2F9ndaikyji8ld9q76wxft.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%2F9ndaikyji8ld9q76wxft.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&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%2F4hnd362cy3vm6s2x7wr5.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%2F4hnd362cy3vm6s2x7wr5.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&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%2Fq9bqdtau01aj3jzmto0k.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%2Fq9bqdtau01aj3jzmto0k.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&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%2F11pkpka72ndmootlvwwr.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%2F11pkpka72ndmootlvwwr.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&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%2Fel7d0f14cxfxlnwr3ltn.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%2Fel7d0f14cxfxlnwr3ltn.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&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%2Fksf7g86ihc5kem0naf66.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%2Fksf7g86ihc5kem0naf66.png" alt=" " width="800" height="365"&gt;&lt;/a&gt;&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%2Fb09cl7owdix7held8aiy.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%2Fb09cl7owdix7held8aiy.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&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%2Fdgib11lmqo3cz2hcpah8.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%2Fdgib11lmqo3cz2hcpah8.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&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%2F0hwhawzv5cwlncarztmm.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%2F0hwhawzv5cwlncarztmm.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&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%2Fmueal9dsoammha3qa0xw.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%2Fmueal9dsoammha3qa0xw.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&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%2Fau4711jka113w07wjqlq.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%2Fau4711jka113w07wjqlq.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&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%2Fvbn63efxx93vrq56tnlz.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%2Fvbn63efxx93vrq56tnlz.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&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%2Fz9h67wtwax86s3vo0c4g.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%2Fz9h67wtwax86s3vo0c4g.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&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%2Fnklk2zh86z4040r4s1kt.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%2Fnklk2zh86z4040r4s1kt.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cloud</category>
      <category>cloudcomputing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Thu, 14 May 2026 20:23:27 +0000</pubDate>
      <link>https://dev.to/4thman/-2cg4</link>
      <guid>https://dev.to/4thman/-2cg4</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/4thman/creating-my-first-azure-resource-group-as-i-begin-my-journey-into-cloud-engineering-and-devops-1cjj" class="crayons-story__hidden-navigation-link"&gt;Creating My First Azure Resource Group as I Begin My Journey Into Cloud Engineering and DevOps&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/4thman" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F3927892%2F43aac6d7-df74-4a90-bdbe-8ba55edf1a70.jpeg" alt="4thman profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/4thman" class="crayons-story__secondary fw-medium m:hidden"&gt;
              David Cletus
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                David Cletus
                
                
              
              &lt;div id="story-author-preview-content-3671755" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/4thman" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F3927892%2F43aac6d7-df74-4a90-bdbe-8ba55edf1a70.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;David Cletus&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/4thman/creating-my-first-azure-resource-group-as-i-begin-my-journey-into-cloud-engineering-and-devops-1cjj" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;May 14&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/4thman/creating-my-first-azure-resource-group-as-i-begin-my-journey-into-cloud-engineering-and-devops-1cjj" id="article-link-3671755"&gt;
          Creating My First Azure Resource Group as I Begin My Journey Into Cloud Engineering and DevOps
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/azure"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;azure&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/beginners"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;beginners&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/cloud"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;cloud&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/4thman/creating-my-first-azure-resource-group-as-i-begin-my-journey-into-cloud-engineering-and-devops-1cjj" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/4thman/creating-my-first-azure-resource-group-as-i-begin-my-journey-into-cloud-engineering-and-devops-1cjj#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
  </channel>
</rss>
