<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Kesara Karannagoda on Medium]]></title>
        <description><![CDATA[Stories by Kesara Karannagoda on Medium]]></description>
        <link>https://medium.com/@kesaralive?source=rss-d60194e755a2------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*r3u69ZSF9naArFUD22c6Zg.jpeg</url>
            <title>Stories by Kesara Karannagoda on Medium</title>
            <link>https://medium.com/@kesaralive?source=rss-d60194e755a2------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 00:14:27 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@kesaralive/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[How to Deploy a High Available Kubernetes Cluster on Azure (Without AKS)]]></title>
            <link>https://medium.com/@kesaralive/how-to-deploy-a-high-available-kubernetes-cluster-on-azure-without-aks-4f2f2f7f099f?source=rss-d60194e755a2------2</link>
            <guid isPermaLink="false">https://medium.com/p/4f2f2f7f099f</guid>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[k8s]]></category>
            <category><![CDATA[kubernetes-cluster]]></category>
            <category><![CDATA[azure]]></category>
            <dc:creator><![CDATA[Kesara Karannagoda]]></dc:creator>
            <pubDate>Mon, 02 Jun 2025 18:53:16 GMT</pubDate>
            <atom:updated>2025-06-02T18:53:16.715Z</atom:updated>
            <content:encoded><![CDATA[<p>Managed Kubernetes offerings like AKS, EKS, and GKE make it easy to spin up clusters with a few clicks. But what if you want complete control? What if you need to host Kubernetes on your own terms — perhaps to meet security, customization, or learning goals?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EQZqUVNaW8-5xAokx2RwCg.jpeg" /></figure><p>In this article, we’ll walk through deploying a <strong>highly available Kubernetes cluster</strong> on <strong>Azure Virtual Machines</strong>, using a <strong>stacked etcd topology.</strong></p><p>I will organize the article into the following four main sections:</p><ol><li><strong>Introduction</strong></li><li><strong>Infrastructure Planning</strong></li><li><strong>Cluster Implementation</strong></li><li><strong>Securing Access to the Cluster</strong></li></ol><h3>🔴 Introduction</h3><h4>🔄 What is High Availability in Kubernetes?</h4><p><strong>High Availability (HA)</strong> in Kubernetes ensures that your cluster can <strong>tolerate failures</strong> and <strong>continue operating without downtime</strong>, even when components or nodes go offline. This is achieved by <strong>redundant deployments</strong> of critical control plane components and careful design of infrastructure to avoid single point of failure.</p><p>In a highly available Kubernetes setup, the focus is on:</p><ul><li><strong>Control plane redundancy</strong> — so the cluster’s brain doesn’t go down with one node.</li><li><strong>etcd quorum</strong> — so the cluster state is never lost or corrupted.</li><li><strong>Worker node failover</strong> — so workloads can continue running even if a node dies.</li></ul><h4>🧠 Components That Require HA</h4><ol><li><strong>kube-apiserver</strong></li></ol><ul><li><strong>Purpose</strong>: It’s the front-end for the Kubernetes control plane. All interactions with the cluster happen via the API server.</li><li><strong>Why HA?</strong>: If this goes down, no users or controllers can make changes to the cluster.</li><li><strong>HA Strategy</strong>: <br> — Run multiple instances across control plane nodes.<br> — Place them behind a load balancer to ensure a single endpoint.<br> — Use health checks (e.g., TCP 6443) to monitor availability.</li></ul><p><strong>2. etcd</strong></p><ul><li><strong>Purpose</strong>: This is the <strong>key-value store</strong> where Kubernetes stores all cluster data (desired state, configs, secrets, events, etc.).</li><li><strong>Why HA?</strong>: If etcd fails or loses quorum, the cluster can’t operate, even if kube-apiserver is running.</li><li><strong>HA Strategy</strong>:<br>— Use an <strong>odd number</strong> of etcd members (minimum 3).<br> — Distribute across control plane nodes (stacked topology) or dedicated machines (external topology).<br> — Ensure <strong>etcd data backups</strong> are in place.</li></ul><p><strong>3. kube-scheduler</strong></p><ul><li><strong>Purpose</strong>: Schedules pods to appropriate worker nodes based on availability, resources, and affinity rules.</li><li><strong>Why HA?</strong>: If the scheduler is down, no new workloads can be scheduled even if nodes are available.</li><li><strong>HA Strategy</strong>:<br> — Run it as a <strong>replicated static pod</strong> on all control plane nodes.<br> — Only <strong>one active leader</strong> schedules at a time, using leader election.<br> — Others remain in standby and take over if the leader fails.</li></ul><p><strong>4. kube-controller-manager</strong></p><ul><li><strong>Purpose</strong>: Manages internal cluster controllers (replication, endpoints, namespace, node monitoring, etc.).</li><li><strong>Why HA?</strong>: If this fails, critical operations like self-healing, scaling, and node health management stop.</li><li><strong>HA Strategy</strong>:<br> — Similar to the scheduler, run multiple instances with <strong>leader election</strong>.<br> — If the active manager goes down, another takes over seamlessly.</li></ul><h4>⚙️ Options for High Available Topology</h4><p>Kubernetes gives us two types of etcd topology options for high availability.</p><ol><li><strong>Stacked etcd topology: </strong>With stacked control plane nodes, where etcd nodes are collocated with control plane nodes.</li><li><strong>External etcd topology: </strong>With external etcd nodes, where etcd runs on separate nodes from the control plane.</li></ol><p>You can visit Kubernete’s official documentation for more details on this.<br><a href="https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/ha-topology/"><em>https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/ha-topology/</em></a></p><h4>🧱 <strong>Stacked etcd Topology vs External etcd topology</strong></h4><p>When designing a highly available Kubernetes cluster, a key decision involves how to deploy the etcd database, which stores all cluster state. There are two common topologies to choose from: <strong>stacked</strong> and <strong>external</strong>.</p><p>In a <strong>stacked etcd topology</strong>, the etcd instances run directly on the same nodes as the control plane components. This approach simplifies the setup process, reduces infrastructure cost, and is well-supported by tools like kubeadm. However, it comes with a tradeoff: if a node fails, you lose both an etcd member and a control plane component simultaneously, which can impact cluster availability.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HaPwGQpJNakMMvFbAimcIA.png" /><figcaption>kubeadm HA topology — stacked etcd (<a href="https://kubernetes.io/images/kubeadm/kubeadm-ha-topology-stacked-etcd.svg">https://kubernetes.io/images/kubeadm/kubeadm-ha-topology-stacked-etcd.svg</a>)</figcaption></figure><p>On the other hand, an <strong>external etcd topology</strong> runs the etcd cluster on separate, dedicated nodes. This improves fault isolation and performance, especially in larger or production environments. But it also introduces additional complexity, requiring more nodes, a separate deployment process, and extra operational overhead to manage etcd independently.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*V2I5_sgxIJKlY-V3xPLcww.png" /><figcaption>kubeadm HA topology — external etcd (<a href="https://kubernetes.io/images/kubeadm/kubeadm-ha-topology-external-etcd.svg">https://kubernetes.io/images/kubeadm/kubeadm-ha-topology-external-etcd.svg</a>)</figcaption></figure><p>For this tutorial, we choose the <strong>stacked etcd topology</strong> because it offers a <strong>good balance of high availability and simplicity</strong>, making it ideal for self-hosted clusters on virtual machines where infrastructure resources may be limited. It also aligns well with kubeadm, which streamlines the bootstrapping process for a highly available Kubernetes control plane.</p><h3>🔴 Infrastructure plan</h3><p>To build a robust, highly available Kubernetes cluster from scratch — without using AKS — we’ll first lay out a secure and scalable network architecture on Azure.</p><h4>1. Virtual Network and Subnet Design</h4><p>We begin by creating an Azure Virtual Network (VNet) that will serve as the backbone of our cluster. Inside this VNet, we define two subnets.</p><ul><li><strong>Public Subnet:</strong> Exposes entry points to the outside world.</li><li><strong>Private Subnet:</strong> Host all Kubernetes-related infrastructure, fully isolated from the direct public access.</li></ul><p>🔐 The private subnet will only be accessible <strong>via the public subnet</strong>, ensuring better control and security.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tgIUk9Zb6i9dQwexLQaD3w.jpeg" /><figcaption>virtual network and subnet design</figcaption></figure><h4>2. Jumpbox Setup</h4><p>We’ll create a VM in the <strong>public subnet</strong>, which acts as a secure access point into the private subnet.</p><ul><li>VM Name: jumpbox-vm</li><li>This VM will have a public IP and SSH access.</li><li>From here, we’ll manage and provision all nodes inside the private subnet.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sn1FGvPOa21077jN1kFK6w.png" /><figcaption>jumpbox setup / secure access point into the private subnet</figcaption></figure><h4>3. Kubernetes Cluster Nodes</h4><p>Our Kubernetes cluster will follow a <strong>high availability control plane</strong> pattern with a <strong>stacked etcd topology</strong>.</p><ul><li><strong>Control Plane Nodes (in private subnet):<br> — </strong>master-1 <br> — master-2 <br> — master-3</li><li><strong>Worker Nodes (in private subnet):<br> — </strong>worker-1 <br> — worker-2 <br> — worker-3</li></ul><p>This 3x3 setup ensures quorum on both etcd and control plane and supports failover for workloads.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Re_vDdkDROWkbSJ3ebVugw.png" /><figcaption>control plane nodes and worker nodes</figcaption></figure><h4>4. Internal Load Balancer and NAT Gateway</h4><ul><li>We’ll create an <strong>Internal Load Balancer (ILB)</strong> to distribute traffic among the control plane nodes. This will be used as the --control-plane-endpoint in <strong>kubeadm, </strong>which we will discuss later in the article.</li><li>All private subnet resources will reach the internet through a <strong>NAT Gateway</strong>, allowing updates and image pulls without exposing any resources directly.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hyCzsAID2EDz1TMrtX-rmQ.png" /><figcaption>internal loadbalancer to complete the stacked etcd topology</figcaption></figure><h4>5. Network Security Groups (NSGs)</h4><p>To enforce strict access control:</p><ul><li><strong>Public Subnet NSG</strong> will only allow SSH (port 22) to the jumpbox-vm.</li><li><strong>Private Subnet NSG</strong> will allow:<br> — Internal communication between cluster nodes<br> — Access <strong>only from the jumpbox</strong></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cz0eQFjkysvHY5IaKIePWQ.png" /></figure><h3>🔴 Implementation</h3><h4>✅ Prerequisites</h4><p>Before we dive into building a highly available Kubernetes cluster on Azure, ensure you have the following tools and access configured:</p><ol><li><strong>Azure Account with Sufficient Permissions</strong></li></ol><p>You’ll need an Azure account with permission to create and manage:</p><ul><li>Resource Groups</li><li>Virtual Networks (VNet)</li><li>Subnets</li><li>Virtual Machines (VMs)</li><li>Network Security Groups (NSGs)</li><li>LoadBalancers and NAT Gateways</li></ul><p>If you don’t already have an Azure account, you can create one here (comes with a free tier): 🔗 <a href="https://azure.microsoft.com/en-us/free/">Create Azure Account</a></p><p><strong>2. Azure CLI Installed</strong></p><p>We’ll use the Azure CLI (az) extensively to provision resources. Make sure it&#39;s installed and configured on your local machine.</p><ul><li>🔗 <a href="https://learn.microsoft.com/en-us/cli/azure/install-azure-cli">Install Azure CLI</a></li><li>After installation, login with:</li></ul><pre>az login</pre><p><strong>3. Visual Studio Code (or any code editor)</strong></p><p>Having a good editor will help you manage and write configuration files and commands.</p><ul><li>🔗 <a href="https://code.visualstudio.com/">Download VS Code</a></li></ul><p><strong>4. Basic Knowledge Requirements</strong></p><p>You should be comfortable with:</p><ul><li>Linux command line and SSH</li><li>Basic networking (subnets, IPs, firewalls)</li><li>Basic Kubernetes concepts</li></ul><h4>⚪️ Implementation Setup</h4><p>Now that you’ve got everything ready, it’s time to build the highly available K8s cluster on Azure — <strong>from scratch</strong> and <strong>without relying on AKS</strong>.</p><p>First, I will create a directory for my project. Inside this directory, I will create a file named vars.sh. You can open the directory in your code editor (e.g., VS Code).</p><pre>.<br>└── vars.sh<br><br>1 directory, 1 file</pre><pre># vars.sh<br>LOCATION=&quot;germanywestcentral&quot;<br>RESOURCE_GROUP=&quot;kubernetes&quot;<br>ADMIN_USERNAME=&quot;azureuser&quot;<br><br>VNET_NAME=&quot;k8sVnet&quot;<br>ADDRESS_PREFIX=&quot;10.0.0.0/16&quot;<br><br>PUBLIC_SUBNET_NAME=&quot;public-subnet&quot;<br>PUBLIC_SUBNET_PREFIX=&quot;10.0.1.0/24&quot;<br><br>PRIVATE_SUBNET_NAME=&quot;private-subnet&quot;<br>PRIVATE_SUBNET_PREFIX=&quot;10.0.2.0/24&quot;<br><br>NSG_PUBLIC=&quot;nsg-public&quot;<br>NSG_PRIVATE=&quot;nsg-private&quot;<br><br>NAT_IP_NAME=&quot;nat-ip&quot;<br>NAT_GATEWAY_NAME=&quot;nat-gateway&quot;<br><br>JUMPBOX_NAME=&quot;jumpbox-vm&quot;<br>VM_SIZE=&quot;Standard_B2ms&quot;<br><br>ILB_NAME=&quot;k8s-ilb&quot;<br>ILB_FRONTEND_NAME=&quot;k8s-ilb-frontend&quot;<br>ILB_BACKEND_POOL=&quot;k8s-ilb-backend&quot;<br>ILB_PROBE_NAME=&quot;kube-api-probe&quot;<br>ILB_RULE_NAME=&quot;kube-api-rule&quot;<br>ILB_PRIVATE_IP=&quot;10.0.2.10&quot;</pre><p>I’m going to use these variables for the setup. For example, I’ve selected germanywestcentral as my location. You can modify these values as needed. If you want your cluster to be in a different location, feel free to change it.</p><p>Then you can run the following command to use it in your terminal session.</p><h4>⚠️ Reminder</h4><p>Every time you start a new terminal session, you need to run the following command to load the environment variables:</p><pre>source vars.sh</pre><p>You can verify it by running echo $LOCATION .</p><pre>echo $LOCATION</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/560/1*9gfQFNC5950Yzhu2JHssCw.png" /></figure><p>Then you have to login to your azure account using azure cli.</p><pre>az login</pre><p>After login, you can verify you account details.</p><pre>az account show</pre><p>Then you can start by creating a resource group.</p><pre>az group create \<br>--name $RESOURCE_GROUP \<br>--location $LOCATION</pre><p>You can verify it by running.</p><pre>az group list \<br>--query &quot;[?name==&#39;$RESOURCE_GROUP&#39;]&quot;</pre><h4>1. Create Virtual Network and Subnets.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tgIUk9Zb6i9dQwexLQaD3w.jpeg" /><figcaption>virtual network and subnet design</figcaption></figure><p>Create the virtual network k8sVnet with an address prefix of 10.0.0.0/16, along with a public subnet (10.0.1.0/24) and a private subnet (10.0.2.0/24) by running the following commands in order.</p><p><strong>virtual-network</strong></p><pre>az network vnet create \<br>--resource-group $RESOURCE_GROUP \<br>--name $VNET_NAME \<br>--address-prefix $ADDRESS_PREFIX</pre><p><strong>public-subnet </strong><strong>10.0.1.0/24</strong></p><pre>az network vnet subnet create \<br>--resource-group $RESOURCE_GROUP \<br>--vnet-name $VNET_NAME \<br>--name $PUBLIC_SUBNET_NAME \<br>--address-prefix $PUBLIC_SUBNET_PREFIX</pre><p><strong>private-subnet </strong><strong>10.0.2.0/24</strong></p><pre>az network vnet subnet create \<br>--resource-group $RESOURCE_GROUP \<br>--vnet-name $VNET_NAME \<br>--name $PRIVATE_SUBNET_NAME \<br>--address-prefix $PRIVATE_SUBNET_PREFIX</pre><h4>2. Network Security Groups</h4><p>Create network security groups and attach them to the subnets.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tXmLdJUFbQbKCh3UIRzQkw.png" /><figcaption>Network Security Groups Setup</figcaption></figure><p><strong>nsg-public to public-subnet</strong></p><pre>az network nsg create -g $RESOURCE_GROUP -n $NSG_PUBLIC<br><br>az network vnet subnet update \<br>  --resource-group $RESOURCE_GROUP --vnet-name $VNET_NAME \<br>  --name $PUBLIC_SUBNET_NAME --network-security-group $NSG_PUBLIC</pre><p><strong>nsg-private to private-subnet</strong></p><pre>az network nsg create -g $RESOURCE_GROUP -n $NSG_PRIVATE<br><br>az network vnet subnet update \<br>  --resource-group $RESOURCE_GROUP --vnet-name $VNET_NAME \<br>  --name $PRIVATE_SUBNET_NAME --network-security-group $NSG_PRIVATE</pre><h4>3. Jumpbox Setup</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sn1FGvPOa21077jN1kFK6w.png" /><figcaption>jumpbox setup / secure access point into the private subnet</figcaption></figure><p>First, let’s create an SSH key pair to connect to the jumpbox-vm. You can customize the command with your email address.</p><pre>ssh-keygen -t rsa -b 4096 -C &quot;&lt;your-email&gt;&quot; -f azureuser -N &quot;&quot;</pre><p>Your directory should now look like the following.</p><pre>.<br>├── azureuser<br>├── azureuser.pub<br>└── vars.sh</pre><p>Here, I’m assigning the private IP 10.0.1.4 to the jumpbox-vm, as shown in our diagram. If you&#39;ve used a different setup, make sure to adjust it accordingly.</p><pre>az vm create \<br>  --resource-group $RESOURCE_GROUP \<br>  --name $JUMPBOX_NAME \<br>  --image Ubuntu2204 \<br>  --vnet-name $VNET_NAME \<br>  --subnet $PUBLIC_SUBNET_NAME \<br>  --private-ip-address 10.0.1.4 \<br>  --size $VM_SIZE \<br>  --nsg $NSG_PUBLIC \<br>  --admin-username $ADMIN_USERNAME \<br>  --ssh-key-values azureuser.pub</pre><p>After running the above command, you will see the <strong>publicIpAddress</strong> of the jumpbox-vm in the output. Save it for later use.</p><p>If you try to SSH into the jumpbox-vm immediately after creating it, it won’t work due to the Network Security Group (NSG) rules. You need to add a rule to allow SSH access to the VM.</p><pre>az network nsg rule create \<br>  --resource-group $RESOURCE_GROUP --nsg-name $NSG_PUBLIC \<br>  --name Allow-SSH --priority 1000 \<br>  --access Allow --direction Inbound \<br>  --protocol Tcp --destination-port-range 22 --source-address-prefixes Internet</pre><p>You can now use the private key we created earlier to SSH into the <strong>jumpbox-vm</strong>.</p><pre>ssh -i azureuser azureuser@&lt;jumpbox-public-ip-address&gt;</pre><h4>4. Create Kubernetes Master Nodes. (Control-plane nodes)</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vgKNa82DB7OQX__hj-sjEQ.png" /><figcaption>Control plane node pool</figcaption></figure><p><strong>master-1 (10.0.2.4)</strong></p><pre>az vm create \<br>    --resource-group $RESOURCE_GROUP \<br>    --name master-1 \<br>    --image Ubuntu2204 \<br>    --vnet-name $VNET_NAME \<br>    --subnet $PRIVATE_SUBNET_NAME \<br>    --private-ip-address 10.0.2.4 \<br>    --size $VM_SIZE \<br>    --nsg $NSG_PRIVATE \<br>    --public-ip-address &quot;&quot; \<br>    --admin-username $ADMIN_USERNAME \<br>    --ssh-key-values azureuser.pub</pre><p><strong>master-2 (10.0.2.5)</strong></p><pre>az vm create \<br>    --resource-group $RESOURCE_GROUP \<br>    --name master-2 \<br>    --image Ubuntu2204 \<br>    --vnet-name $VNET_NAME \<br>    --subnet $PRIVATE_SUBNET_NAME \<br>    --private-ip-address 10.0.2.5 \<br>    --size $VM_SIZE \<br>    --nsg $NSG_PRIVATE \<br>    --public-ip-address &quot;&quot; \<br>    --admin-username $ADMIN_USERNAME \<br>    --ssh-key-values azureuser.pub</pre><p><strong>master-3 (10.0.2.5)</strong></p><pre>az vm create \<br>    --resource-group $RESOURCE_GROUP \<br>    --name master-3 \<br>    --image Ubuntu2204 \<br>    --vnet-name $VNET_NAME \<br>    --subnet $PRIVATE_SUBNET_NAME \<br>    --private-ip-address 10.0.2.6 \<br>    --size $VM_SIZE \<br>    --nsg $NSG_PRIVATE \<br>    --public-ip-address &quot;&quot; \<br>    --admin-username $ADMIN_USERNAME \<br>    --ssh-key-values azureuser.pub</pre><h4>5. Accessing Control Plane nodes</h4><p>To access these nodes, you first need to copy the private key to the jumpbox-vm. You can do this using the following command: <strong><em>use your jumpbox vm ip</em></strong></p><pre>scp -i azureuser azureuser azureuser@&lt;jumpbox-public-ip&gt;:/home/azureuser/</pre><p>You can now SSH into the jumpbox-vm and run the ls command to verify that the file is there.</p><pre>azureuser@jumpbox-vm:~$ ls<br>azureuser</pre><p>You should see the private key there.<br>Now, if you try to SSH into one of the control plane nodes using the copied private key, you should be able to connect successfully.</p><pre>ssh -i azureuser azureuser@10.0.2.4</pre><p>By default, Azure Network Security Groups allow access to any resources inside the Virtual Network from other resources within the same network.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vWPCIdbEgMOHHyFJb_gENg.png" /><figcaption>Inbound Security Rule (priority: 65000) AllowVnetInBound</figcaption></figure><p>So, we’re going to add a rule to <strong>block all SSH access</strong> initially.</p><pre>az network nsg rule create \<br>  --resource-group $RESOURCE_GROUP --nsg-name $NSG_PRIVATE \<br>  --name Deny-SSH --priority 1200 \<br>  --access Deny --direction Inbound \<br>  --protocol Tcp --destination-port-range 22</pre><p>Now, if you try to SSH into the Kubernetes nodes, you won’t be able to. Let’s <strong>allow SSH access from the jumpbox</strong>.</p><pre>az network nsg rule create \<br>  --resource-group $RESOURCE_GROUP --nsg-name $NSG_PRIVATE \<br>  --name Allow-SSH-from-jumpbox --priority 1100 \<br>  --access Allow --direction Inbound \<br>  --protocol Tcp --destination-port-range 22 \<br>  --source-address-prefix 10.0.1.4/32</pre><p>You should now be able to access the control plane nodes from the jumpbox-vm again.</p><h4>6. Create the Internal Load Balancer.</h4><pre>az network lb create \<br>  --resource-group $RESOURCE_GROUP \<br>  --name $ILB_NAME \<br>  --sku Standard \<br>  --vnet-name $VNET_NAME \<br>  --subnet $PRIVATE_SUBNET_NAME \<br>  --frontend-ip-name $ILB_FRONTEND_NAME \<br>  --backend-pool-name $ILB_BACKEND_POOL \<br>  --private-ip-address $ILB_PRIVATE_IP \<br>  --private-ip-address-version IPv4 \<br>  --frontend-ip-zone 1 \<br>  --public-ip-address &quot;&quot; \<br>  --output table</pre><p><strong>Health probe</strong></p><pre>az network lb probe create \<br>  --resource-group $RESOURCE_GROUP \<br>  --lb-name $ILB_NAME \<br>  --name $ILB_PROBE_NAME \<br>  --protocol Tcp \<br>  --port 6443 \<br>  --interval 15 \<br>  --threshold 4</pre><p><strong>Loadbalancing rule for the port 6443</strong></p><pre>az network lb rule create \<br>  --resource-group $RESOURCE_GROUP \<br>  --lb-name $ILB_NAME \<br>  --name $ILB_RULE_NAME \<br>  --protocol Tcp \<br>  --frontend-port 6443 \<br>  --backend-port 6443 \<br>  --frontend-ip-name $ILB_FRONTEND_NAME \<br>  --backend-pool-name $ILB_BACKEND_POOL \<br>  --probe-name $ILB_PROBE_NAME \<br>  --idle-timeout 15 \<br>  --enable-tcp-reset true</pre><h4>7. Add Control-plane nodes to the LoadBalancer</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*o0YC1lO97mq--cGUrHjL-A.png" /><figcaption>Control plane pool and LoadBalancer</figcaption></figure><p>Get the network interface names for the newly created nodes, such as master-1.</p><p>To add the control plane nodes to the load balancer backend pool, retrieve both the NIC name and the IP configuration name.</p><pre>az vm show \<br>  --resource-group $RESOURCE_GROUP \<br>  --name master-1 \<br>  --query &quot;networkProfile.networkInterfaces[0].id&quot; \<br>  --output tsv | xargs az network nic show --ids | jq &#39;{nicName: .name, ipConfigName: .ipConfigurations[0].name}&#39;</pre><p>Add the master-1 to the backend pool.</p><pre>az network nic ip-config address-pool add \<br>  --address-pool $ILB_BACKEND_POOL \<br>  --ip-config-name &lt;ipConfigName&gt; \<br>  --nic-name &lt;nicName&gt; \<br>  --lb-name $ILB_NAME \<br>  --resource-group $RESOURCE_GROUP</pre><p>🔁 <strong>Repeat the same steps for the other two control plane nodes, </strong><strong>master-2 and </strong><strong>master-3.</strong></p><p>After adding all three nodes, you can verify the changes in the UI as well.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UyAL33RDFS8tjujzjmV69A.png" /><figcaption>added control-plane nodes in the loadbalancer backend pool</figcaption></figure><p>Now, the control plane nodes are created and connected to the load balancer through a load balancing rule.</p><h4>8. Add a Nat Gateway</h4><p>We need a NAT Gateway to allow the private nodes (such as Kubernetes nodes) to access the public internet. For example, nodes may need to install packages or fetch updates. This access will be routed through the NAT Gateway.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*c7KsJscJSH5IeCxMJYVsBA.png" /><figcaption>Nat Gateway</figcaption></figure><p><strong>Step 1: Create a Public IP Address</strong></p><pre>az network public-ip create \<br>  --resource-group $RESOURCE_GROUP \<br>  --name $NAT_IP_NAME \<br>  --sku Standard \<br>  --allocation-method Static \<br>  --location $LOCATION</pre><p><strong>Step 2: Create the NAT Gateway using the Public IP</strong></p><pre>az network nat gateway create \<br>--resource-group $RESOURCE_GROUP \<br>--name $NAT_GATEWAY_NAME \<br>--location $LOCATION \<br>--public-ip-address $NAT_IP_NAME \<br>--idle-timeout 10</pre><p><strong>Step 3: Attach the NAT Gateway to the Private Subnet</strong></p><pre>az network vnet subnet update \<br>--resource-group $RESOURCE_GROUP \<br>--vnet-name $VNET_NAME \<br>--name $PRIVATE_SUBNET_NAME \<br>--nat-gateway $NAT_GATEWAY_NAME</pre><h4>Jumpbox update</h4><p>Before configuring the control plane nodes, let’s SSH into our jumpbox-vm and make some changes to simplify the upcoming steps.</p><pre>ssh -i azureuser azureuser@&lt;public-ip-address-jumpbox-vm&gt;</pre><p>We previously stored our private key in the azureuser directory to access the control plane nodes. Let’s add it to the SSH agent on the jumpbox-vm so we don’t have to specify it explicitly in our SSH commands.</p><pre>eval &quot;$(ssh-agent -s)&quot;       <br>ssh-add azureuser </pre><p>Now, if you’re logged in as azureuser on the jumpbox-vm, you should be able to SSH into any control plane node simply by running ssh 10.0.2.4 or ssh master-1.</p><pre>whoami</pre><p>Use one of the commands I mentioned to access the master-1 VM.</p><p>📌 Tip (for beginners): You can always press <strong>Ctrl + D</strong> to exit the current SSH session and return to the jumpbox-vm shell.</p><pre>ssh 10.0.2.4</pre><p>Once you’ve SSH’ed into a master node, you can run the following command to check whether your node is accessing the internet through the previously created NAT Gateway.</p><pre>curl ifconfig.me</pre><p>If this shows the public IP address of the NAT Gateway, it means the nodes are accessing the internet through the NAT Gateway.</p><h4>📖 Extra Knowledge: NAT Gateway</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XI1Q0W3t-1mUsBGT7C3HgQ.png" /><figcaption>NAT Gateway</figcaption></figure><p><strong>Imagine you’re running an external service on Server X, which is located behind a firewall in another cloud environment. You only want to give access to VMs or nodes running within your private subnet.</strong></p><p>To make this secure and manageable, you need to ensure that all outbound traffic from your private nodes uses a <strong>static egress IP address</strong>. This way, you can configure Server X’s firewall to allow traffic only from that known IP.</p><p>In this architecture, the <strong>NAT Gateway</strong> is assigned the static public IP address 4.1.35.201. It is attached to the private subnet (10.0.2.0/24) that hosts all your Kubernetes nodes—masters and workers. Although these nodes don’t have public IPs and cannot be accessed directly from the internet, they can still <strong>initiate outbound connections</strong> (e.g., to Server X) through the NAT Gateway.</p><p><strong>Here’s how it works step-by-step:</strong></p><ol><li>A Kubernetes node (e.g., master-1 or worker-1) sends a request to Server X.</li><li>Since the node is in a private subnet with no public IP, the request is forwarded to the NAT Gateway.</li><li>The NAT Gateway <strong>translates the source IP</strong> of the outgoing packet to its own public IP (4.1.35.201) and sends it to Server X over the internet.</li><li>Server X receives the request and sees it as coming from 4.1.35.201, which is the only IP it is configured to allow.</li><li>Server X responds, and the NAT Gateway translates the destination IP back to the internal IP of the original requesting node, ensuring the response reaches the correct machine inside your private network.</li></ol><p>This setup provides:</p><ul><li><strong>Security</strong>: Your Kubernetes nodes remain private and inaccessible from the internet.</li><li><strong>Simplicity</strong>: You only have to manage a single IP (4.1.35.201) in Server X&#39;s firewall.</li><li><strong>Reliability</strong>: The egress IP remains static, which is essential for services that require IP allow listing.</li></ul><h3><strong>🔧 Essential Components for Configuring a Kubernetes Node</strong></h3><p>Before configuring a Kubernetes node, it’s important to understand the key components involved in the setup process.</p><h4>1. cgroups (Control Groups)</h4><p>cgroups is a <strong>low-level Linux kernel feature</strong> that allows you to allocate CPU, memory, disk I/O, and other resources to processes. It forms the <strong>foundation</strong> for container runtimes like containerd and Docker.</p><p>Kubernetes depends on cgroups to enforce pod-level resource limits like cpu and memory.</p><h4>2. containerd</h4><p>containerd is a <strong>lightweight container runtime</strong> used by Kubernetes to run containers. It handles image management, container lifecycle, and storage interfaces. It interacts with the Linux kernel (via cgroups) to isolate and run containers.</p><p>Kubernetes uses containerd to <strong>create and run pods</strong> behind the scenes.</p><p>Other than containerd Kubernetes supports some other container runtimes as well. For more details, you can check the official documentation.<br><a href="https://kubernetes.io/docs/setup/production-environment/container-runtimes/">https://kubernetes.io/docs/setup/production-environment/container-runtimes/</a></p><h4>3. kubelet</h4><p>kubelet is a <strong>daemon</strong> that runs on every Kubernetes node. It watches for <strong>pod specifications</strong> (from the API server), pulls the required container images via containerd, and ensures containers are running as expected.</p><p>It’s the <strong>core component on each node</strong> that keeps things working locally.</p><h4>4. kubeadm</h4><p>kubeadm is a <strong>one-time setup tool</strong> used to initialize a Kubernetes <strong>control plane</strong> or join a <strong>node</strong> to an existing cluster. It handles all the low-level configuration of certificates, networking, and kubelet bootstrap configs.</p><p>Used only during <strong>cluster creation or node joining</strong>.</p><h4>5. kubectl</h4><p>kubectl is a <strong>command-line interface</strong> used by users (not the node itself) to interact with the Kubernetes API server. You use it to deploy apps, inspect resources, and manage the cluster.</p><p>It is <strong>not required on the node itself</strong>, but essential for managing the cluster from a user/admin perspective.</p><h3>⚓️ Configure a Kubernetes Node (Ubuntu)</h3><p>🔁 Repeat these steps on <strong>all worker and control plane nodes</strong> before joining them to the cluster. <strong>(First do this for control plane nodes)</strong></p><h4>1. Enable IP Forwarding</h4><p>Kubernetes networking requires IP forwarding to route packets between pods and services.</p><pre>sudo sysctl -w net.ipv4.ip_forward=1<br>echo &quot;net.ipv4.ip_forward = 1&quot; | sudo tee -a /etc/sysctl.conf<br>sudo sysctl -p</pre><h4>2. Install and Configure containerd (Container Runtime)</h4><p>Kubernetes needs a container runtime to run containers. containerd is a lightweight, recommended option.</p><pre>sudo apt update &amp;&amp; sudo apt install -y containerd</pre><p>Generate the default <strong>containerd</strong> configuration:</p><pre>sudo mkdir -p /etc/containerd<br>containerd config default | sudo tee /etc/containerd/config.toml</pre><p>Restart <strong>containerd</strong> to apply the configuration:</p><pre>sudo systemctl restart containerd</pre><h4>3. Configure systemd as the cgroup driver for containerd</h4><p>Kubernetes works best when the container runtime uses systemd for managing <strong>cgroups</strong>, which matches the kubelet&#39;s default behavior.</p><p>Open /etc/containerd/config.toml and edit the following block:</p><p><a href="https://kubernetes.io/docs/setup/production-environment/container-runtimes/#containerd-systemd">https://kubernetes.io/docs/setup/production-environment/container-runtimes/#containerd-systemd</a></p><pre>[plugins.&quot;io.containerd.grpc.v1.cri&quot;.containerd.runtimes.runc]<br>  ...<br>  [plugins.&quot;io.containerd.grpc.v1.cri&quot;.containerd.runtimes.runc.options]<br>    SystemdCgroup = true</pre><p><strong>✴️Optional: You can also use the following command to edit that.</strong></p><pre>sudo sed -i &#39;s/SystemdCgroup = false/SystemdCgroup = true/&#39; /etc/containerd/config.toml</pre><p>Save and restart <strong>containerd</strong> again:</p><pre>sudo systemctl restart containerd</pre><h4>4. Install Kubernetes Tools: kubelet, kubeadm, kubectl</h4><p>These are the core tools for running and managing Kubernetes.</p><ul><li><strong>kubelet</strong>: Runs on each node and manages containers.</li><li><strong>kubeadm</strong>: Bootstraps and manages the cluster.</li><li><strong>kubectl</strong>: CLI to interact with the cluster.</li></ul><p>Add the Kubernetes APT repository and install the tools:</p><pre>sudo apt-get update<br>sudo apt-get install -y apt-transport-https ca-certificates curl gpg<br>curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.33/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg<br>echo &#39;deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.33/deb/ /&#39; | sudo tee /etc/apt/sources.list.d/kubernetes.list</pre><p>Install the components:</p><pre>sudo apt-get update<br>sudo apt-get install -y kubelet kubeadm kubectl</pre><p>Prevent automatic updates that might cause version mismatches:</p><pre>sudo apt-mark hold kubelet kubeadm kubectl</pre><h4>5. Enable the Kubelet Service</h4><p>Ensure the kubelet starts at boot and is running now:</p><pre>sudo systemctl enable --now kubelet</pre><h3>🚧 Control-plane pool</h3><h4>Initialize the Kubernetes Control-plane</h4><p>To initialize the Kubernetes control plane, we will use kubeadm with a custom kubeadm init configuration file. Knowing the private IP addresses of the three master nodes and the load balancer, we will configure the kubeadm init file accordingly.</p><h4>In master-1 (10.0.2.4)</h4><ol><li>Create the following kubeadm-init.yaml using the nano editor.</li></ol><pre>sudo nano kubeadm-init.yaml</pre><pre>apiVersion: kubeadm.k8s.io/v1beta4<br>kind: InitConfiguration<br>localAPIEndpoint:<br>  advertiseAddress: 10.0.2.4<br>  bindPort: 6443<br>nodeRegistration:<br>  criSocket: unix:///var/run/containerd/containerd.sock<br>  name: master-1<br>  taints:<br>  - key: node-role.kubernetes.io/control-plane<br>    effect: NoSchedule<br><br>---<br>apiVersion: kubeadm.k8s.io/v1beta4<br>kind: ClusterConfiguration<br>kubernetesVersion: v1.33.0<br>controlPlaneEndpoint: 10.0.2.10:6443<br>networking:<br>  dnsDomain: cluster.local<br>  podSubnet: 10.244.0.0/16<br>  serviceSubnet: 10.96.0.0/12</pre><p>In Azure, when using an <strong>internal Load Balancer</strong> for your Kubernetes control plane (like controlPlaneEndpoint: 10.0.2.10:6443), the <strong>hairpin NAT issue</strong> prevents a control plane node from accessing the Load Balancer IP that maps back to itself. This is a known limitation in Azure networking.</p><p>You can read more about this in following links.</p><p><a href="https://github.com/microsoft/Azure-ILB-hairpin">https://github.com/microsoft/Azure-ILB-hairpin</a></p><p>To work around this <strong>for the first control plane node</strong>, you can use a <strong>loopback interface</strong> that binds the controlPlaneEndpoint IP locally on that node.</p><p><strong>✅ Workaround: Bind the Load Balancer IP to a Loopback Interface</strong></p><p>This tricks the first node into thinking the Load Balancer IP is local, allowing it to talk to itself through the Load Balancer VIP (in our case 10.0.2.10).</p><pre>sudo ip addr add 10.0.2.10/32 dev lo</pre><blockquote>⚠️ Optional</blockquote><blockquote>You don’t have to make this change persist after reboot if you plan to add the other control plane nodes at the same time. Otherwise, it’s better to make it persistent.</blockquote><p>2. Initialize the first control plane node (master-1) using the kubeadm-init.yaml file. (This process might take some time to complete.)</p><pre>sudo kubeadm init --config kubeadm-init.yaml --v=5</pre><p>At the end, you will see two commands in the output similar to the following:</p><p>These are used to join additional control plane nodes.</p><pre>kubeadm join 10.0.2.10:6443 --token 3yfgob.oczs4ho9aynj0p6g \<br>        --discovery-token-ca-cert-hash sha256:9cd4043be872b09b8af8b10f06f22922cc21d34abff4628e1999028000834da2 \<br>        --control-plane </pre><p>To join worker nodes</p><pre>kubeadm join 10.0.2.10:6443 --token fetdv4.ngb7hi792zs4150x \<br>        --discovery-token-ca-cert-hash sha256:3c25328284a8436a959c51df6942a6c8c9660f0eaba77a3044f8bbb3c841a65c</pre><p>Save these commands somewhere, as you’ll need them to join the other nodes in the next steps.</p><p><strong>⚠️ You should save above commands from your output.</strong></p><p>3. Run the following commands from the output to access the cluster.</p><p><strong>First switch to the root user.</strong></p><pre>sudo su</pre><pre>mkdir -p $HOME/.kube<br>sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config<br>sudo chown $(id -u):$(id -g) $HOME/.kube/config</pre><pre>export KUBECONFIG=/etc/kubernetes/admin.conf</pre><p>Check the created node using the following command:</p><pre>sudo kubectl get nodes</pre><p>4. Install a CNI Plugin</p><h4><strong>Purpose of CNI in Kubernetes</strong></h4><p>In Kubernetes, CNI plugins are responsible for <strong>setting up the network</strong> for your pods. This includes:</p><ul><li>Assigning an <strong>IP address</strong> to each pod.</li><li>Setting up the <strong>routes</strong> so pods can talk to each other.</li><li>Enabling <strong>external access</strong> to pods if required.</li><li>Optionally enforcing <strong>network policies</strong> (e.g., deny pod-to-pod communication).</li></ul><p>Without a CNI plugin, pods cannot get IP addresses or communicate with each other, and the node stays in a NotReady state.</p><p><strong>Install Calico as CNI</strong></p><pre>sudo kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml</pre><p>Check if Calico pods come up</p><pre>sudo kubectl get pods -n kube-system -o wide</pre><p>Run the following command to get the certificate-key to use when joining the next node.</p><pre>sudo kubeadm init phase upload-certs --upload-certs</pre><p>Save the certificate key.</p><h4>👐 Join other control-plane nodes. (master-2, master-3)</h4><p>SSH into master-2 (10.0.2.5).</p><p>Then, run the previously saved kubeadm join command, appending the certificate key you stored, as shown below. master-2 should be configured as in the previous strep (<strong>Configure a Kubernetes Node (Ubuntu)</strong>). If not, go back and complete that configuration first.</p><p>Append the previously saved kubeadm join command with the certificate key you saved earlier, as follows.</p><p>⚠️ Use your own stored commands in the following steps — not the examples shown here.</p><pre>kubeadm join 10.0.2.10:6443 --token fetdv4.ngb7hi792zs4150x \<br>        --discovery-token-ca-cert-hash sha256:3c25328284a8436a959c51df6942a6c8c9660f0eaba77a3044f8bbb3c841a65c \<br>        --control-plane --certificate-key 848a88f680f1050d15f101977799c4bde43d514ed5385333dbfdc304de39dfa9</pre><p><strong>Then run the command as the root user.</strong></p><pre>sudo su</pre><pre>sudo kubeadm join 10.0.2.10:6443 --token fetdv4.ngb7hi792zs4150x \<br>        --discovery-token-ca-cert-hash sha256:3c25328284a8436a959c51df6942a6c8c9660f0eaba77a3044f8bbb3c841a65c \<br>        --control-plane --certificate-key 848a88f680f1050d15f101977799c4bde43d514ed5385333dbfdc304de39dfa9</pre><p>You can execute the output commands just like you did before.</p><pre>mkdir -p $HOME/.kube<br>        sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config<br>        sudo chown $(id -u):$(id -g) $HOME/.kube/config</pre><p>You’ll be able to see both nodes listed by running kubectl get nodes.</p><p>Follow the same steps for <strong>master-3</strong>.</p><h3>👷 Worker Nodes</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kQeifx-VzXANfk9xxze9tA.png" /><figcaption>Adding worker nodes</figcaption></figure><ol><li>Create three worker nodes in the same private subnet.</li></ol><p>worker-1</p><pre>az vm create \<br>    --resource-group $RESOURCE_GROUP \<br>    --name worker-1 \<br>    --image Ubuntu2204 \<br>    --vnet-name $VNET_NAME \<br>    --subnet $PRIVATE_SUBNET_NAME \<br>    --private-ip-address 10.0.2.7 \<br>    --size $VM_SIZE \<br>    --nsg $NSG_PRIVATE \<br>    --public-ip-address &quot;&quot; \<br>    --admin-username $ADMIN_USERNAME \<br>    --ssh-key-values azureuser.pub</pre><p>worker-2</p><pre>az vm create \<br>    --resource-group $RESOURCE_GROUP \<br>    --name worker-2 \<br>    --image Ubuntu2204 \<br>    --vnet-name $VNET_NAME \<br>    --subnet $PRIVATE_SUBNET_NAME \<br>    --private-ip-address 10.0.2.8 \<br>    --size $VM_SIZE \<br>    --nsg $NSG_PRIVATE \<br>    --public-ip-address &quot;&quot; \<br>    --admin-username $ADMIN_USERNAME \<br>    --ssh-key-values azureuser.pub</pre><p>worker-3</p><pre>az vm create \<br>    --resource-group $RESOURCE_GROUP \<br>    --name worker-3 \<br>    --image Ubuntu2204 \<br>    --vnet-name $VNET_NAME \<br>    --subnet $PRIVATE_SUBNET_NAME \<br>    --private-ip-address 10.0.2.9 \<br>    --size $VM_SIZE \<br>    --nsg $NSG_PRIVATE \<br>    --public-ip-address &quot;&quot; \<br>    --admin-username $ADMIN_USERNAME \<br>    --ssh-key-values azureuser.pub</pre><p>2. SSH into the worker nodes (worker-1, worker-2, worker-3) through the jumpbox-vm, and configure each of them as Kubernetes nodes.</p><p>Go back to the previous step: <strong>⚓️ <em>Configure a Kubernetes Node (Ubuntu)</em>.</strong></p><p>3. Run the kubeadm join command to join the worker node to the cluster.</p><p>⚠️ Not the command to add the master nodes.</p><p>The difference between the kubeadm join command for adding a master node and a worker node is the use of the --control-plane flag and the --certificate-key.<br> When joining a worker node, omit the --control-plane flag and the --certificate-key from your join command.</p><pre>sudo kubeadm join 10.0.2.10:6443 --token fetdv4.ngb7hi792zs4150x \<br>        --discovery-token-ca-cert-hash sha256:3c25328284a8436a959c51df6942a6c8c9660f0eaba77a3044f8bbb3c841a65c </pre><p>Now, if you SSH into a master node,<br> you’ll be able to confirm the availability of the connected worker nodes by running kubectl get nodes.</p><p>4. Repeat the same steps for the remaining two worker nodes.</p><h3>🔴 Accessing the Cluster</h3><p>After connecting all the nodes, you’ve successfully completed the cluster setup. You now have a stacked etcd cluster with 3 master nodes and 3 worker nodes.<br> The next step is figuring out how to securely connect to the cluster from your local machine.</p><h4>How do you connect to your cluster from your local computer?</h4><ol><li>First, SSH into the jumpbox-vm and install kubectl on it.</li></ol><pre>curl -LO &quot;https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl&quot;<br>sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl</pre><p>2.Since we’re using the same private key to SSH into both our nodes and the jumpbox-vm, I’m going to copy the private key from the jumpbox-vm to my master-1 node (10.0.1.4).</p><pre>scp -i azureuser azureuser azureuser@10.0.2.4:/home/azureuser</pre><p><strong>3. ssh into the </strong><strong>master-1 node.</strong></p><p>📌 Once you’ve SSH’ed into master-1, you can run the ls command to see the azureuser private key you copied in the previous step.</p><p>Copy the kubeconfig file to the<strong> jumpbox VM</strong> (10.0.1.4).</p><pre>sudo scp -i azureuser /etc/kubernetes/admin.conf azureuser@10.0.1.4:/home/azureuser</pre><p>4. Return to the <strong>jumpbox VM</strong> and create the kubeconfig by using the copied admin.conf file from master-1.</p><pre>mkdir -p ~/.kube<br>mv ~/admin.conf ~/.kube/config</pre><p>Then verify by running.</p><pre>kubectl get nodes</pre><p>If you see the output listing your nodes, it means you’ve successfully connected to the cluster.</p><p>5. Connect from you local machine using kube-proxy</p><p>🧩 What is kubectl proxy?</p><p>kubectl proxy sets up a <strong>local HTTP proxy server</strong> that forwards requests to the Kubernetes API server using your kubeconfig credentials. It&#39;s <strong>authenticated and secure</strong>, and only listens locally unless configured otherwise.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N1KcKauAiNjwi2_xZkdMiQ.png" /><figcaption>kubectl proxy</figcaption></figure><ol><li>First copy the kubeconfig from your jumpbox-vm to the local machine.</li></ol><p>copy the content of the below config in your jumpbox-vm and create a new config file in your local machine.</p><pre>cat ~/.kube/config</pre><p>2. update the config in your local machine.</p><p>You should update the server address in your config file to http://localhost:38001. (You can choose any port you prefer; here, I’ve chosen port 38001.)</p><pre>...<br>server: http://localhost:38001<br>...</pre><p>3. ssh into the jumpbox-vm using the following command.</p><pre>ssh -i azureuser -L 38001:127.0.0.1:8001 azureuser@&lt;jumpbox-ip-address&gt;</pre><ul><li>-i azureuser : specifies the <strong>SSH private key </strong>used for authentication.</li><li>-L 38001:127.0.0.1:8001 : sets up the local port forwading, 38001 on your local machine and 127.0.0.1:8001 on your remote machine (in this case, <strong>jumpbox-vm)</strong></li><li>azureuser@&lt;jumpbox-ip-address&gt; : <strong>remote user</strong> (azureuser) and the <strong>public IP address or DNS name</strong> of the <strong>jumpbox</strong></li></ul><p>4. start the proxy on your jumpbox-vm with the following command. (Flag --port is optional here because <strong>port 8001</strong> is the default.)</p><pre>kubectl proxy --port=8001</pre><p>It will start the kube proxy server.</p><p>5. In your local terminal.</p><pre>export KUBECONFIG=&lt;path-to-your-updated-config&gt;<br>kubectl get nodes</pre><p>As you can see, you are now able to access your Kubernetes cluster from your local machine.<br>You can also use tools like <strong>K8sLens</strong> by adding your updated kubeconfig file from your local machine to manage the cluster easily.</p><blockquote><strong><em>If you’ve enjoyed this article, don’t forget to give it a 👏. Your support gives me more energy and enthusiasm to write more articles like this. <br>Thank you!</em></strong></blockquote><blockquote>Happy Coding!!! 🐧</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4f2f2f7f099f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[OAUTH2 Proxy Authentication flow — Part 2]]></title>
            <link>https://medium.com/@kesaralive/oauth2-proxy-authentication-flow-part-2-799b90f98a15?source=rss-d60194e755a2------2</link>
            <guid isPermaLink="false">https://medium.com/p/799b90f98a15</guid>
            <category><![CDATA[preflight-request]]></category>
            <category><![CDATA[cors]]></category>
            <category><![CDATA[oauth2-proxy]]></category>
            <category><![CDATA[oauth2]]></category>
            <category><![CDATA[microservices]]></category>
            <dc:creator><![CDATA[Kesara Karannagoda]]></dc:creator>
            <pubDate>Fri, 26 Jul 2024 18:57:47 GMT</pubDate>
            <atom:updated>2024-07-26T18:57:47.308Z</atom:updated>
            <content:encoded><![CDATA[<h3>OAUTH2 Proxy Authentication flow — Part 2</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7KTqVVq_Wy6Qios-gFTdxQ.png" /></figure><p>If you haven’t read the first part of this article, feel free to check it out here:<br><a href="https://medium.com/devops-dev/securing-microservice-apis-with-oauth2-proxy-a-complete-project-71fabc79147d">https://medium.com/devops-dev/securing-microservice-apis-with-oauth2-proxy-a-complete-project-71fabc79147d</a></p><p>This article includes,</p><ul><li>More about OAuth2 flow.</li><li>CSRF Cookie.</li><li>CORS and Preflight requests.</li><li>OAuth2 proxy cookie refresh, and more options.</li></ul><h3>Our Current Setup and the Auth flow</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SaOqJGXbBuhPMHS5aHkz1w.gif" /></figure><h4>OAuth2 Proxy authentication flow</h4><ol><li><strong>User Request Access: </strong>The user tries to access a protected resource (todo-api) without being authenticated. Our nginx server (reverse proxy) redirects the user to the OAuth2 proxy.</li><li><strong>OAuth2 Proxy checks for </strong><strong>_oauth2_proxy Cookie:</strong> The OAuth2 proxy checks if the user has a valid _oauth2_proxy cookie, which indicates that the user is already authenticated.</li><li><strong>No </strong><strong>_oauth2_proxy cookie:</strong> If the _oauth2_proxy cookie is not present or is invalid, the OAuth2 proxy redirects the user to the configured authentication (Google).</li><li><strong>User Authenticates:</strong> The user logs in to the authentication provider and grants permission to the application to access their information.</li><li><strong>Authentication Provider Sends Authorization:</strong> After successful authentication, the authentication provider (Google) sends an authorization token or code back to the OAuth2 proxy.</li><li><strong>OAuth2 Proxy Grants Access:</strong> The OAuth2 proxy uses the authorization token to validate the user’s identity and grants access by setting a valid _oauth2_proxy cookie in the user’s browser.</li><li><strong>User Accesses Protected Resource (todo-api):</strong> With the valid _oauth2_proxy cookie, the user is redirected back to the original application and can access the protected resource.</li><li><strong>Access Granted:</strong> If the _oauth2_proxy cookie is valid, the OAuth2 proxy allows the user to access the protected resource without requiring re-authentication.</li></ol><h3>Let’s break this down to four steps.</h3><p>we have the normal authentication flow when using OAuth2 proxy with cookie-based authentication.</p><p>Let’s discuss how this is applied in our project by breaking it down into four steps.</p><h4><strong>Step 01: OAuth2 proxy checks for </strong><strong>_oauth2_proxy cookie</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ewXM1sFFKLcmvECU1Jee9w.png" /><figcaption>OAuth2 proxy checks for _oauth2_proxy cookie</figcaption></figure><p>When a user requests a protected resource, it goes through an authentication check at OAuth2 proxy.</p><p>As you might recall from the <a href="https://medium.com/devops-dev/securing-microservice-apis-with-oauth2-proxy-a-complete-project-71fabc79147d">previous story</a>, this is done using the auth_request directive in our nginx.conf .</p><p>OAuth2 proxy verifies whether the cookie is present and valid before granting access. If the check fails, the user is redirected to the configured authentication provider, which in our case is <strong>Google OAuth2</strong>.</p><h4>Step 02: OAuth2 proxy authentication flow starts</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*f5Dt6McR51JkSF33TgO1Bw.png" /><figcaption>Invoking OAuth2 proxy authentication flow</figcaption></figure><p>In our setup, we initiate the OAuth2_proxy authentication process by invoking the /oauth2/start API endpoint. This is the standard starting endpoint provided by OAuth2 proxy.</p><p>By doing that, a<strong> CSRF cookie</strong> is set on the browser, and user is redirected to the configured authentication provider, which in our case is Google.</p><p>The user can then log in through the provider’s login page and will be redirected back to the application. If you have followed the <a href="https://medium.com/devops-dev/securing-microservice-apis-with-oauth2-proxy-a-complete-project-71fabc79147d">previous story</a>, you should have already experienced this.</p><h3><strong>CSRF Cookie?</strong></h3><p>OAuth2 proxy uses a CSRF (Cross-Site Request Forgery) cookie as a security measure to protect against <strong>CSRF attacks</strong>. When a user initiates the OAuth2 authentication flow by accessing the /oauth2/startendpoint, OAuth2 proxy generates a CSRF token and sets it as a cookie in the user’s browser.</p><p>When the user is redirected to the authentication provider (e.g., Google) to log in, the CSRF token is included in the request. After the authentication provider validates the user’s credentials, it redirects the user back to OAuth2 proxy along with the CSRF token.</p><p>OAuth2 proxy then verifies that the CSRF token received from the authentication provider matches the one set in the user’s browser. This verification ensures that the request is coming from the same user who initiated the authentication flow, helping to prevent CSRF attacks where a malicious website tries to trick a user into performing actions on another website without their consent.</p><p>You can read more about CSRF prevention <a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html"><strong>here</strong></a>.</p><h4>Step 03: Sets _oauth2_proxy cookie and redirects user</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dE4YkoPngpy_E_eQl4g-lw.png" /><figcaption>Sets _oauth2_proxy cookie and redirects back to the application</figcaption></figure><p>After logging in, you’ll be redirected to the redirect URL configured in the oauth2_proxy variables.</p><p>If you recall from the <a href="https://blog.devops.dev/securing-microservice-apis-with-oauth2-proxy-a-complete-project-71fabc79147d">previous story</a>, we authorized that redirect URI when creating the Google OAuth2 consent screen and credentials.</p><p>OAuth2 proxy then validates whether the authentication was successful. If successful, it sets a valid _oauth2_proxy cookie in the user&#39;s browser and removes the _oauth2_proxy_csrf cookie.</p><h4>Step 04: Sends a request with a valid _oauth2_proxy cookie</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hVULrTB_7HV0ileVZuo8jw.png" /><figcaption>Sending a request with the valid _oauth2_proxy cookie</figcaption></figure><p>Finally, the user will be able to access protected API endpoints using the valid _oauth2_proxy cookie. This cookie can be included in the request header to access any protected microservice authenticated by oauth2_proxy.</p><p>When the authentication process is repeated with a valid cookie, invoking the auth_request directive (/oauth2/auth) in the nginx configuration, <strong>OAuth2 Proxy</strong> will send a 202 Accepted response to the client and grant access to protected micro-services.</p><h3>OAuth2 Proxy configuration options</h3><p>Now you should have a better understand about the oauth2 proxy auth flow in our project. Let’s talk about our oauth2 proxy configuration in the docker-compose.yml file.</p><pre>  oauth2-proxy:<br>    container_name: oauth2-proxy<br>    image: quay.io/oauth2-proxy/oauth2-proxy:latest<br>    environment:<br>      - OAUTH2_PROXY_CLIENT_ID=&lt;your-client-id&gt;<br>      - OAUTH2_PROXY_CLIENT_SECRET=&lt;your-client-secret&gt;<br>      - OAUTH2_PROXY_COOKIE_SECRET=&lt;your-cookie-secret&gt;<br>      - OAUTH2_PROXY_EMAIL_DOMAINS=*<br>      - OAUTH2_PROXY_REVERSE_PROXY=true<br>      - OAUTH2_PROXY_REDIRECT_URL=http://localhost/oauth2/callback<br>      - OAUTH2_PROXY_COOKIE_SECURE=false<br>      - OAUTH2_PROXY_UPSTREAM=http://todo-api:8000<br>      - OAUTH2_PROXY_HTTP_ADDRESS=http://0.0.0.0:4180<br>      - OAUTH2_PROXY_SET_AUTHORIZATION_HEADER=true<br>      - OAUTH2_PROXY_SET_XAUTHREQUEST=true<br>      - OAUTH2_PROXY_WHITELIST_DOMAINS=.localhost:3000<br>    command:<br>      - --http-address=0.0.0.0:4180<br>      - --upstream=http://todo-api:8000<br>      - --skip-provider-button=true<br>      - --skip-auth-preflight=true<br>    networks:<br>      - microservices</pre><ol><li>As I mentioned in the previous story, <strong>OAUTH2_PROXY_CLIENT_ID</strong> and <strong>OAUTH2_PROXY_CLIENT_SECRET</strong> env variables are used to connect Auth provider to the OAuth2 proxy.</li><li><strong>OAUTH2_PROXY_COOKIE_SECRET</strong> is the secret key used to encrypt and sign cookies in our authentication flow. It should be unique and kept secret from the outside world. If you recall from the previous story, you should remember that we generated a cookie secret using the Linux terminal or Windows Power-Shell.</li><li><strong>OAUTH2_PROXY_EMAIL_DOMAINS</strong> specifies the allowed email domains for authentication. Using <strong>*</strong> allows any email domain.</li><li><strong>OAUTH2_PROXY_REVERSE_PROXY </strong>indicates that OAuth2 proxy is running behind a reverse proxy (in our case its nginx) and should use headers provided by the reverse proxy for client information.</li><li><strong>OAUTH2_PROXY_REDIRECT_URL </strong>is the URL to which the OAuth provider will redirect the user after authentication. (<strong>Step 03</strong>)</li><li>Certainly! Here’s a revised version:</li><li><strong>OAUTH2_PROXY_COOKIE_SECURE</strong> determines whether the cookie is sent only over HTTPS. In our project, we’ve set this to false because we’re using our local network and want to avoid complicating things by implementing self-signed certificates.</li><li><strong>OAUTH2_PROXY_UPSTREAM </strong>specifies the upstream server to proxy requests to after authentication. it’s like telling OAuth2 Proxy, “After you’ve checked that the user is authenticated, send their request to this server.”</li><li><strong>OAUTH2_PROXY_HTTP_ADDRESS</strong> The HTTP address on which OAuth2 proxy should listen for incoming requests.</li><li><strong>OAUTH2_PROXY_SET_AUTHORIZATION_HEADER </strong>If you’ve set this to <strong>true</strong>, OAuth2 proxy will set the Authorization header with the user’s access token for upstream requests.</li><li><strong>OAUTH2_PROXY_SET_XAUTHREQUEST </strong>If you’ve set this to <strong>true</strong>, OAuth2 proxy will add a X-Auth-Request-Email header with the user’s email address for upstream requests.</li><li><strong>OAUTH2_PROXY_WHITELIST_DOMAINS </strong>This specifies the allowed domains for the OAuth2 proxy. Requests from domains not in this list will be rejected. (This is <strong>important</strong> because your redirect requests will also rejected if the relavant domain is not in this list.) <br>Ex: If we didn’t specify .localhost:3000 in this list. when we’ve invoked http:localhost/oauth2/auth/start?<strong>rd=http%3A%2F%2Flocalhost%3A3000</strong> , OAuth2 proxy will reject the redirection to http://localhost:3000 after authentication.</li><li><strong>--skip-provider-button </strong>This option tells OAuth2 Proxy not to show the provider selection page. If you have only one OAuth provider configured, this page is skipped, and users are directly sent to the authentication provider’s login page.</li><li><strong>--skip-auth-preflight </strong>This option skips the authentication preflight check. Normally, OAuth2 Proxy checks if the user is already authenticated before processing a request. This check helps to avoid unnecessary redirects. However, setting this option to <strong>true</strong> skips this check, which might slightly reduce latency but could potentially lead to more redirects for authentication.</li></ol><h3>CORS and Preflight Requests</h3><h4><strong>What is a preflight request?</strong></h4><p>Preflight request is a <strong>CORS request</strong> that checks to see if the CORS protocol is understood and a server is aware using specific methods and headers.</p><p>It is an <strong>OPTIONS</strong> request, using two or three HTTP request headers: Access-Control-Request-Method, Origin, and optionally Access-Control-Request-Headers.</p><h4><strong>What is CORS?</strong></h4><p><strong>CORS (Cross-Origin Resource Sharing) </strong>is a way for web servers to tell browsers that they’re okay with allowing certain types of requests from other websites.</p><p>Normally, browsers prevent websites from accessing resources on different domains for security reasons, but <strong>CORS allows servers to override this restriction</strong>. It does this by transmitting special HTTP headers that give browsers permission to allow these cross-origin requests.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/905/1*ElzWwJOrYYdsnxQVTa7vhw.jpeg" /></figure><p>The basic request flow of a cross-origin request involves two steps: The main request and the preflight request.</p><ul><li><strong>Main Request:</strong> The browser sends the main request (GET or POST request) from the frontend Javascript code to the server on a different domain. This request includes the relevant CORS headers, such as Origin, to indicate the origin of the request.</li><li><strong>Preflight Request:</strong> Before sending the actual request, the browser may send a preflight request (using the OPTIONS method) to the CORS headers and asks the server if the main request is allowed based on the CORS policy set by the server.</li></ul><p>If the preflight request is successful and the server responds with the appropriate CORS headers indicating that the main request is allowed, the browser will proceed to send the main request.</p><p>Otherwise, the browser will block the main request, and the frontend code will not able to access the server’s resources.</p><h4><strong>!Important</strong></h4><p>Some requests don’t trigger a CORS preflight request. Those are called <strong>simple requests.</strong></p><p>That’s why some requests were able to succeed to the backend server without setting <strong>--skip-auth-preflight=true</strong>. Those requests were able to proceed without a problem because they didn’t have a preflight request at all.</p><h4>What happened to the other requests?</h4><p>If you’ve ran the docker compose up -d without setting <strong>--skip-auth-preflight=true</strong> (You can set it to false or comment it), you’ll see that some <strong>simple requests</strong> are handled successfully while some are not.</p><p>In order to be a simple request, it should meet all the conditions listed <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests"><strong>here.</strong></a><strong> </strong>and<strong> </strong>It’s decided by the browser.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*z1r0x3mHhnB9TjdveZedyA.png" /></figure><p>For example, if you try to edit a todo, the request may fail with a CORS error due to a failed preflight request.</p><p>If you examine the preflight request closely, you’ll notice that the <strong>_oauth2_cookie</strong> is missing, even though it exists in the browser and we’ve included the credentials in our fetch request.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FL1V7v0QP_NmfJ4B1UNh_g.png" /></figure><h4>Why?</h4><p><strong>CORS preflight requests must never include credentials.</strong></p><p>Since <strong>preflight requests are handled by the browser</strong>, they don’t include credentials by default. Therefore, we have to set <strong>--skip-auth-preflight=true</strong> to process these types of requests in OAuth2 proxy. Otherwise, OAuth2 proxy will attempt to authenticate preflight requests and will cause the request to fail.</p><p>Since we’re dealing with requests from different origins using <strong>CORS</strong>, this step is <strong>necessary</strong> when the origin of the request is different from the origin of the resource. In our case we have to do this because we’re requesting from the <a href="http://localhost:3000/">http://localhost:3000</a> and the resource origin is <a href="http://localhost/">http://localhost/</a>.</p><p>You might be wondering why it’s failing even though we’re on the same network and using the name “localhost.” That’s because <strong>CORS also restricts sharing resources with different ports.</strong> Even if the origin name is the same, if the port is different, <strong>CORS</strong> won’t allow sharing resources. That’s the standard behavior of the <strong>CORS</strong>.</p><p>You can learn more about preflight requests and requests with credentials <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#requests_with_credentials"><strong>here</strong></a>.</p><h3>Cookie expiry and Cookie refresh</h3><p>Now you have a better understanding of CORS, preflight requests, and the OAuth 2 authentication flow with OAuth2 proxy.</p><p>If you recall the last story, you should now remember how we handled login/logout in the Next.js middleware and started the auth flow by invoking the/oauth2/start endpoint.</p><p>You might be wondering what will happen if the cookie becomes invalid or expires.</p><p>Since we haven’t configured any cookie expiry settings in our current setup, we can’t clearly see what’s happening.</p><p>Let’s simulate what happens when the cookie expires by updating the docker compose configuration with <strong>--cookie-expire=15s</strong>, as shown in the following snippet.</p><pre>oauth2-proxy:<br>    container_name: oauth2-proxy<br>    image: quay.io/oauth2-proxy/oauth2-proxy:latest<br>    environment:<br>      - OAUTH2_PROXY_CLIENT_ID=&lt;your-client-id&gt;<br>      - OAUTH2_PROXY_CLIENT_SECRET=&lt;your-client-secret&gt;<br>      - OAUTH2_PROXY_COOKIE_SECRET=&lt;your-cookie-secret&gt;<br>      - OAUTH2_PROXY_EMAIL_DOMAINS=*<br>      - OAUTH2_PROXY_REVERSE_PROXY=true<br>      - OAUTH2_PROXY_REDIRECT_URL=http://localhost/oauth2/callback<br>      - OAUTH2_PROXY_COOKIE_SECURE=false<br>      - OAUTH2_PROXY_UPSTREAM=http://todo-api:8000<br>      - OAUTH2_PROXY_HTTP_ADDRESS=http://0.0.0.0:4180<br>      - OAUTH2_PROXY_SET_AUTHORIZATION_HEADER=true<br>      - OAUTH2_PROXY_SET_XAUTHREQUEST=true<br>      - OAUTH2_PROXY_WHITELIST_DOMAINS=.localhost:3000<br>    command:<br>      - --http-address=0.0.0.0:4180<br>      - --upstream=http://todo-api:8000<br>      - --skip-provider-button=true<br>      - --skip-auth-preflight=true<br>      - --cookie-expire=15s <br>    networks:<br>      - microservices</pre><p>Now, sign out from the current session and try to sign in again while opening the network tab.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ime3zVuYkffjMfLaEyZA9g.png" /></figure><p>After logging in, try to interact with todos while monitoring the request status.</p><p>After 15 seconds, you’ll see that your request is getting a CORS error. That’s because we’ve set the cookie to expire in 15 seconds.</p><p>Now, if you check for cookies in your browser, you’ll see that the <strong>oauth2_proxy</strong> cookie is unset from the browser and replaced with the <strong>_oauth2_proxy_csrf</strong> cookie.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*onx5jpH5N9GmPhG3AkaGww.png" /></figure><p>This happens because when the <strong>_oauth2_proxy</strong> cookie expires, OAuth2 proxy responds by removing the <strong>_oauth2_proxy</strong> cookie in our browser and setting the <strong>_oauth2_proxy_csrf</strong> cookie to redirect us to the Auth Provider to relogin.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rGIrp7TG6JoAf8cYouUsDA.png" /></figure><p>You’ll see that in the above figure. Even if the request has the <strong>_oauth2_proxy</strong> cookie credential with it, since it’s an invalid cookie, OAuth2 proxy responds by removing it and setting the <strong>_oauth2_proxy_csrf</strong> cookie to redirect us for the login with the Auth provider.</p><p>If you try to refresh the page now, you’ll be redirected to the login because the cookie is not present in your browser. We’ve handled this in our Next.js middleware.</p><p>In the current implementation, even if the user is actively engaged with our todo frontend application, they may be logged out due to cookie expiration. This could be problematic in scenarios such as online examination systems or similar cases.</p><h4><strong>We can handle these kind of scenarios by using cookie refresh.</strong></h4><p>We can address this issue by using a cookie refresh mechanism. <br>For example, if we set --cookie-expire=15s, we can set --cookie-refresh=10s in our docker-compose.yml configuration.</p><pre><br>oauth2-proxy:<br>    container_name: oauth2-proxy<br>    image: quay.io/oauth2-proxy/oauth2-proxy:latest<br>    environment:<br>      - OAUTH2_PROXY_CLIENT_ID=&lt;your-client-id&gt;<br>      - OAUTH2_PROXY_CLIENT_SECRET=&lt;your-client-secret&gt;<br>      - OAUTH2_PROXY_COOKIE_SECRET=&lt;your-cookie-secret&gt;<br>      - OAUTH2_PROXY_EMAIL_DOMAINS=*<br>      - OAUTH2_PROXY_REVERSE_PROXY=true<br>      - OAUTH2_PROXY_REDIRECT_URL=http://localhost/oauth2/callback<br>      - OAUTH2_PROXY_COOKIE_SECURE=false<br>      - OAUTH2_PROXY_UPSTREAM=http://todo-api:8000<br>      - OAUTH2_PROXY_HTTP_ADDRESS=http://0.0.0.0:4180<br>      - OAUTH2_PROXY_SET_AUTHORIZATION_HEADER=true<br>      - OAUTH2_PROXY_SET_XAUTHREQUEST=true<br>      - OAUTH2_PROXY_WHITELIST_DOMAINS=.localhost:3000<br>    command:<br>      - --http-address=0.0.0.0:4180<br>      - --upstream=http://todo-api:8000<br>      - --skip-provider-button=true<br>      - --skip-auth-preflight=true<br>      - --cookie-expire=15s <br>      - --cookie-refresh=10s<br>    networks:<br>      - microservices</pre><p>This setup will refresh the OAuth2 cookie every 10 seconds if the user is actively making requests to the OAuth2-protected API.</p><p>However, if the user remains idle during the refresh interval (15–10 = 5 seconds) or for a total of 15 seconds, the OAuth2 cookie will expire, and the application will prompt the user to re-login.</p><p>By implementing cookie refresh, we can mitigate such problematic situations and ensure a smoother user experience.</p><h4>Message to the audience</h4><p>It took me a while to finish Part 2 of the OAuth2 Proxy article due to personal commitments. I focused on the main points of OAuth2 Proxy for now and left out details about the FastAPI and Todo frontend. I’ll be covering improvements to the Next.js Todo frontend and FastAPI microservice in future posts.</p><h3>Thank you. ☕</h3><p>Thank you! If you’ve made it this far, well done.</p><blockquote><em>If you’ve enjoyed the project, don’t forget to give it a 👏. Your support gives me more energy and enthusiasm to write more projects like this. Thank you!</em></blockquote><p>Happy Coding!!! 🐧</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=799b90f98a15" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Securing Microservice APIs with OAuth2 Proxy: A Complete Project]]></title>
            <link>https://blog.devops.dev/securing-microservice-apis-with-oauth2-proxy-a-complete-project-71fabc79147d?source=rss-d60194e755a2------2</link>
            <guid isPermaLink="false">https://medium.com/p/71fabc79147d</guid>
            <category><![CDATA[nginx]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[microservices]]></category>
            <category><![CDATA[oauth2]]></category>
            <category><![CDATA[nextjs]]></category>
            <dc:creator><![CDATA[Kesara Karannagoda]]></dc:creator>
            <pubDate>Tue, 16 Apr 2024 19:45:34 GMT</pubDate>
            <atom:updated>2024-04-19T17:01:42.040Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wcneGZJTEpANBOLZN5GJUw.png" /></figure><h3>Overview</h3><p>In this hands-on project, we will discuss how to build &amp; secure microservice APIs using OAuth2 Proxy behind a reverse proxy. We will implement a microservice API, a frontend application, a reverse proxy, and OAuth2 Proxy. This setup will add an additional layer of security to our microservices, ensuring that only authenticated and authorized users can access our APIs.</p><h4>Introduction</h4><p>We’re going to implement the following architecture on your local machine to learn how to accomplish this task.</p><blockquote><strong>Note:</strong> If you have a strong understanding of APIs and have your own service ready, you can skip the first two parts of this project and proceed directly to the third part.</blockquote><figure><img alt="Architecture" src="https://cdn-images-1.medium.com/max/1024/1*nhefZtJtP3_LhMKLueUzMg.gif" /><figcaption>Project Architecture</figcaption></figure><p>As I mentioned in the overview section,</p><ol><li>First, we’ll establish a microservice API using FastAPI to test this architecture. (I’ll provide a pre-coded solution for you.)</li><li>Next, we’ll configure a frontend application using Next.js to interact with our FastAPI service.</li><li>Following that, we’ll implement a reverse proxy between the frontend app and the microservice we’ve created using Nginx.</li><li>Lastly, we’ll set up OAuth2 Proxy to secure the FastAPI service we’ve developed.</li></ol><h4>The Goal</h4><blockquote>The goal of this project is to understand the workings of a modern, secure, and scalable web application architecture and to gain a thorough understanding of the OAuth2 authentication flow.</blockquote><h4>Prerequisites</h4><p>A good internet connection.</p><p>You need to have docker installed in your computer. If not you can follow the official documentation to download docker. <a href="https://docs.docker.com/engine/install/">https://docs.docker.com/engine/install/</a></p><p>It’s nice to have a solid grasp of Docker and Docker networking, as I’ll be providing you with a pre-coded API service and a frontend application to work with. However, it’s <strong>not mandatory</strong>, as I’ll explain what would be necessary for this project.</p><p>If you need any help while working on this project, you can refer to my Docker networking project to understand concepts like ‘Host Network’ and ‘Bridge Network’. Feel free to leave a comment below if you have any questions.</p><p><strong>Docker networking:</strong> <a href="https://medium.com/@kesaralive/diving-deeper-into-docker-networking-with-docker-compose-737e3b8a3c8c">https://medium.com/@kesaralive/diving-deeper-into-docker-networking-with-docker-compose-737e3b8a3c8c</a></p><p>With these in place, you should be ready to start working on the project.</p><h4>Before Starting,</h4><p>This will be a long project with many steps to complete. If you find anything difficult to understand, please leave a comment in the article. I’ll do my best to help you out. If you get stuck at any point, don’t hesitate to reach out to me. I’m here to help. ✌️</p><h3>Project Architecture</h3><figure><img alt="Architecture" src="https://cdn-images-1.medium.com/max/1024/1*nhefZtJtP3_LhMKLueUzMg.gif" /><figcaption>Project Architecture</figcaption></figure><h4>OAuth2 Proxy</h4><p>OAuth2 Proxy is a reverse proxy and static file server that provides authentication using OAuth2. You’ll configure OAuth2 Proxy to secure access to your FastAPI service. This will add an extra layer of security by requiring users to authenticate before accessing the API.</p><h4>Identity Server</h4><p>You’ll configure OAuth2 Proxy to use Google OAuth 2.0 as the Auth Provider. This means that when a user tries to access your FastAPI service, they will be redirected to Google’s authentication service to log in. Once authenticated, Google will issue an access token that allows the user to access your API.</p><h4>Microservices</h4><p>Microservices are small, autonomous services that work together. They’re like individual pieces of a puzzle that work together to create a bigger picture. Each piece (Microservice) has its own job to do and can be developed, deployed, and scaled independently, making the overall system more flexible and easier to manage.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Sz49gmuIVD3_zgrnXupl-g.gif" /><figcaption>A microservice (todo service)</figcaption></figure><p>Today, we’re going to create one piece of a puzzle (a microservice) to understand how we can secure this puzzle using OAuth2 Proxy. We’ll create a very simple Todo service API using FastAPI and MongoDB to represent a piece of this puzzle.</p><h4>Reverse Proxy/ API Gateway</h4><p>A reverse proxy/ API gateway acts as an intermediary between cleints and servers. It receives requests from clients and forwards them to appropriate backend servers. Reverse proxies can also perform additional functions like load balancing, caching, SSL termination, and request routing.</p><p>We’re going to use Nginx as our reverse proxy/ API gateway for this project. Nginx is known for its high performance, stability, and low resource consumption. It’s commonly used as a reverse proxy and API gateway due to its ability to handle a large number of concurrent connections efficiently.</p><h4>Front-end App / Client</h4><p>We’ll use a Todo client application implemented using Next.js for this project. It’s a simple one-page frontend application that can be used to create, read, delete, and update todos.</p><h3>FastAPI Todo-service — Step 01</h3><p>Create a new folder named oauth2-project and an docker-compose.yml file inside it. I&#39;m going to use the name oauth2-project folder in the rest of the project. It&#39;s better if you can use the same name as mine.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/590/1*VhJxMFAQsnp7gro2QSVjzw.png" /><figcaption>after creating ‘docker-compose.yml’</figcaption></figure><p>Then clone the <strong>fastapi-todo-service</strong> into that folder using following command:</p><pre>git clone https://github.com/kesaralive-medium/fastapi-todo-service.git </pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/532/1*OsiWtJibjL8kLrBM0Wh2NA.png" /><figcaption>After cloning the fastapi-todo-service</figcaption></figure><p>Then update the docker-compose.yml file with below code snippet.</p><pre>version: &quot;3.8&quot;<br>services:<br>  mongodb:<br>    image: mongo:latest<br>    container_name: mongodb<br>    volumes:<br>      - mongodb_data:/data/db<br>    environment:<br>      MONGO_INITDB_ROOT_USERNAME: root<br>      MONGO_INITDB_ROOT_PASSWORD: password<br><br>  fastapi-todo-service:<br>    container_name: todo-api<br>    build:<br>      context: ./fastapi-todo-service<br>      dockerfile: Dockerfile<br>    depends_on:<br>      - mongodb<br>    ports:<br>      - 8000:8000<br>    environment:<br>      ATLAS_URI: mongodb://root:password@mongodb:27017<br>      DB_NAME: todoapi<br><br>volumes:<br>  mongodb_data:<br>    driver: local</pre><p>In the above docker-compose.yml configuration, I’ve included a MongoDB container to provide the database service to our FastAPI service. If you have a MongoDB account and a free tier cluster, you can use your MongoDB Atlas URI and a preferred database name as you wish. (In that case you can remove the <strong>mongodb</strong> service from docker-compose.yml configuration.)</p><p>If you’re going to use your mongodb cloud atlas uri docker-compose.yml will look like this,</p><pre>version: &quot;3.8&quot;<br>services:<br>  fastapi-todo-service:<br>    container_name: todo-api<br>    build:<br>      context: ./fastapi-todo-service<br>      dockerfile: Dockerfile<br>    ports:<br>      - 8000:8000<br>    environment:<br>      ATLAS_URI: &lt;your-mongodb-atlas-uri&gt;<br>      DB_NAME: todoapi</pre><p>Once you’ve selected your way, you can run the following command to up and run the todo-service.</p><pre>docker compose up -d</pre><p>If you’ve done everything correctly so far, you should be able to see the docs page of the FastAPI Todo service by accessing the <a href="http://localhost:8000/docs">http://localhost:8000/docs</a> URL from your browser.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XJw99bUJmkOf1DVjyl3wRg.png" /><figcaption>fastapi-todo-service</figcaption></figure><p>You will see 6 API endpoints related to the Todo service and one API endpoint to get the headers of our request. (This particular endpoint will help us understand the behavior of our Auth flow later.)</p><p>You can try to create a todo using the POST / Create Todo API endpoint and retrieve it using the GET / Get Todos API endpoint to make sure everything is working fine.</p><p>You can simply press the <strong>“Try it out”</strong> button, enter your todo in the request body, and then <strong>Execute</strong> the request.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3Rg-4Cmt3iRR8kEHkW6dQg.png" /><figcaption>creating the todo request POST /</figcaption></figure><p>If you’ve set up your database URI correctly, you should be able to retrieve the created todo using the GET / Get Todos endpoint. The response body will be shown below after executing the request, as shown in the image below.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eRljK-oJXPYMRbUaEo5efQ.png" /><figcaption>retrieving todos using GET /</figcaption></figure><p>Try to access the /headers endpoint from your browser.</p><p><a href="http://localhost:8000/headers">http://localhost:8000/headers</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GixhF9rcCxNaMatLsI4TKg.png" /><figcaption>request headers at the fastapi-todo-service end.</figcaption></figure><p>Keep the above step in mind; we’re going to compare these headers every time we integrate something into this project.</p><p>You can use the following command to stop the running containers.</p><pre>docker compose down</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/758/1*B4EpLwnw646kEC8XF1d3Uw.png" /><figcaption>docker compose down</figcaption></figure><p>If you have followed the instructions correctly, you have completed the following part of our project architecture.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Sz49gmuIVD3_zgrnXupl-g.gif" /><figcaption>fastapi-todo-service (Microservice)</figcaption></figure><h4><strong>Before proceeding to the next step, I’ll share some important details about Docker Compose with you:</strong></h4><ul><li><strong>docker compose up</strong> — Use this command to up and run all the containers defined in the docker-compose.yml file.</li><li><strong>docker compose up -d</strong> — This command starts the containers in the background (detached mode).</li><li><strong>docker compose down</strong> — Stops and removes all containers, networks, and volumes created by docker compose up.</li><li><strong>dockse compose build &lt;SERVICE&gt;</strong> —Builds or rebuilds a specific service defined in the docker-compose.yml file. Replace &lt;SERVICE&gt; with the name of the service you want to build. <strong>If you’ve done some changes in any service after using </strong>docker compose up<strong>, you must run this command to reflect those changes in the new build.</strong></li></ul><blockquote>You should run all of the above commands only inside the project directory.</blockquote><p><strong>Some helpful docker-compose.yml specification attributes details</strong></p><p>In the docker-compose.yml file of this project, there are three main sections, and under the services section, there will be around 4-5 services. Here are some important attributes you should understand:</p><ul><li><strong>build</strong>: Specifies how to build the Docker image for the service.</li><li><strong>depends_on</strong>: Specifies that the service depends on another service or services. However, it&#39;s important to note that this does not wait for the other service to be &quot;ready&quot; before starting the current service. It simply starts the other service before the current service.</li><li><strong>environment</strong>: This attribute is used to set environment variables required by the service.</li></ul><h3>Front-end App/ Client — Step 02</h3><p>Now, let’s integrate the front-end application into this setup by cloning the next-todo-app repository into the project folder using the following command.</p><pre>git clone https://github.com/kesaralive-medium/next-todo-app.git</pre><p>After cloning the next-todo-app, your directory should resemble the image below.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/657/1*1L8tWkcGLaMUmJjMAnT7KA.png" /><figcaption>After cloning the next-todo-app</figcaption></figure><p>Now, let’s add the frontend configuration to the docker-compose.yml file.</p><pre>version: &quot;3.8&quot;<br>services:<br>  mongodb:<br>    image: mongo:latest<br>    container_name: mongodb<br>    volumes:<br>      - mongodb_data:/data/db<br>    environment:<br>      MONGO_INITDB_ROOT_USERNAME: root<br>      MONGO_INITDB_ROOT_PASSWORD: password<br><br>  fastapi-todo-service:<br>    container_name: todo-api<br>    build:<br>      context: ./fastapi-todo-service<br>      dockerfile: Dockerfile<br>    depends_on:<br>      - mongodb<br>    ports:<br>      - 8000:8000<br>    environment:<br>      ATLAS_URI: mongodb://root:password@mongodb:27017<br>      DB_NAME: todoapi<br><br>  next-todo-app:<br>    container_name: todo-client<br>    build:<br>      context: ./next-todo-app<br>      dockerfile: Dockerfile<br>    ports:<br>      - 3000:3000<br><br><br>volumes:<br>  mongodb_data:<br>    driver: local</pre><p>Again, run the following command to start and run all three containers.</p><pre>docker compose up -d</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/778/1*Jgl2zmYOBS4xzO1VmfeKig.png" /><figcaption>docker compose up -d (detached mode)</figcaption></figure><p>If you’ve integrated the todo-client correctly, you should be able to see the todo frontend application at <a href="http://localhost:3000/">http://localhost:3000</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tpmsMfNz-KxJC0xEhG-UCQ.png" /><figcaption>simple todo service frontend (next-todo-app)</figcaption></figure><p>You will also see the previously created todo in the todo client application. Try creating, updating, and deleting todos. (Press ‘<strong>Enter</strong>’ to add todos after typing.)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nUNejMA3vN3JXAO1SGGK6Q.png" /><figcaption>simple todo service frontend (next-todo-app)</figcaption></figure><p>Now, try accessing the <a href="http://localhost:3000/headers">http://localhost:3000/headers</a> page. You will notice that the headers output is different from when we accessed it from the FastAPI Todo service itself.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vSLw0h15__7ZH2WdS6inaw.png" /><figcaption>/headers from todo-client</figcaption></figure><p>You will see some newly added values and updated values there.</p><pre>&quot;accept&quot;: &quot;*/*&quot;<br>&quot;origin&quot;: &quot;http://localhost:3000&quot;<br>&quot;sec-fetch-site&quot;: &quot;same-site&quot;<br>&quot;sec-fetch-mode&quot;: &quot;cors&quot;<br>&quot;sec-fetch-dest&quot;: &quot;empty&quot;<br>&quot;referer&quot;: &quot;http://localhost:3000/&quot;</pre><p>The headers mentioned above are part of the CORS (Cross-Origin Resource Sharing) mechanism in web browsers. Since I’ve already granted access to any origin at the FastAPI Todo service end, our todo-client can access the todo-service using CORS. You’ll be able to see the code regarding that in the /fastapi-todo-service/app/main.py file.</p><pre>...<br><br>origins = [<br>    &quot;*&quot;<br>]<br><br>app.add_middleware(<br>    CORSMiddleware,<br>    allow_origins=origins,<br>    allow_credentials=True,<br>    allow_methods=[&quot;*&quot;],<br>    allow_headers=[&quot;*&quot;],<br>)<br><br>...</pre><ul><li><strong>“accept”: “*</strong><strong><em>/*</em></strong><strong>”</strong> — Indicates that the client accepts any media type.</li><li><strong>“origin”: “</strong><a href="http://localhost:3000/"><strong>http://localhost:3000</strong></a><strong>&quot;</strong> — Specifies the origin of the request, which is the domain where the todo-client is hosted.</li><li><strong>“sec-fetch-site”: “same-site”</strong> — Indicates that the request is being made from the same site as the todo-client.</li><li><strong>“sec-fetch-mode”: “cors”</strong> — Specifies that the request is a cross-origin request and should be handled using CORS.</li><li><strong>“sec-fetch-dest”: “empty”</strong> — Indicates that the request’s destination is not a specific resource type.</li><li><strong>“referer”: “</strong><a href="http://localhost:3000/"><strong>http://localhost:3000/</strong></a><strong>&quot;</strong>— Specifies the address of the previous web page from which the current request originated.</li></ul><p><strong>MDN web docs</strong> is the best place to learn more about the <strong>HTTP protocol and HTTP headers</strong>.</p><p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept">Accept - HTTP | MDN</a></p><p>If you have followed the instructions correctly, you have completed the following part of our project architecture.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3LXmdVZAFxWdtxtchrcmxg.gif" /><figcaption>todo-client integration</figcaption></figure><h3>Reverse Proxy/ API Gateway — Step 03</h3><p>Now that we have a <strong>todo-client</strong>, <strong>todo-api-service</strong>, and a <strong>database</strong> up and running, let’s integrate Nginx as a reverse proxy between the todo-client and the FastAPI todo-service.</p><p>Create a folder named <strong>todo-api-gateway</strong> inside your project folder. Your directory structure should look like this.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/539/1*6JSYR7UdPJ-1v46GbuMcuA.png" /><figcaption>after creating todo-api-gateway folder</figcaption></figure><p>After that, create a <strong>Dockerfile</strong> and an <strong>nginx.conf</strong> file inside the <strong>todo-api-gateway</strong> folder, as shown in the below snippet.</p><pre>.<br>├── docker-compose.yml<br>├── fastapi-todo-service<br>├── next-todo-app<br>└── todo-api-gateway<br>    ├── Dockerfile<br>    └── nginx.conf</pre><p><strong>Dockerfile</strong></p><pre>FROM nginx:latest<br><br>COPY nginx.conf /etc/nginx/nginx.conf<br><br>EXPOSE 80<br>EXPOSE 443<br><br>CMD [&quot;nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]</pre><p>This Dockerfile is used to build a custom Nginx image for our project. We use <strong>nginx:latest</strong> as the base image for this custom image. Then, we update the default <strong>nginx.conf</strong>with our configuration file.</p><p><strong>nginx.conf</strong></p><pre>events {<br>    worker_connections 1024;<br>}<br><br>http {<br>    server {<br>        listen 80;<br>        server_name localhost;<br><br>        location / {<br>            proxy_pass http://todo-api:8000;<br>        }<br>    }<br>}</pre><p>This nginx.conf configuration sets up Nginx as a reverse proxy to forward requests from port 80 to the todo-api service running on port 8000.</p><ul><li>events: This block defines parameters that affect the Nginx event loop processing, such as the maximum number of connections per worker process.</li><li>http: This block contains configuration for the HTTP protocol.</li><li>server: This block defines a virtual server for handling HTTP requests.</li><li>listen 80: Configures the server to listen on port 80 for incoming HTTP requests.</li><li>server_name localhost;: Sets the server name to localhost</li><li>location /: Defines how Nginx should process requests for the root URL. When you access <a href="http://localhost/">http://localhost/</a>, you will be accessing the logic inside the curly braces {} in this location block.</li><li>proxy_pass http://todo-api:8000;: Forwards requests to http://todo-api:8000, which is the hostname of the todo-api service running on port 8000.</li></ul><p>Now let’s add this nginx reverse-proxy setup to our docker-compose.yml configuration.</p><pre>version: &quot;3.8&quot;<br>services:<br>  mongodb:<br>    image: mongo:latest<br>    container_name: mongodb<br>    volumes:<br>      - mongodb_data:/data/db<br>    environment:<br>      MONGO_INITDB_ROOT_USERNAME: root<br>      MONGO_INITDB_ROOT_PASSWORD: password<br><br>  fastapi-todo-service:<br>    container_name: todo-api<br>    build:<br>      context: ./fastapi-todo-service<br>      dockerfile: Dockerfile<br>    depends_on:<br>      - mongodb<br>    ports:<br>      - 8000:8000<br>    environment:<br>      ATLAS_URI: mongodb://root:password@mongodb:27017<br>      DB_NAME: todoapi<br><br>  next-todo-app:<br>    container_name: todo-client<br>    build:<br>      context: ./next-todo-app<br>      dockerfile: Dockerfile<br>    ports:<br>      - 3000:3000<br><br>  api-gateway:<br>    container_name: nginx<br>    build:<br>      context: ./todo-api-gateway<br>      dockerfile: Dockerfile<br>    depends_on:<br>      - fastapi-todo-service<br>    ports:<br>      - 80:80<br><br>volumes:<br>  mongodb_data:<br>    driver: local</pre><p>Now, you can use the docker compose down command to stop all the running containers, and the docker compose up -d command to run the newly added API gateway in the project.</p><pre>docker compose down</pre><pre>docker compose up -d</pre><p>If you enter the following command, you will see four running containers.</p><pre>docker ps</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Bah4-9EMFEV5gBMVv42ZyQ.png" /><figcaption>docker ps</figcaption></figure><p>In the docker-compose.yml file, we’ve mapped the Nginx service to the host machine’s port 80. Therefore, now you should be able to access it at <a href="http://localhost/">http://localhost/</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ViH_X_jZl89lw-VAKQSGTQ.png" /><figcaption>nginx reverse-proxy (http://localhost/)</figcaption></figure><p>If you have previously added todos, you will be able to see them as well. Similar to before when we accessed <a href="http://localhost:8000/docs">http://localhost:8000/docs</a>, now you should be able to access the API documentation at <a href="http://localhost/docs">http://localhost/docs</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*q7oH0puoLTA4YZH059vMZw.png" /><figcaption>accessing the todo-api through reverse proxy</figcaption></figure><p>Since we can now access the todo-api through the Nginx reverse proxy, let’s update our todo-client to send requests using the reverse proxy.</p><p>Navigate to the file next-todo-app/actions/todo.ts and update the <strong>API_URL</strong> to<a href="http://localhost/%60">http://localhost/</a>. Please note that the <strong>trailing slash</strong> is important, so keep that in mind.</p><p><strong>next-todo-app/actions/todo.ts</strong></p><pre>...<br><br>const API_URL = &quot;http://localhost/&quot;<br><br>...</pre><p>Then, go to the file next-todo-app/app/headers/page.tsx and update the fetch URL inside the useEffect hook as well.</p><p><strong>next-todo-app/app/headers/page.tsx</strong></p><pre>...<br><br>const response = await fetch(&#39;http://localhost/headers&#39;)<br><br>...</pre><p>Since we’ve updated 2 files in the todo-client application, we now need to rebuild it in the Docker Compose. To do that, you can run the following command inside the project folder:</p><pre>docker compose build next-todo-app</pre><p>This command will rebuild the next-todo-app service in the Docker Compose, incorporating the changes you made to the files.</p><pre>docker compose up -d</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-wQDLdAnS_grpyjzCf5JYg.jpeg" /><figcaption>requests through nginx reverse-proxy</figcaption></figure><p>Now, Let’s check requests headers by accessing the <a href="http://localhost:3000/headers">http://localhost:3000/headers</a> page.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VBH2V8mHJ8s1wh-twZvnQQ.png" /><figcaption>returned request headers</figcaption></figure><p>You will see some headers are updated there.</p><pre>&quot;host&quot;: &quot;todo-api:8000&quot;<br>&quot;connection&quot;: &quot;close&quot;</pre><ul><li>“host”: “todo-api:8000” — indicates the hostname and port number of the server to which the request is being sent (todo-api on port 8000). This information is exposed to the client because we forwarded the client requests directly to the todo-api server without any additional configurations. <br><strong>“Revealing internal infrastructure details to the client is not ideal.”<br></strong>In the next step of this project, we will change this behavior.</li><li>“connection”: “close” — indicates that the connection will be closed after the current transaction is completed. This means that the client or server intends to close the TCP connection after sending or receiving the current HTTP message. <br>In an Nginx reverse proxy, this practice can help manage server resources by limiting the number of open connections. It is a part of the connection management strategy to control the lifecycle of connections between clients and servers, helping in the optimization of resource usage and performance improvement.</li></ul><p>If you have followed the instructions correctly, you have completed the following part of our project architecture.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*c09N3lOzlf8jDuD7NTiwLg.gif" /><figcaption>after integrating the frontend app</figcaption></figure><h3>Network Isolation — Step 04</h3><p>In the current setup, you should be able to access both the Nginx reverse proxy service and the todo-api service. However, when using a reverse proxy, there’s no point in giving direct access to the todo-api. First, let’s see how our current setup works by inspecting the docker-compose.yml file.</p><pre>...<br><br>services:<br>  mongodb:<br>    image: mongo:latest<br>    container_name: mongodb<br>    volumes:<br>      - mongodb_data:/data/db<br>    environment:<br>      MONGO_INITDB_ROOT_USERNAME: root<br>      MONGO_INITDB_ROOT_PASSWORD: password<br><br>  fastapi-todo-service:<br>    container_name: todo-api<br>    build:<br>      context: ./fastapi-todo-service<br>      dockerfile: Dockerfile<br>    depends_on:<br>      - mongodb<br>    ports:<br>      - 8000:8000<br>    environment:<br>      ATLAS_URI: mongodb://root:password@mongodb:27017<br>      DB_NAME: todoapi<br><br>  next-todo-app:<br>    container_name: todo-client<br>    build:<br>      context: ./next-todo-app<br>      dockerfile: Dockerfile<br>    ports:<br>      - 3000:3000<br><br>  api-gateway:<br>    container_name: nginx<br>    build:<br>      context: ./todo-api-gateway<br>      dockerfile: Dockerfile<br>    depends_on:<br>      - fastapi-todo-service<br>    ports:<br>      - 80:80<br><br>...</pre><p>Currently, we have 4 services up and running, and all the services are running on the default bridge network created by Docker Compose when we first ran the setup. You can verify this by using the following command.</p><pre>docker network ls</pre><p>You’ll see a bridge network named token-handler-pattern_default. This name may vary depending on your project folder name. If you want to understand docker networking deeply, refer my previous article about docker networking.</p><p>However, if we visualize the current setup in a diagram, it would look like the image below.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/906/1*ucqv_SLFOOcGI6vx9ITv4A.png" /><figcaption>all the containers in token-handler-pattern_default bridge network</figcaption></figure><p>The current setup includes the following mappings and configurations:</p><ul><li>The <strong>todo-client</strong> container’s port 3000 is mapped to the host’s port 3000.</li><li>The <strong>nginx</strong> container’s port 80 is mapped to the host’s port 80.</li><li>The <strong>todo-api</strong> container’s port 8000 is mapped to the host’s port 8000.</li><li>The <strong>todo-api</strong> container accesses the <strong>mongodb</strong> container’s port 27017.</li><li>The <strong>todo-client</strong> accesses <strong>nginx</strong> through the host network using the host’s port 80.</li><li><strong>nginx</strong> proxies requests to the <strong>todo-api</strong> container using port 8000.</li></ul><p>To fix this, we need to remove the unnecessary port mappings and isolate the nginx, todo-api, and mongodb containers to the token-handler-pattern_default network. Here&#39;s how you can do it:</p><pre>version: &quot;3.8&quot;<br>services:<br>  mongodb:<br>    image: mongo:latest<br>    container_name: mongodb<br>    volumes:<br>      - mongodb_data:/data/db<br>    environment:<br>      MONGO_INITDB_ROOT_USERNAME: root<br>      MONGO_INITDB_ROOT_PASSWORD: password<br>    networks:<br>      - microservices<br><br>  fastapi-todo-service:<br>    container_name: todo-api<br>    build:<br>      context: ./fastapi-todo-service<br>      dockerfile: Dockerfile<br>    depends_on:<br>      - mongodb<br>    environment:<br>      ATLAS_URI: mongodb://root:password@mongodb:27017<br>      DB_NAME: todoapi<br>    networks:<br>      - microservices<br><br>  next-todo-app:<br>    container_name: todo-client<br>    build:<br>      context: ./next-todo-app<br>      dockerfile: Dockerfile<br>    ports:<br>      - 3000:3000<br>    networks:<br>      - client-host<br><br>  api-gateway:<br>    container_name: nginx<br>    build:<br>      context: ./todo-api-gateway<br>      dockerfile: Dockerfile<br>    depends_on:<br>      - fastapi-todo-service<br>    ports:<br>      - 80:80<br>    networks:<br>      - microservices<br><br>networks:<br>  microservices:<br>    driver: bridge<br>  client-host:<br>    driver: bridge<br><br>volumes:<br>  mongodb_data:<br>    driver: local</pre><h4>Updates</h4><p>I’ve set up two networks: one for hosting microservices (microservices network) and another for hosting the frontend (client-host).</p><pre>...<br><br>networks:<br>  microservices:<br>    driver: bridge<br>  client-host:<br>    driver: bridge<br><br>...</pre><p>In this setup, the microservices network represents a place to host all your microservices, such as Azure AKS, Azure Container Apps, Google Cloud Run, GKE, or EKS.</p><p>The client-host network, on the other hand, represents a different place to host your frontend, like Vercel, Hostinger, or AWS Amplify.</p><p><strong>These two networks don’t have direct connectivity with each other.</strong></p><ul><li>Removed the unnecessory port mapping of <strong>todo-api</strong> container’s port 8000 to the host’s port 8000.</li><li>Added all the containers to the microservices network except for the todo-client .</li><li>todo-client container is added to the client-host network and its port 3000 is mapped to the host’s port 3000.</li></ul><h4>Diagram</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*v3kzp7VjTujt0xUpRCc1QA.jpeg" /><figcaption>separated bridge networks (user defined)</figcaption></figure><p>Now, run the following command to restart the containers and observe the changes.</p><pre>docker compose up -d</pre><p>You can check these two networks by running the following commands. You’ll see two newly created networks, each prefixed by the project folder name.</p><p>For example, if your project folder is named “oauth2-project”, the networks might be named “oauth2-project_microservices” and “oauth2-project_client-host”.</p><pre>docker network ls</pre><p>You can inspect the network using the following command. This will allow you to see the network configuration for the containers mentioned above.</p><pre>docker network inspect &lt;&lt;network-name&gt;&gt;</pre><p>If you access <a href="http://localhost:3000/headers%60">http://localhost:3000/headers</a>, you’ll still see the host header as “todo-api:8000”. Let’s fix that now.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/903/1*a1YGjpk9_JCzpq9f_Yv7aw.png" /><figcaption>returned request headers</figcaption></figure><p>You can fix this by setting the Host header to the domain of the Nginx API gateway. Add a proxy_set_header directive in the Nginx configuration, like in the snippet below:</p><p><strong>todo-api-gateway/nginx.conf</strong></p><pre>events {<br>    worker_connections 1024;<br>}<br><br>http {<br>    server {<br>        listen 80;<br>        server_name localhost;<br><br>        location / {<br>            proxy_set_header Host       $host;<br>            proxy_pass http://todo-api:8000;<br><br>        }<br>    }<br>}</pre><p>To rebuild the API gateway image and restart the container, you can use the following commands:</p><pre>docker compose build api-gateway</pre><pre>docker compose up -d</pre><p>If you access <a href="http://localhost:3000/headers%60">http://localhost:3000/headers</a> now, you will see that the host has been changed to “localhost” and doesn’t give any information about the underlying architecture.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/903/1*K2V0THq6UcP-ypZK8DT3Mg.png" /><figcaption>updated returned request headers</figcaption></figure><p>OK. now we have the setup to implement OAuth2 proxy to secure our microservice API. (fastapi-todo-service)</p><h3>Securing APIs (OAuth2 Proxy setup)— part 05</h3><h4>Current Setup</h4><p>First, let’s understand the current setup using the following diagram.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RUmMuGibgsNx1y-j8inppA.png" /><figcaption>sequence diagram for the /headers request</figcaption></figure><ol><li>User initiates a request to the /headers endpoint.</li><li>NGINX (API Gateway) proxies the request to todo-api:8000/headers as configured.</li><li>The resource server (todo-api) returns the response to the user&#39;s browser.</li></ol><h4>Our Goal</h4><p>What we’re aiming to do is implement an authentication mechanism at the API gateway to inspect incoming requests and determine whether to forward the request to the resource server or not.</p><p>This is where we need OAuth2 Proxy. Let’s configure OAuth2 Proxy for our current setup and discuss what happens when we try to access our todo-api service.</p><h4>Google OAuth 2.0 setup</h4><p>In order to setup OAuth2 proxy we need to have an auth provider, In this project we’re going to use Google OAuth2.0 as our provider.</p><ol><li>Go to your Google Cloud Console and search for “Manage Resources.”</li><li>Click the “Create Project” button to create a new project.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LbT8uW9dZlyu_oqQTdaylQ.png" /></figure><p>3. Provide a project name and click “Create.”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/863/1*muRDZKjeXwYr72XY9xAI8A.png" /></figure><p>4. Navigate to the OAuth consent screen by searching for it in the Google Cloud Console.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*i7XHovnPgTAJ5u4e3_xcvA.png" /></figure><p>5. Make sure you are in the correct project, ‘todo-app.’</p><p>6. Select ‘External’ and click ‘Create.’</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3QJO9BMYkDjKuj4U8apJpg.png" /></figure><p>7. Complete all the required details in the form. You can skip non-required ones.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FTEi6aLEoPwJw_6jcfptjw.png" /></figure><p>8. Save and continue after entering your email address for the developer contact information.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1004/1*-78oL6_bNZMjJqOfqUjTsQ.png" /></figure><p>7. Select all three scopes in the right-hand panel and click ‘Update.’</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3oNedLxWXPNhO5gEwljEww.png" /></figure><p>8. Next, enter two or more Gmail accounts to test the setup later.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vPa3WCB2nQeKftni8qxHTw.png" /></figure><p>9. Click ‘Add’ and then go back to the dashboard.</p><p>10. Click the ‘Credentials’ button in the left-side menu and create an OAuth client ID.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sGs-dQtNjtb2DeJrMimGFA.png" /></figure><p>11. Select the application type as ‘Web application’ and give it a name to identify it.</p><p>12. Add ‘<a href="http://localhost/">http://localhost</a>&#39; and ‘<a href="http://localhost:3000/">http://localhost:3000</a>&#39; as JavaScript origins.</p><p>13. Add ‘<a href="http://localhost/oauth2/callback">http://localhost/oauth2/callback</a>&#39; as the redirect URL. Since our NGINX OAuth2 Proxy URL is ‘<a href="http://localhost/">http://localhost/</a>&#39;, the callback URL of the OAuth2 Proxy will be that.</p><p>14. Create the credential, and it will show you the client ID and client secret. Save it for later usage.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gHEvntLKvCgNk6sR9WbcPQ.png" /></figure><h4>OAuth2 Proxy setup</h4><p>First, stop all running containers by running the following command.</p><pre>docker compose down</pre><p>Let’s add the OAuth2 Proxy to the docker-compose.yml file. Remember, correct indentation is crucial in YAML, so make sure to avoid indentation errors.</p><pre>  oauth2-proxy:<br>    container_name: oauth2-proxy<br>    image: quay.io/oauth2-proxy/oauth2-proxy:latest<br>    environment:<br>      - OAUTH2_PROXY_CLIENT_ID=&lt;your-client-id&gt;<br>      - OAUTH2_PROXY_CLIENT_SECRET=&lt;your-client-secret&gt;<br>      - OAUTH2_PROXY_COOKIE_SECRET=&lt;cookie-secret&gt;<br>      - OAUTH2_PROXY_EMAIL_DOMAINS=*<br>      - OAUTH2_PROXY_REVERSE_PROXY=true<br>      - OAUTH2_PROXY_REDIRECT_URL=http://localhost/oauth2/callback<br>      - OAUTH2_PROXY_COOKIE_SECURE=false<br>      - OAUTH2_PROXY_UPSTREAM=http://todo-api:8000<br>      - OAUTH2_PROXY_HTTP_ADDRESS=http://0.0.0.0:4180<br>      - OAUTH2_PROXY_SET_AUTHORIZATION_HEADER=true<br>      - OAUTH2_PROXY_SET_XAUTHREQUEST=true<br>      - OAUTH2_PROXY_WHITELIST_DOMAINS=.localhost:3000<br>    command:<br>      - --http-address=0.0.0.0:4180<br>      - --upstream=http://todo-api:8000<br>      # - --skip-provider-button=true<br>    networks:<br>      - microservices</pre><ol><li>Update the OAUTH2_PROXY_CLIENT_ID and OAUTH2_PROXY_CLIENT_SECRET with your Google OAuth credentials.</li><li>To generate a cookie secret,</li></ol><p><strong>Linux terminal</strong></p><pre>dd if=/dev/urandom bs=32 count=1 2&gt;/dev/null | base64 | tr -d -- &#39;\n&#39; | tr -- &#39;+/&#39; &#39;-_&#39; ; echo</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XdH_JZgPS5NAEIwAuJsv6A.png" /><figcaption>generating a cookie secret</figcaption></figure><p>COPY the output value for your cookie secret.</p><p><strong>Windows Powershell</strong></p><pre># Add System.Web assembly to session, just in case<br>Add-Type -AssemblyName System.Web<br>[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([System.Web.Security.Membership]::GeneratePassword(32,4))).Replace(&quot;+&quot;,&quot;-&quot;).Replace(&quot;/&quot;,&quot;_&quot;)</pre><p>Follow the documentation for more details.</p><p><a href="https://oauth2-proxy.github.io/oauth2-proxy/configuration/overview/#generating-a-cookie-secret">Overview | OAuth2 Proxy</a></p><p>Before running the services, you need to update your NGINX configuration as well.</p><p><strong>nginx.conf</strong></p><pre>events {<br>    worker_connections 1024;<br>}<br><br>http {<br><br>    server {<br>    listen       80;<br>    server_name  localhost;<br><br>        location / {<br>            proxy_set_header Host       $host;<br>            proxy_set_header X-Real-IP  $remote_addr;<br><br>            proxy_pass http://oauth2-proxy:4180/;<br>        }<br>    }<br><br>    server {<br>        listen  80;<br>        server_name localhost;<br><br>        auth_request /internal-auth/oauth2/auth;<br><br>        error_page 401 = http://localhost/oauth2/start?rd=$scheme://$host$request_uri;<br><br>        location / {<br>            proxy_pass http://todo-api:8000;<br>        }<br><br><br>        location /internal-auth/ {<br>            internal;<br><br>            proxy_set_header Host       $host;<br>            proxy_set_header X-Real-IP  $remote_addr;<br>            proxy_set_header X-Forwarded-Uri $request_uri;<br>            proxy_set_header Content-Length   &quot;&quot;;<br>            proxy_pass_request_body           off;<br>            proxy_pass http://oauth2-proxy:4180/;<br>        }<br>    }<br>}</pre><p>Now rebuild the NGINX API gateway service using the following command.</p><pre>docker compose build api-gateway</pre><p>Let’s run all the containers again to see the effect.</p><pre>docker compose up -d</pre><p>If you’ve followed all the instructions correctly, when you access `<a href="http://localhost/%60">http://localhost/headers`</a>, you should see the OAuth2 Proxy “Sign in with Google” button, similar to the image below.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1010/1*8xPFZXGw1J-B-1B-uYLUCA.png" /><figcaption>oauth2 proxy login portal</figcaption></figure><p>Try to log in with one of your Gmail accounts that you added as a test user when you were creating the Google OAuth 2.0 consent screen.</p><p>You will be asked to continue. Click the “Continue” button.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/880/1*Wbf8qXQkHiNNvpy9uaxrXQ.png" /></figure><p>Now, you will see the headers page with a large cookie header.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*q9H25o_U7HbGb0aKgld9Sg.png" /><figcaption>returned request headers with the _oauth2_proxy cookie</figcaption></figure><p>Let’s discuss what’s happening here.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xWmeUL_oUHk9GEjnMX5ayQ.gif" /><figcaption>auth flow simplified</figcaption></figure><ol><li>When a user requests access to a service, it first goes through the NGINX reverse proxy, which checks if the user is authenticated. If you check the nginx.conf, you’ll see that before proxying to the todo-api service, we’ve added an auth_request to verify whether the user is authenticated or not.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/903/1*sR338ARN1T4R5cQDDHy46w.png" /><figcaption>auth_request directive in the nginx.conf file</figcaption></figure><p>2. Next, OAuth2 Proxy will validate the user authentication via an OAuth2 request with the auth provider as configured. If you look at the docker-compose.yml, you’ll see all the settings that you’ve configured there.</p><p><strong>You can find all the configuration options in the documentation. </strong><br><a href="https://oauth2-proxy.github.io/oauth2-proxy/configuration/overview/#command-line-options">https://oauth2-proxy.github.io/oauth2-proxy/configuration/overview/#command-line-options</a></p><p>3. Then, OAuth2 Proxy will return the authenticated state to NGINX.</p><p>4. Depending on the authenticated state (if successful), the user will be allowed to access the service. Otherwise, they will be redirected to the login portal.</p><p>If you try to access <a href="http://localhost:3000/headers%60">http://localhost:3000/headers</a> now, you’ll see a 403 Forbidden status for our request. Let’s fix that.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*a3bx3Io5YSxXCa98eNLjjw.png" /></figure><p>Before moving to the frontend, let’s ensure that users are redirected to the Google login if they try to directly access our API URLs through NGINX. To do this, uncomment the comment and add the — skip-provider-button=true option under the command in the docker-compose.yml file.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/578/1*8Lls9uZubh39x_QUd_cIGA.png" /><figcaption>uncommenting the — skip-provider-button=true</figcaption></figure><p>Rerun the command docker compose up -d to apply the updates made to the docker-compose.yml file.</p><pre>docker compose up -d</pre><p>Now, if you try to access <a href="http://localhost/headers%60">http://localhost/headers</a>, you’ll still be able to see the headers because you’ve already authenticated and the _auth2_proxy cookie is stored in your browser.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4NWKc25G7Z5i7-va5LFJpQ.png" /></figure><p>You can right-click and manually delete the cookie or use the <a href="http://localhost/oauth2/sign_out%60"><strong>http://localhost/oauth2/sign_out</strong></a> endpoint to clear the cookie.</p><p>Now, if you try to access <a href="http://localhost/%60">http://localhost/</a> again, you will be redirected to the Google login without showing the provider button.</p><p>Okay, now let’s fix our frontend to work as before.</p><h4>Fix Todo Client</h4><p>In order to fetch data correctly from our secured API, we need to pass the _oauth2_proxy cookie with our request. We can achieve that by setting <strong>credentials: “include”</strong> in the fetch request.</p><p>To learn more about the Fetch API, refer to the MDN Web Docs documentation.</p><p><a href="https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials">Request: credentials property - Web APIs | MDN</a></p><p><strong>next-todo-app/app/headers/page.tsx</strong></p><pre>...<br><br>const response = await fetch(&#39;http://localhost/headers&#39;, {<br>    credentials: &quot;include&quot;<br>})<br><br>...</pre><p><strong>next-todo-app/actions/todo.ts (update 6 todo fetch requests)</strong></p><pre>...<br><br>// create a todo<br>const response = await fetch(API_URL, {<br>    method: &#39;post&#39;,<br>    credentials: &#39;include&#39;,<br>    headers: {<br>        &#39;Content-Type&#39;: &#39;application/json&#39;,<br>    },<br>    body: JSON.stringify(data)<br>})<br><br>...</pre><pre>...<br><br>// get todos<br>const response = await fetch(API_URL, {<br>    credentials: &#39;include&#39;<br>})<br><br>...</pre><pre>...<br><br>// delete todo<br>const response = await fetch(`${API_URL}${id}`, {<br>    method: &#39;delete&#39;,<br>    credentials: &#39;include&#39;<br>})<br><br>...</pre><pre>...<br><br>// complete todos<br>const response = await fetch(`${API_URL}${id}/complete`, {<br>    method: &#39;post&#39;,<br>    credentials: &#39;include&#39;<br>})<br><br>...</pre><pre>...<br><br>// withdraw todos<br>const response = await fetch(`${API_URL}${id}/incomplete`, {<br>    method: &#39;post&#39;,<br>    credentials: &#39;include&#39;<br>})<br><br>...</pre><pre>...<br><br>// update todos<br>const response = await fetch(`${API_URL}${id}`, {<br>    method: &#39;put&#39;,<br>    credentials: &#39;include&#39;,<br>    headers: {<br>        &#39;Content-Type&#39;: &#39;application/json&#39;,<br>    },<br>    body: JSON.stringify(data)<br>})<br><br>...</pre><p>Now, rebuild the todo-client service in Docker Compose and rerun the service again to see the updated changes.</p><pre>docker compose build next-todo-app</pre><pre>docker compose up -d</pre><p>Now, if you access <a href="http://localhost:3000/headers%60"><strong>http://localhost:3000/headers</strong></a> or <a href="http://localhost:3000/%60"><strong>http://localhost:3000/</strong></a>, and you have a valid _oauth2_proxy cookie stored in your browser, you will be able to see todos rendered in the todo-client application as you saw before implementing the authentication. (<strong>But still you won’t be able to create todos or delete them. We’ll fix that later in the project.</strong>)</p><h4>How can we login from the Next.js todo-client?</h4><p>In the current setup, to set the _oauth2_proxy cookie, we have to access http://localhost/.</p><p>However, in a real scenario, redirecting users out of your client application like this may not be ideal.</p><p>In OAuth2 Proxy, to start the authentication flow, they’ve given us the /oauth2/start endpoint. Invoking that with the rd (redirect) URL parameter will help us achieve this task.</p><p><strong>Next.js middleware</strong></p><p>We can create a file named middleware.ts inside the /next-todo-app/ folder and add the following code snippet to achieve that.</p><p><strong>/next-todo-app/middleware.ts</strong></p><pre>import {NextRequest} from &quot;next/server&quot;;<br><br>export async function middleware(request: NextRequest) {<br>    const oauthCookie = request.cookies.get(&#39;_oauth2_proxy&#39;)<br><br>    console.log(&#39;Current path&#39;, request.url)<br><br><br>    if (!oauthCookie &amp;&amp; request.nextUrl.pathname !== &#39;/login&#39;) {<br>        return Response.redirect(new URL(&#39;/login&#39;, request.url));<br>    }<br>}<br><br>export const config = {<br>    matcher: [&#39;/&#39;, &#39;/headers&#39;, &#39;/login&#39;]<br>}</pre><p>Next Middleware allows you to run code before a request is completed. Then, based on the incoming request, you can modify the response by rewriting, redirecting, modifying the request or response headers, or responding directly.</p><p><a href="https://nextjs.org/docs/app/building-your-application/routing/middleware">Routing: Middleware | Next.js</a></p><p>I’ve already added a simple login screen with a Login with Google button. From middleware.ts we will be redirected to that page if we don’t have the oauth2 cookie stored in the browser. we can start the auth flow from there.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/906/1*SzqAaHLkerEFrVjda80OKw.png" /><figcaption>simple login page for the todo-client</figcaption></figure><p>If you want to have a simple logout button, add the following code snippet in your next-todo-app/app/page.tsx file.</p><p><strong>next-todo-app/app/page.tsx</strong></p><pre>...<br><br>&lt;main className=&quot;mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-10 bg-slate-50 space-y-2&quot;&gt;<br>    {/* add logout button here */}<br>    &lt;div className=&quot;flex justify-end&quot;&gt;<br>        &lt;a href=&quot;http://localhost/oauth2/sign_out?rd=http%3A%2F%2Flocalhost%3A3000%2F&quot;&gt;&lt;p<br>            className=&quot;text-blue-500 underline transition cursor-pointer&quot;&gt;Logout&lt;/p&gt;&lt;/a&gt;<br>    &lt;/div&gt;<br>    {/* logout button end */}<br><br>...</pre><p>Now, rebuild the todo-client service in Docker Compose and rerun the service again to see the updated changes.</p><pre>docker compose build next-todo-app</pre><pre>docker compose up -d</pre><p>Now, If you access <a href="http://localhost:3000/">http://localhost:3000</a> with _oauth2_proxy cookie stored in the browser. You’ll be able to see your previously entered todos with the newly added logout button.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MGwQsComQ5FXn1TkS48ipg.png" /></figure><p>If you try to interact with the app, you’ll notice that you cannot create new todos. This is because OAuth2 Proxy is trying to authenticate preflight requests. You can change that behavior by adding <strong>— skip-auth-preflight=true</strong> in your docker-compose.yml file.</p><pre>...<br> <br>     - OAUTH2_PROXY_SET_XAUTHREQUEST=true<br>      - OAUTH2_PROXY_WHITELIST_DOMAINS=.localhost:3000<br>    command:<br>      - --http-address=0.0.0.0:4180<br>      - --upstream=http://todo-api:8000<br>      - --skip-provider-button=true<br>      - --skip-auth-preflight=true<br>    networks:<br>      - microservices<br><br>...</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/460/1*cPs0QBGg5aL3uHYvMbENNw.png" /><figcaption>— skip-auth-preflight=true for not to authenticate preflight requests</figcaption></figure><p>Rerun all the services to see the effect.</p><pre>docker compose up -d</pre><p>If you have followed all the instructions correctly, you should now be able to log in and log out of your todo application using the todo-client app. You have also completed the following project architecture:</p><figure><img alt="Architecture" src="https://cdn-images-1.medium.com/proxy/1*nhefZtJtP3_LhMKLueUzMg.gif" /><figcaption>project architecture</figcaption></figure><p>Let’s have a look at the docker container diagram with network boundaries too.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6WAdak685CEI4qoaWLAPow.png" /><figcaption>final container diagram</figcaption></figure><h3>Auth flow — Step 06</h3><p>In the diagram below, we explain how a protected API route request is handled through the OAuth2 proxy flow. The user starts by requesting the <strong>/headers</strong> API endpoint. Below diagram is inspired by the 1438th issue in the OAuth2-proxy GitHub repository.</p><p><a href="https://github.com/oauth2-proxy/oauth2-proxy/issues/1438">Document/sketch OAuth2-Proxy authentication flow · Issue #1438 · oauth2-proxy/oauth2-proxy</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MGNR_gT4icsR0DDeVgIMtQ.png" /><figcaption>OAuth2 proxy AUTH FLOW</figcaption></figure><h3>Congratulations. ☕</h3><p>Congratulations! If you’ve made it this far, well done. While I couldn’t cover everything I had hoped to in this article, I promise to provide an extension to it. In the next part, I plan to discuss:</p><ul><li>How to handle sign-in, sign-out, and protected routes in the Next.js middleware.</li><li>How to leverage our current setup in the Next.js front-end.</li><li>How to handle authorization in the FastAPI back-end.</li><li>CORS and preflight requests.</li><li>Cross-Site Request Forgery (CSRF) and CSRF cookies.</li><li>OAuth2 proxy refresh cookies, and more options.</li><li>How to add more services to our current setup?</li></ul><p>Following that, I plan to continue writing about how to host this kind of architecture in managed services and Kubernetes in the near future.</p><blockquote>If you’ve enjoyed this project, don’t forget to give it a 👏. Your support gives me more energy and enthusiasm to write more projects like this. Thank you!</blockquote><p>Happy Coding!!! 🐧</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=71fabc79147d" width="1" height="1" alt=""><hr><p><a href="https://blog.devops.dev/securing-microservice-apis-with-oauth2-proxy-a-complete-project-71fabc79147d">Securing Microservice APIs with OAuth2 Proxy: A Complete Project</a> was originally published in <a href="https://blog.devops.dev/">DevOps.dev</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Deploy your first containerized application on Google Cloud Run]]></title>
            <link>https://medium.com/@kesaralive/how-to-deploy-your-first-containerized-application-on-google-cloud-run-6b9d3e8802f1?source=rss-d60194e755a2------2</link>
            <guid isPermaLink="false">https://medium.com/p/6b9d3e8802f1</guid>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[google-cloud-run]]></category>
            <category><![CDATA[containerization]]></category>
            <category><![CDATA[php]]></category>
            <dc:creator><![CDATA[Kesara Karannagoda]]></dc:creator>
            <pubDate>Thu, 17 Aug 2023 22:46:15 GMT</pubDate>
            <atom:updated>2023-08-18T08:05:50.949Z</atom:updated>
            <content:encoded><![CDATA[<h3>Introduction</h3><p>In the world of making software, things happen really fast nowadays. One important thing developers do is put their software inside containers, like virtual boxes. This helps the software run well no matter where it goes. <strong>Docker</strong> is a popular tool for doing this.</p><p>In this article, we will show you how to put a simple php-api application into a Docker container and host it on the cloud.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zWGarSPPLAGMmwCj0ASs7g.png" /><figcaption>Deploy your first containerized application on Google Cloud Run</figcaption></figure><h3>Prerequisites:</h3><p>Before we start the process of dockerizing the application, make sure the following requirements are satisfied:</p><ol><li><strong>Docker:</strong> Install Docker to create, manage, and run containers.</li><li><strong>Git &amp; GitHub:</strong> Having the basic idea behind local repositories &amp; remote repositories.</li></ol><p>I’m suggesting you to read the below article before beginning. It will give you a better idea about the local deployment of the same application.</p><p><a href="https://medium.com/@kesaralive/getting-started-with-docker-compose-hands-on-project-experience-e562ab07e24c">Building a Simple 3-Tier Architecture with Docker Compose: A Hands-On Project Journey</a></p><h3>Steps to Dockerize the simple-php-todo-api Application</h3><h4>1. Create the Application</h4><ul><li>Create a project folder named simple-php-todo-api .</li><li>Inside that folder create another folder calledsrc .</li><li>Inside the src folder create a new file named index.php and add the below code to it.</li></ul><pre>&lt;?php<br>// Set the response content type to JSON<br>header(&#39;Content-Type: application/json&#39;);<br>header(&#39;Access-Control-Allow-Origin: *&#39;);<br>header(&#39;Access-Control-Allow-Methods: GET, POST, OPTIONS&#39;);<br><br>// Create a response array<br>$response = [<br>    [<br>        &quot;_id&quot; =&gt; 1,<br>        &quot;todo&quot; =&gt; &quot;Todo 1&quot;<br>    ],<br>    [<br>        &quot;_id&quot; =&gt; 2,<br>        &quot;todo&quot; =&gt; &quot;Todo 2&quot;<br>    ],<br>    [<br>        &quot;_id&quot; =&gt; 3,<br>        &quot;todo&quot; =&gt; &quot;Todo 3&quot;<br>    ],<br>    [<br>        &quot;_id&quot; =&gt; 4,<br>        &quot;todo&quot; =&gt; &quot;Todo 4&quot;<br>    ]<br>];<br><br>// Encode the response array as JSON<br>$jsonResponse = json_encode($response);<br><br>// Output the JSON response<br>echo $jsonResponse;</pre><ul><li>Above code will create a simple hard-coded todo api server.</li></ul><h4>2. Create the Dockerfile</h4><p>In the root directory of your project, create a file named Dockerfile (without any file extensions) and add the following content:</p><pre># Use the official PHP image with Apache as the base image<br>FROM php:8.0-apache<br><br># Copy your application files to the appropriate directory<br>COPY ./src /var/www/html/<br><br># Expose the default HTTP port (port 80)<br>EXPOSE 80<br><br># Enable the rewrite module (assuming your PHP application uses URL rewriting)<br>RUN a2enmod rewrite<br><br># Install any necessary PHP extensions or dependencies here (if needed)<br># For example, if you use database connections, you might need to install relevant extensions.<br><br># Set the working directory to the web root<br>WORKDIR /var/www/html/<br><br># Start the Apache web server when the container starts<br>CMD [&quot;apache2-foreground&quot;]</pre><h4>3. Build the Docker Image</h4><p>Run the terminal on your project folder and then, Build the Docker image using the following command, replacing simple-php-todo-api with a suitable image name:</p><pre>docker build -t simple-php-todo-api .</pre><h4>4. Run the Docker Container</h4><p>After building the Docker image, run a container from it:<br>Container port 80 will be mapped to the host port 8000 in the below command.</p><pre>docker run -p 8000:80 simple-php-todo-api</pre><h4>5. Access the simple-php-todo-api</h4><p>Open your web browser and navigate to <a href="http://localhost:8000/">http://localhost:8000</a>/ to see your simple-php-todo-api application running inside a Docker container.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/899/1*n3w8LC-Ig2hl08nJ-gXc9g.png" /><figcaption>simple-php-todo-api at http://localhost:8000/</figcaption></figure><h3>Steps to Host the Dockerized Next.js Application in Google Cloud Run</h3><p>This part of the tutorial explains how to put the created app on Google Cloud Run using GitHub. This way is smooth and makes it easy to get apps up and running. It will enable the collaboration between developers as well.</p><p>Going further you can automate the deployment process and ensure consistent updates across environments like dev , test &amp; prod .</p><h4>1. Set Up GitHub Repository</h4><p>First we have to make sure our code is on GitHub.</p><ol><li>Create a new repository in your GitHub account.</li><li>You can copy the code from your GitHub repository Quick Setup and push your code to that repository.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QRNGIAF2iPX4NQZ3H0XJQQ.png" /><figcaption>GitHub repository Quick setup</figcaption></figure><p>Quick setup will only add the README.md file. After that follow the below commands to push your project code to the repository.</p><pre>git add .<br>git commit -m &quot;feat: add simple-todo-api&quot;<br>git push</pre><p>Okay, Now we’ve done setting up our GitHub repository.</p><h4>2. Getting Google Cloud Run Ready</h4><ol><li>Go to the cloud run <a href="https://cloud.google.com/run">https://cloud.google.com/run</a> and create a new service.</li><li>Select the option <strong>“Continuously deploy new revisions from a source repository” </strong>like in the below image.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/574/1*Ov9DTCKyujDKiy79dysE3w.png" /><figcaption>Continuously deploy new revisions from a source repository — Cloud Run</figcaption></figure><p>3. Then click the <strong>“SET UP WITH CLOUD BUILD”</strong> button.</p><ul><li>Select <strong>“GitHub”</strong> as Repository provider and connect your GitHub account.</li><li>Click the <strong>“Repository”</strong> dropdown.</li></ul><p>4. If you see your repository there, select it and continue. If not click the <strong>“Manage connected repository”</strong> link and authorize your GitHub repository from there.</p><p>5. Then click the <strong>“Next”</strong> button to continue.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/759/1*AfHDli0-TW7rIxjy-2dkJw.png" /></figure><p>6. In the build configuration main branch is already selected, That means Every time when you push any changes to the main branch, Cloud Run will build a new image and deploy from that branch. You can change it as you want. If you’re maintaining an existing repository with a master branch. You can change it to ^master$ or else you can choose a specific branch that you want to deploy your application.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/760/1*sUcXbhuUBXWuX7chKEXBCw.png" /><figcaption>Setup with Cloud Build — Build Configuration</figcaption></figure><p>7. Since we’re using Docker to build the project, select the <strong>“Build Type”</strong> as <strong>Dockerfile.</strong></p><p>8. Then save it and go back to the <strong>“Cloud Run”</strong> configuration.</p><p>9. You can select the region as you want, I select asia-southeast1 (Singapore) because It gives me better performance.</p><blockquote>Lower the distance, higher the performance.</blockquote><p>10. End of the configuration, Select the <strong>“Allow unauthenticated invocations” </strong>option.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/766/1*BUGNKbPRapj_uqb5IYlJ4Q.png" /><figcaption>Allow unauthenticated invocations</figcaption></figure><p>11. Click the <strong>“Container, Networking, Security” </strong>drop down and change the container port to <strong>“80”</strong>. Since we’re serving the application in PORT 80 it should be changed.</p><blockquote><strong>Note:</strong> I’m recommending you to click <strong>“Learn more”</strong> links and discover a lot about Cloud Run. This will help you to understand why each option is there.</blockquote><p>12. Then click the <strong>“Create”</strong> button.</p><p>It will take some time to deploy your container on Cloud Run. In the end it will show you an output like in the below figure.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/880/1*Z1db0HMt_mzMe0S3rIBTlQ.png" /><figcaption>Deploymend status — Cloud Run</figcaption></figure><p>13. Now you should be able to access your simple-php-todo-api using the URL there.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/900/1*bLgOF3qV4crOI7bgVWAP6w.png" /><figcaption>simple-php-todo-api running on Cloud Run</figcaption></figure><blockquote><strong>Note:</strong><br>Please remember that Cloud Run is designed for stateless applications, so It’s well-suited for serving static content or APIs like applications. If your application requires dynamic content or server-side processing, Cloud Run might not be the best choice. In such cases, you might consider other Google Cloud services like <strong>Compute Engine</strong> or <strong>Google Kubernetes Engine (GKE).</strong></blockquote><h3>Experience the CI/CD process</h3><ul><li>CI: Continuous Integration</li><li>CD: Continuous Deployment/Delivery</li></ul><ol><li>Replace the index.php with below code.</li></ol><pre>&lt;?php<br>// Set the response content type to JSON<br>header(&#39;Content-Type: application/json&#39;);<br>header(&#39;Access-Control-Allow-Origin: *&#39;);<br>header(&#39;Access-Control-Allow-Methods: GET, POST, OPTIONS&#39;);<br><br>// Create a response array<br>$response = [<br>    [<br>        &quot;_id&quot; =&gt; 0001,<br>        &quot;todo&quot; =&gt; &quot;Buy groceries for the week&quot;<br>    ],<br>    [<br>        &quot;_id&quot; =&gt; 0002,<br>        &quot;todo&quot; =&gt; &quot;Complete the presentation slides&quot;<br>    ],<br>    [<br>        &quot;_id&quot; =&gt; 0003,<br>        &quot;todo&quot; =&gt; &quot;Call the utility company to resolve billing issue&quot;<br>    ],<br>    [<br>        &quot;_id&quot; =&gt; 0004,<br>        &quot;todo&quot; =&gt; &quot;Schedule a dentist appointment&quot;<br>    ]<br>];<br><br>// Encode the response array as JSON<br>$jsonResponse = json_encode($response);<br><br>// Output the JSON response<br>echo $jsonResponse;</pre><p>2. Push changes to the GitHub repository.</p><pre>git add .<br>git commit -m &quot;update todos&quot;<br>git push</pre><p>3. You’ll be able to see the build happening in the Cloud Run and will be able to access the updated todo-api after that.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/900/1*eMku10yjL22q4HGKxMV7pw.png" /><figcaption>Updated todo-api on Cloud Run URL</figcaption></figure><h3>Resources</h3><p>Github repository for this tutorial: <a href="https://github.com/kesaralive/simple-php-todo-api">https://github.com/kesaralive/simple-php-todo-api</a></p><h3>Conclusion</h3><p>So, to sum it all up: first, we made a basic “<strong>simple-php-todo-api</strong>” program. Then, we made it even cooler by putting it into a special Docker image. We made sure it worked on our computer and then stored it on <strong>GitHub.</strong></p><p>But that’s not all! We brought in <strong>Google Cloud Run</strong>. It let our program live on the internet.</p><p>Using <strong>Docker</strong>, <strong>GitHub</strong>, and <strong>Google Cloud Run</strong> together we’ve experienced the power of <strong>CI/CD </strong>in the end.</p><h3>Some other docker articles by me</h3><ul><li><a href="https://medium.com/@kesaralive/from-os-hassles-to-team-harmony-how-docker-saved-our-group-project-e37c52ce8509">From OS Hassles to Team Harmony: How Docker Saved Our Group Project</a></li><li><a href="https://medium.com/@kesaralive/diving-deeper-into-docker-networking-with-docker-compose-737e3b8a3c8c">Diving Deeper into Docker Networking with Docker Compose</a></li></ul><h3>Connect with me</h3><ul><li>LinkedIn: <a href="https://www.linkedin.com/in/kesaralive">https://www.linkedin.com/in/kesaralive</a></li><li>GitHub: <a href="https://github.com/kesaralive">https://github.com/kesaralive</a></li><li>Twitter: <a href="https://twitter.com/kesaralive">https://twitter.com/kesaralive</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6b9d3e8802f1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Hosting LiveKit Server on Google Cloud: A Step-by-Step Guide ]]></title>
            <link>https://medium.com/@kesaralive/hosting-livekit-server-on-google-cloud-a-step-by-step-guide-52c5b3746a3e?source=rss-d60194e755a2------2</link>
            <guid isPermaLink="false">https://medium.com/p/52c5b3746a3e</guid>
            <category><![CDATA[livekit]]></category>
            <category><![CDATA[video-streaming-server]]></category>
            <category><![CDATA[virtual-machine]]></category>
            <category><![CDATA[google-cloud]]></category>
            <category><![CDATA[webrtc]]></category>
            <dc:creator><![CDATA[Kesara Karannagoda]]></dc:creator>
            <pubDate>Mon, 31 Jul 2023 12:55:31 GMT</pubDate>
            <atom:updated>2023-08-02T07:06:29.825Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*O8qUvKq-0BZYPa14ydUf1A.jpeg" /></figure><p>In a recent project, I had to use Livekit, but I had trouble finding up to date resources on how to deploy a Livekit server on Google Cloud Platform (GCP).</p><p>So, I decided to write an article to help others who might be facing the same issue. The article will be a step-by-step guide to set up a Livekit server on GCP, making it easier to use this useful tool for real-time communication applications.</p><h3>What is Livekit?</h3><p>Livekit is an open-source toolkit designed for building scalable, real-time video and audio applications.</p><p>It provides a set of APIs and SDKs that developers can use to create applications such as video conferencing, live streaming, and real-time communication tools. Livekit aims to make it easier for developers to integrate live video and audio features into their applications without having to handle the complexities of video encoding, streaming, and signaling protocols.</p><h3>Let’s see how can we deploy a Livekit server on GCP.</h3><h4>Prerequisites</h4><ol><li>You need to have a Google Cloud Account.</li><li>A domain that you own.</li><li>The ability to add DNS records for subdomains for your new LiveKit server</li></ol><p>For this tutorial I’m gonna use my own domain (kesaralive.com)</p><h3><strong>Virtual Machine Setup</strong></h3><p>First we need to have a virtual machine provisioned in google cloud. In order to do that, go to the cloud console and navigate to the VM instances like in the below image.</p><ol><li>Click the Hamburger icon in the top left corner of the cloud console.</li><li>In the side menu, go to <strong>“Compute Engine”</strong>.</li><li>Select <strong>“VM Instances”.</strong></li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/635/1*5jh_GR9vdqgbhQNR-keufg.png" /><figcaption>Navigating to the VM instances in GCP</figcaption></figure><p>Click the button <strong>“Create Instance”</strong> to configure a new Virtual Machine (VM) Instance.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/732/1*WBjrJJsmsVJwMjgNhcRY7Q.png" /></figure><h4>VM Instance Configuration</h4><ol><li>Enter a name you prefer to identify your VM Instance.</li><li>Select the appropriate region to you. <br><strong>“Closer the region, better performance.”</strong></li><li>I’m selecting a low cost E2 series Virtual Machine. <br><strong>(Not recommended for a production server)</strong></li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/821/1*-pepR3grvcG2Qh_QE3ltuw.png" /></figure><p>4. Click the <strong>“Change”</strong> button to configure the boot disk.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/841/1*oH-vRuRLL-AgolaltHbH8g.png" /></figure><p>5. Select the <strong>“Ubuntu” </strong>operating system from the dropdown menu.</p><p>6. Click the <strong>“Select”</strong> button to save the boot disk configuration.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/799/1*dB2bJ5-G3HnpvodLw3EC_A.png" /></figure><p>7. Allow both <strong>HTTP</strong> &amp; <strong>HTTPS</strong> traffic to access port 80 &amp; 443 from the Internet. (This will allow you to access the VM through your browser.)<br>8. Click the <strong>“Create”</strong> button to start your Virtual Machine.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*puFGDhu4Li2e4B5_RuH0JA.png" /></figure><h3>DNS Configuration</h3><p>In order to setup Livekit, you need to have two domain names. You can use subdomains for this. One for the Livekit server and other one for the turn server. Since Livekit has built in TURN, we can use the same virtual machine for TURN. In this tutorial I’m using following sub domains for the setup.</p><ol><li>For LiveKit: livekit.kesaralive.com</li><li>For TURN: turn.kesaralive.com</li></ol><p>You can use your domain as below,</p><ol><li>livekit.&lt;your domain name&gt;</li><li>turn.&lt;your domain name&gt;</li></ol><blockquote><strong>Note:</strong> I’m going to use Cloud DNS to manage these subdomains but you can create an <strong>“A record”</strong> to your subdomain directly from you main domain authority. (This can be much simpler for you.)</blockquote><p>First, Navigate to the <strong>“Cloud DNS”</strong> like in the blow image.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/875/1*N69RPBEfMbhg-LK4xrJx_A.png" /></figure><p>If you haven’t enable <strong>“Cloud DNS API”</strong>, enable it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/463/1*4oc0UbZ6766g5UqjEniRyA.png" /></figure><p>We have to create two domain zones for this.</p><ol><li>First one is for the livekit.&lt;your domain name&gt;</li><li>Second one is for the turn.&lt;your domain name&gt;</li></ol><p>In my case, I will use live.kesaralive.com and turn.kesaralive.com for the tutorial.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2xotRHrg5aoO5zQD3gP-pA.png" /><figcaption>livekit.kesaralive.com &amp; turn.kesaralive.com domain zone settings</figcaption></figure><p>After creating a domain zone we have to give the sub domain authority to the <strong>Cloud DNS</strong> to resolve any requests on that domain. In order to do that, we have to setup custom nameservers for our subdomains in the main domain name’s authoritative DNS server provider. In my case It’s cloudflare.</p><p>After creating a domain zone, the next step is to allow <strong>“Cloud DNS”</strong> to manage the subdomains ( livekit.kesaralive.com and turn.kesaralive.com ) so that it can handle requests for them. To do this, we need to set up custom nameservers for our subdomains in Cloudflare, which is the service I use to manage my main domain’s (<strong>“kesaralive.com”</strong>)settings.</p><p>In a nutshell, setting up custom nameservers in Cloudflare is essential for giving authority to Cloud DNS</p><h4>Sub Domain Setup</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/818/1*z4dxpSYNzsY-vRtFqBByig.png" /><figcaption>livekit &amp; turn domain zones for our domain name</figcaption></figure><p>Before going to create nameserver records in <strong>“Cloudflare”, <br></strong>Let’s set <strong>“A Records” </strong>in both subdomains for the previously created virtual machine’s ip address.</p><blockquote>When you create an “<strong>A record”</strong> in <strong>DNS (Domain Name System)</strong>, you are essentially associating a specific IP address with a domain name. This enables the DNS system to translate human-readable domain names into machine-readable IP addresses, allowing computers to locate and communicate with the correct server hosting the website or service associated with that domain.</blockquote><p>Go to the domain zone by clicking it. (Ex: livekit.kesaralive.com domain zone)</p><ol><li>Click to <strong>“Add standard”</strong>.</li><li>In Cloud DNS, when working within the same GCP Project, you have the convenience of selecting an IP address for a resource without the need for manual input. This is especially helpful when the desired IP address belongs to a resource within the same GCP Project. As we have already created our virtual machine within this project, we can easily avail ourselves of this feature by simply clicking the <strong>“Select IP Address”</strong> button. This streamlined process ensures a smooth and efficient experience when configuring our DNS settings, eliminating the burden of manual entry and facilitating seamless integration within the project’s infrastructure.</li><li>Select the appropriate <strong>“IP address”</strong> which has directed to the previously created Virtual Machine.</li><li>Then click the <strong>“Create” </strong>button to create the <strong>A record.</strong></li></ol><p>Do that for the both livekit and turn sub domains like in the below image.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*G87EO9KXB_hUq-IVXVVkfA.png" /><figcaption>Setting A records for sub domains</figcaption></figure><h4>Nameservers</h4><p>Like we discussed earlier, to give domain authority to the <strong>“Cloud DNS”</strong> we have to create NS records in our Authoritative DNS server provider. In my case It’s “CloudFlare”, This can be your domain name provider (Where you bought the domain name).</p><p>COPY all NS records one by one and add them into the authoritative DNS server provider like in the below image. (In my case, It’s Cloudflare.)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*J9vhndynH3rIahJwAPrmxQ.png" /><figcaption>Creating NS records to give authority to Cloud DNS</figcaption></figure><p>Do the same for both livekit and turn subdomains. In the end you should have a recordset like in the below image.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nKlpM8W1SRpqZY1qiyxLew.png" /><figcaption>NS records in Cloudflare</figcaption></figure><h3>Livekit deployment on the Virtual Machine</h3><ol><li>Navigate to the <strong>“VM Instances” </strong>like we did in the previous step.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*o1WA5NsxWtBYp6SSuRsJcQ.png" /><figcaption>virtual machine listed on VM instances</figcaption></figure><p>2. Click the <strong>“SSH” </strong>button, this will give you a dialog box to authorize. Click <strong>“Authorize”</strong> button and continue.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/909/1*3MvZme7FZXyaSLgzhigDAA.png" /><figcaption>Authorization dialog box</figcaption></figure><p>3. It will give you the access to your <strong>Virtual Machine terminal</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/909/1*9WBTblISNz0YwqdXsTYU2A.png" /><figcaption>Cloud VM Terminal</figcaption></figure><p>COPY the provided code and paste it into your <strong>Cloud VM terminal</strong> to initiate a clean installation of Docker in your virtual machine.</p><p>You can use <strong>“ctrl + shift + v”</strong> to paste the code, or simply do a<strong> right-click and select “paste.”</strong></p><p>This installation process will take some time as it sets up Docker on your <strong>“Cloud VM”</strong>, ensuring a smooth and efficient deployment of Docker’s containerization capabilities within your virtual environment.</p><pre>for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt-get remove $pkg; done<br>sudo apt-get update<br>sudo apt-get install ca-certificates curl gnupg<br>sudo install -m 0755 -d /etc/apt/keyrings<br>curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg<br>sudo chmod a+r /etc/apt/keyrings/docker.gpg<br>echo \<br>  &quot;deb [arch=&quot;$(dpkg --print-architecture)&quot; signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \<br>  &quot;$(. /etc/os-release &amp;&amp; echo &quot;$VERSION_CODENAME&quot;)&quot; stable&quot; | \<br>  sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null<br>sudo apt-get update<br>sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y<br>sudo docker run hello-world</pre><p>RUN docker --version to check whether you have installed the docker correctly.</p><pre>  docker --version</pre><p>Now let’s add the default user to the docker user group</p><pre>sudo usermod -aG docker $USER<br>newgrp docker</pre><p>Verify that you can run docker commands using the below command</p><pre>docker run hello-world</pre><p>Then RUN the below command to generate the starter files for the <strong>Livekit server</strong>.</p><pre>docker run --rm -it -v$PWD:/output livekit/generate</pre><p>We’ll go with <strong>“LiveKit Server only” </strong>option<strong> </strong>in this tutorial.</p><pre>$ docker run --rm -it -v$PWD:/output livekit/generate<br>Generating config for production LiveKit deployment<br>This deployment will utilize docker-compose and Caddy. It&#39;ll set up a secure LiveKit installation with built-in TURN/TLS<br>SSL Certificates for HTTPS and TURN/TLS will be generated automatically via LetsEncrypt or ZeroSSL.</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/897/1*79emp7FKfHHHcHHobTha-Q.png" /></figure><p>For the primary domain name, give livekit.&lt;your domain name&gt; . In my case, it’s livekit.kesaralive.com .</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/897/1*V9SCSMsRwPCMALVTZ2Po-Q.png" /></figure><p>For the TURN domain name, give turn.&lt;your domain name&gt; . In my case, it’s turn.kesaralive.com .</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/897/1*ZjadUClATKhkHCZM1BgImg.png" /></figure><p>Choose “<strong>Let’s Encrypt (no account required)” </strong>option.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/897/1*zEPaZMTbBSe6mejDb5tErQ.png" /></figure><p>Use the “<strong>latest”</strong> version of LiveKit.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/897/1*jCORatcSfedwWVRTQ2WV2A.png" /></figure><p>Select “<strong>no — (we’ll bundle Redis)” </strong>option.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/897/1*6f9TNvi4yHHm6tVHtq-gAQ.png" /></figure><p>Select “<strong>Startup Shell Script”</strong> for this tutorial.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/897/1*MasJyRepOpa-q4i0R3NPdA.png" /></figure><p>It will generate an <strong>API KEY</strong>, <strong>API SECRET</strong> and a <strong>TEST TOKEN</strong>. <br>Save it somewhere for later use.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/902/1*lq4PFUUvOGL1sVJwE3_Fqg.png" /></figure><h3>Firewall Setup</h3><p>We’ll use <strong>“Linux UFW”</strong> to configure the firewall.</p><p>Linux UFW (Uncomplicated Firewall) is a user-friendly front-end for managing iptables, the built-in firewall tool for Linux systems. It is designed to simplify the process of configuring a firewall and makes it easier for users to control network traffic and secure their systems.</p><p>First switch to the root user using sudo su command.</p><pre>sudo su</pre><p>Enable the <strong>UFW</strong> in your virtual machine.</p><pre>ufw enable</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/908/1*0LKAHyaUJDIwokQwkX49Yg.png" /></figure><p>Say <strong>yes (y) </strong>to this and enable the <strong>“OpenSSH”</strong> asap in your virtual machine. If you close the window without enabling SSH, you will lose the access to the VM.</p><pre>ufw allow &quot;OpenSSH&quot;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/908/1*DIujUchivxykLE5KDZy0UQ.png" /></figure><p>In order to deploy Livekit server successfully, we need to make sure that the following ports are open on our firewall and accessible on the instance:</p><ul><li>443 — primary HTTPS and TURN/TLS</li><li>80 — TLS issuance</li><li>7881 — WebRTC over TCP</li><li>443/UDP — TURN/UDP</li><li>50000–60000/UDP — WebRTC over UDP</li></ul><p>Let’s Configure these ports using following commands.</p><pre>ufw allow 443<br>ufw allow 80<br>ufw allow 7881<br>ufw allow 50000:60000/udp</pre><p>RUN the command ufw status . You’ll get an output like in the below figure.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/439/1*Q8yEA3dj04MwAPQC-OcJRw.png" /></figure><p>List the current directory using ls command to see generated folder.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/569/1*nWMDuTbZ0OKyoPborHDcjA.png" /></figure><p>cd to the generated folder and set permissions to execute for init_script.sh file. <strong>Folder name will be different according to your domain name.</strong></p><pre>cd livekit.kesaralive.com/<br>chmod +x init_script.sh</pre><p>Then run the script using below command.</p><pre>./init_script.sh</pre><p>It will take some time for the DNS to propagate and for everything to be set up.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/220/1*HnPTFvWwQ3NDEsfAHXW-5A.gif" /></figure><p>After a successful deployment, you will be able to see an <strong>“OK”</strong> message when you browse your Livekit URL.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/709/1*R0NzPJXUdKy7ce8q9EWTvA.png" /><figcaption>OK message</figcaption></figure><p>Then install the livekit-cli to generate a room token.</p><pre>curl -sSL https://get.livekit.io/cli | bash</pre><p>Use your <strong>API credentials</strong> generated by the previous steps to create the token.</p><pre>livekit-cli create-token \<br>    --api-key &lt;project_api_key&gt; --api-secret &lt;project_secret_key&gt; \<br>    --join --room my-first-room --identity user1 \<br>    --valid-for 24h</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/923/1*r2AynzgwOYpLAMb_3U3_yg.png" /><figcaption>room accessToken</figcaption></figure><p>You can test the server using Livekit’s <a href="https://meet.livekit.io/?tab=custom">example app</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/636/1*9BwIzFb9VY_AeD_yHpJFDA.png" /><figcaption>Livekit’s example app</figcaption></figure><p>Click the <strong>“Connect”</strong> button and join to the room. 🧨</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UBN73H8znv3qPno_fSc7CQ.png" /></figure><p>LiveKit provides official client SDKs for JavaScript, React, iOS/MacOS (Swift), Android (Kotlin), Flutter, Unity (WebGL). You can use the Livekit server you built in your projects using these client SDKs.</p><p>You can see all the available SDKs using the below link.</p><p><a href="https://docs.livekit.io/references/client-sdks/">https://docs.livekit.io/references/client-sdks/</a></p><p>If you want another tutorial creating a simple app using the server we’ve built, Let me know in the comments section.</p><p>Let’s meet with another informative article soon.</p><p>Until then, Happy Coding!</p><p>Follow me on LinkedIn: <a href="https://www.linkedin.com/in/kesaralive">https://www.linkedin.com/in/kesaralive</a></p><h3>Resources</h3><p><strong>Livekit CLI Setup:</strong> <a href="https://docs.livekit.io/getting-started/cli-setup/">https://docs.livekit.io/getting-started/cli-setup/</a><br><strong>Livekit Deployment to a VM:</strong> <a href="https://docs.livekit.io/oss/deployment/vm/">https://docs.livekit.io/oss/deployment/vm/</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=52c5b3746a3e" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Diving Deeper into Docker Networking with Docker Compose]]></title>
            <link>https://medium.com/@kesaralive/diving-deeper-into-docker-networking-with-docker-compose-737e3b8a3c8c?source=rss-d60194e755a2------2</link>
            <guid isPermaLink="false">https://medium.com/p/737e3b8a3c8c</guid>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[networking]]></category>
            <category><![CDATA[docker-networking]]></category>
            <category><![CDATA[docker-compose]]></category>
            <category><![CDATA[devops]]></category>
            <dc:creator><![CDATA[Kesara Karannagoda]]></dc:creator>
            <pubDate>Tue, 25 Jul 2023 09:57:35 GMT</pubDate>
            <atom:updated>2023-07-25T17:34:34.116Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ocml9MWofurZ1AfuoWlK5Q.jpeg" /><figcaption>Docker Networking</figcaption></figure><p>In my previous article, I made a commitment to explore Docker networking, and now I’m delivering on that promise. In this new article, we’ll dive into the workings of Docker networking and gain a better understanding of how it operates.</p><h3>Network Modes</h3><p>In Docker, We have several Docker network types available:</p><ol><li><strong>Bridge :</strong> The default type of network in Docker. It provides isolation between containers, but also allows containers to communicate with each other.</li><li><strong>Host : </strong>A special type of network that allows containers to connect directly to the host machine’s network stack.</li><li><strong>Overlay :</strong> A more complex type of network that allows containers to communicate with each other even if they are not running on the same host machine.</li><li><strong>Macvlan: </strong>This network allows you to specify a MAC address for the container, allowing the container to be a physical device in the network.</li><li><strong>Network plugins:</strong> You can install and use third-party network plugins.</li><li><strong>None:</strong> This won’t create any new network environment. If the none driver is used, the container can only use the local network (Your computer network).</li></ol><p>In this article, I’m planning to cover the Bridge and the Host Network modes.</p><h3>The Default Bridge Network</h3><p>When you install Docker on your local computer, it will create a new network called <strong>Docker Bridge</strong>. You can check this by running the following command in your terminal.</p><p>In Linux Terminal:</p><pre>ip address ls</pre><p>You’ll see the <strong>docker0</strong>network interface there.</p><p>Additionally, you can run the following Docker command to list all the Docker networks available on your computer. (Windows &amp; Linux)</p><pre>docker network ls</pre><p>You will get an output like the one below.</p><p>If you’ve worked with Docker for some time, it might show you some additional networks alongside it. We’ll discuss those in more detail later in this article.</p><pre>NETWORK ID          NAME                DRIVER              SCOPE<br>17c1d0d4956b        bridge              bridge              local<br>f04d3c0691be        host                host                local<br>cea56a468e79        none                null                local</pre><p>In simple terms, the above output tells us about a connection between two entities known as a <strong>“bridge network”</strong> and a <strong>“bridge driver.”</strong> It’s essential to understand that the network and the driver are linked, but they are not same components. The output also indicates that the <strong>bridge network</strong> has a local scope, meaning it exists only on this Docker host.</p><p>All the bridge networks offer a <strong>single host network</strong>, It means that the network is limited to enabling communication and connectivity among containers running on the same host. It does not extend to other hosts or computers in the network. This approach provides isolation and enhances security for the containers, allowing them to interact exclusively within the confines of the same system.</p><p>However, containers running on different hosts cannot directly communicate with each other using the bridge network. For <strong>multi-host communication</strong>, Docker offers alternative network drivers, such as <strong>overlay networks</strong>. These overlay networks enable containers across multiple hosts to communicate with each other over a distributed network, facilitating seamless communication in a clustered and distributed Docker environment.</p><p>Now, let’s see how this behavior works by doing a simple tutorial.</p><h3>Docker Compose: Default Bridge Network Tutorial</h3><h4>Prerequisites</h4><p>I hope you’ve installed docker in your computer. If not follow the official documentation. <a href="https://docs.docker.com/engine/install/">https://docs.docker.com/engine/install/</a></p><p><strong>In this article, I will frequently refer to the following terms:</strong></p><ol><li><strong>Mars: </strong>Planet in our solar system, used for a network name.</li><li><strong>Phobos: </strong>One of the moons of Mars, used as a network node (container service).</li><li><strong>Deimos: </strong>Another moon of Mars, used as a network node (container service).</li><li><strong>Earth: </strong>Our planet, used for a network name.</li><li><strong>Moon: </strong>Our moon, used as a network node (container service).</li></ol><p>First, we need to have a docker-compose.yaml file to get started. <br>In this tutorial, we will create the following &quot;<strong>Mars</strong>&quot; network by the end of the process.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/380/1*ODrqIUrGuUScTL2gjuPqNQ.png" /><figcaption>Mars Network</figcaption></figure><ol><li>First, Create a new project folder in your local computer.</li><li>Within the project folder, create a new file named docker-compose.yaml.</li><li>Now, copy and paste the following code snippet into your docker-compose.yaml file</li></ol><p><a href="https://github.com/kesaralive/docker-networking/tree/main/section-01"><strong>docker-compose.yaml</strong></a></p><pre>version: &quot;3&quot;<br>services:<br>  phobos:<br>    container_name: phobos<br>    image: busybox<br>    command: sleep infinity<br>  deimos:<br>    container_name: deimos<br>    image: busybox<br>    command: sleep infinity<br></pre><p>We will use<a href="https://hub.docker.com/_/busybox"> ‘Busybox’</a> docker image in this tutorial. It’s a linux docker image coming in somewhere between 1 and 5 Mb in on-disk size. It’s small and it contains ‘iputils-ping’ package. Simply, It comes with the ping command.</p><p>With the ping command, you can determine whether a remote destination IP is active or inactive. You can also find the round-trip delay in communicating with the destination and check whether there is a packet loss. Basically, You can check the connectivity between two destinations (computers/ nodes).</p><h4>Let’s see what’s inside our docker-compose.yaml file.</h4><p><strong>version: &quot;3&quot;</strong>: This line specifies the version of the Docker Compose file format being used, which is version 3 in this case.</p><p><strong>services</strong>: This section defines the individual services that make up the application.</p><p><strong>phobos</strong>: This is the name of the first service, which corresponds to a container named &quot;phobos&quot;. <em>Phobos is one of moons of the Mars.</em></p><p><strong>container_name: phobos</strong>: This specifies the name of the Docker container that will be created for the &quot;phobos&quot; service. The container&#39;s name will be &quot;phobos&quot;.</p><p><strong>image: busybox</strong>: This line indicates that the &quot;phobos&quot; service will use the &quot;busybox&quot; image as its base image. BusyBox is a lightweight and minimal container image often used for debugging purposes or as a base image for other containers.</p><p><strong>command: sleep infinity</strong>: This command is executed when the &quot;phobos&quot; container starts. In this case, the command is &quot;sleep infinity&quot;, which means the container will do nothing and remain running indefinitely, essentially acting as a background process.</p><ul><li><strong>deimos </strong>service is also as same as the the phobos.</li></ul><p>Now Let’s open the terminal in the project directory and execute the docker compose up -dcommand to <strong>build</strong> and <strong>run</strong> the both services.</p><pre>docker compose up -d</pre><p>We add -d at the end of the command to run all the containers in detach mode. You can check the running containers by using the docker-compose ps command.</p><pre>$ docker compose ps<br>NAME                IMAGE               COMMAND             SERVICE             CREATED             STATUS              PORTS<br>deimos              busybox             &quot;sleep infinity&quot;    deimos              17 hours ago        Up 17 hours         <br>phobos              busybox             &quot;sleep infinity&quot;    phobos              17 hours ago        Up 17 hours         </pre><p>Now you should be able to see two running containers.</p><p>If you run docker network ls now, you&#39;ll notice that it creates a new network named &lt;your-project-folder&gt;_default. In my case, it&#39;s docker-networking_default because I named my project folder as docker-networking.</p><pre>$ docker network ls<br>NETWORK ID     NAME                        DRIVER    SCOPE<br>17c1d0d4956b   bridge                      bridge    local<br>eeadb6a2dc43   docker-networking_default   bridge    local<br>f04d3c0691be   host                        host      local<br>cea56a468e79   none                        null      local</pre><p>Like I said in <a href="https://medium.com/@kesaralive/getting-started-with-docker-compose-hands-on-project-experience-e562ab07e24c#:~:text=Docker%20Compose%20understands,over%20that%20network.">my last article</a>,</p><blockquote><strong>Docker Compose understands the idea behind running services for one application on one network.</strong></blockquote><p>This is precisely what happens when you build and run your containers using <strong>Docker Compose</strong>. Docker Compose’s standard behavior involves creating a <strong>new bridge network</strong> specifically for its containers. This bridge network enables seamless communication and connectivity between the containers defined in the docker-compose.yaml file, facilitating a well-organized and isolated environment for the applications to run.</p><h3>Connectivity</h3><p>First of all, Let’s inspect our new network created by docker compose using the following command.</p><p>⚠️ <strong><em>Note: When you follow this tutorial, you might receive different IP addresses for your containers. If you prefer copy and paste, please carefully use the appropriate IP addresses from your output while continuing with this tutorial. The IP addresses may differ depending on your system and configuration, so it’s important to handle them carefully to ensure a successful implementation of the tutorial.</em></strong></p><pre>docker network inspect &lt;newly-created-network&gt;</pre><pre>$ docker network inspect docker-networking_default<br>[<br>    {<br>        &quot;Name&quot;: &quot;docker-networking_default&quot;,<br>        &quot;Id&quot;: &quot;1bdb95b66268640cf272174afa966913db2ad40ac38553dc6511028b51952652&quot;,<br>        &quot;Created&quot;: &quot;2023-07-22T10:34:07.181685944+05:30&quot;,<br>        &quot;Scope&quot;: &quot;local&quot;,<br>        &quot;Driver&quot;: &quot;bridge&quot;,<br>        &quot;EnableIPv6&quot;: false,<br>        &quot;IPAM&quot;: {<br>            &quot;Driver&quot;: &quot;default&quot;,<br>            &quot;Options&quot;: null,<br>            &quot;Config&quot;: [<br>                {<br>                    &quot;Subnet&quot;: &quot;172.18.0.0/16&quot;,<br>                    &quot;Gateway&quot;: &quot;172.18.0.1&quot;<br>                }<br>            ]<br>        },<br>        &quot;Internal&quot;: false,<br>        &quot;Attachable&quot;: false,<br>        &quot;Ingress&quot;: false,<br>        &quot;ConfigFrom&quot;: {<br>            &quot;Network&quot;: &quot;&quot;<br>        },<br>        &quot;ConfigOnly&quot;: false,<br>        &quot;Containers&quot;: {<br>            &quot;687a21b31cf4715391f3858e86c4c43c4a254359faa4aa05bf002194e036e5d0&quot;: {<br>                &quot;Name&quot;: &quot;phobos&quot;,<br>                &quot;EndpointID&quot;: &quot;8039fdaa2c7bdb87b2afbb2fad48bed9aa1b42d9e1c3f7ce1f31773a70c32e31&quot;,<br>                &quot;MacAddress&quot;: &quot;02:42:ac:12:00:03&quot;,<br>                &quot;IPv4Address&quot;: &quot;172.18.0.3/16&quot;,<br>                &quot;IPv6Address&quot;: &quot;&quot;<br>            },<br>            &quot;9fa4b494b685eb3557cbeca86c40cb4dbabf9a6bd4b8a6e62e36f9f6fe2b50ea&quot;: {<br>                &quot;Name&quot;: &quot;deimos&quot;,<br>                &quot;EndpointID&quot;: &quot;e529ce9c375d41d7c9717f9abb3820a553d39da327f3b79126f0eafc0ef81cd3&quot;,<br>                &quot;MacAddress&quot;: &quot;02:42:ac:12:00:02&quot;,<br>                &quot;IPv4Address&quot;: &quot;172.18.0.2/16&quot;,<br>                &quot;IPv6Address&quot;: &quot;&quot;<br>            }<br>        },<br>        &quot;Options&quot;: {},<br>        &quot;Labels&quot;: {<br>            &quot;com.docker.compose.network&quot;: &quot;default&quot;,<br>            &quot;com.docker.compose.project&quot;: &quot;docker-networking&quot;,<br>            &quot;com.docker.compose.version&quot;: &quot;2.18.1&quot;<br>        }<br>    }<br>]</pre><p>You will receive a <strong>JSON</strong> output similar to the snippet above. As you can see under the “<strong>Containers</strong>” section, we have our “phobos” and “deimos” containers, and each of them has been <strong>assigned to an individual IP address from the same network</strong>. This illustrates that the containers are successfully connected and can communicate with each other using the designated network.</p><h3>Now let’s check the connectivity between the two containers.</h3><p>Run the docker exec -it &lt;container&gt; /bin/sh command to start an interactive shell session.</p><pre>docker exec -it phobos /bin/sh</pre><p>The exec sub-command of Docker is used to run a command inside a running container.</p><p>-it These are two separate options provided together.</p><p>The -i represents --interactive and it keeps the standard input (stdin) open even if not attached, and the -t represents --tty which enables an interactive shell session by allocating a pseudo-terminal.</p><p>It will then execute the specified command /bin/sh inside that container, which starts an interactive shell session. In our case, It’s /bin/sh because <strong>Busybox</strong> image has “sh” installed by default. This allows you to work directly with the command-line interface inside the container, just as if you were logged in to a virtual machine or a remote server.</p><p>Just for the sake of easiness, I extracted the below part from the previous output. <strong>The results might vary for you, so please keep that in mind.</strong></p><pre>&quot;Containers&quot;: <br>{<br>&quot;687a21b31cf4715391f3858e86c4c43c4a254359faa4aa05bf002194e036e5d0&quot;: <br>  {<br>    &quot;Name&quot;: &quot;phobos&quot;,<br>    &quot;EndpointID&quot;: &quot;8039fdaa2c7bdb87b2afbb2fad48bed9aa1b42d9e1c3f7ce1f31773a70c32e31&quot;,<br>    &quot;MacAddress&quot;: &quot;02:42:ac:12:00:03&quot;,<br>    &quot;IPv4Address&quot;: &quot;172.18.0.3/16&quot;,<br>    &quot;IPv6Address&quot;: &quot;&quot;<br>  },<br>&quot;9fa4b494b685eb3557cbeca86c40cb4dbabf9a6bd4b8a6e62e36f9f6fe2b50ea&quot;: <br>  {<br>    &quot;Name&quot;: &quot;deimos&quot;,<br>    &quot;EndpointID&quot;: &quot;e529ce9c375d41d7c9717f9abb3820a553d39da327f3b79126f0eafc0ef81cd3&quot;,<br>    &quot;MacAddress&quot;: &quot;02:42:ac:12:00:02&quot;,<br>    &quot;IPv4Address&quot;: &quot;172.18.0.2/16&quot;,<br>    &quot;IPv6Address&quot;: &quot;&quot;<br>  }<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/782/1*yvQlWaUrtlfYBiM29tdnog.png" /><figcaption>pinging phobos -&gt; deimos</figcaption></figure><blockquote>Ping operates by means of Internet Control Message Protocol (ICMP) packets. Pinging involves sending an ICMP echo request to the target host and waiting for an ICMP echo reply.</blockquote><p><strong>For networking enthusiasts:</strong> ICMP packets pass through the Gateway of 172.18.0.1. The above figure is simply to help you understand our next steps and what we aim to achieve.</p><p>First, let’s run the command ip address ls to validate the above output obtained from using the docker network inspect command. As you can see in Terminal 1.0, you will see the same IP addresses displayed as shown in the above output.</p><p><strong>phobos : 172.18.0.3/16</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/740/1*jynnth7-qL70Itq2PnerTQ.png" /><figcaption>docker exec -it phobos /bin/sh (Terminal 1.0)</figcaption></figure><p>Now, let’s test the connectivity between two containers by calling “<strong>deimos</strong>” at IP address <strong>172.18.0.2/16</strong> from “<strong>phobos</strong>” at IP address <strong>172.18.0.3/16</strong>, just like in the figure shown in Terminal 2.0.</p><pre>ping 172.18.0.2</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/740/1*q85MJm-WL1Rk8-QUjDkISg.png" /><figcaption>ping 172.18.0.2 (Terminal 2.0)</figcaption></figure><p>As shown in the figure Terminal 2.0, if you have followed all the steps correctly so far, you should now be able to successfully transmit packets between the two containers, “<strong>phobos</strong>” and “<strong>deimos</strong>.” This demonstrates the successful establishment of communication between the containers over the created bridge network.</p><h4>Docker DNS</h4><p>When you create a Docker network, Like we discuss earlier, Docker automatically sets up a <strong>bridge network</strong>, which acts as a <strong>virtual network switch</strong> connecting all containers in that network. The <strong>Docker daemon </strong>acts as the <strong>DHCP server</strong> for this bridge network, providing IP addresses to containers and handling their <strong>DNS</strong> resolution.</p><h4><strong>Docker daemon:</strong></h4><p>AKA “<strong>Dockerd</strong>,” is a core component of the Docker platform. It is responsible for managing Docker objects like images, containers, networks, and volumes, and it acts as the <strong><em>intermediary between the Docker CLI (Command Line Interface) and the container runtime</em></strong>. The Docker daemon runs as a background process on the host machine and listens for Docker API requests.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/944/1*QR6xQ5YmDr-gtxEaJgqm9A.png" /><figcaption>Docker Overview</figcaption></figure><h4>DHCP server</h4><p>A DHCP server is a computer program that automatically assigns IP addresses to devices on a network, simplifying the process of connecting to and communicating with other devices.</p><h4>DNS server</h4><p>A DNS server is a server that translates human-readable domain names (like medium.com) into numerical IP addresses, allowing devices to find and access websites and services on the internet.</p><p>If you run ns lookup medium.com command in your terminal, you’ll be able to see it’s numerical IP address. You can use it to inspect ip address of a domain name.</p><h3>Let’s call deimos again!</h3><p>If you call “<strong>deimos</strong>” by its name in the same interactive terminal, you will notice that it resolves to the same IP address as before (Terminal 3.0). This behavior occurs because Docker’s internal DNS resolution allows containers to communicate with each other using their assigned names, which are mapped to their respective IP addresses within the Docker network.</p><pre>ping deimos</pre><p>Since we’ve given the <strong>container_name</strong> as <strong>deimos</strong> in our docker-compose.yaml we can call our container using it’s name.</p><blockquote>Note: you don’t have to worry about the <strong><em>ip configuration</em></strong> of a container in a docker network. You can call the container by its name.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/740/1*j8_W5ZcCyEUZipaRna626Q.png" /><figcaption>ping deimos (Terminal 3.0)</figcaption></figure><p>You can exit from the interactive terminal by entering the exit it it.</p><pre>exit</pre><h3>Now let’s call phobos from deimos</h3><p>Like we did in the previous step, we can create an interactive shell session in deimos by running the below command (Terminal 4.0).</p><pre>docker exec -it deimos /bin/sh</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/740/1*uq91dkmxN56tBTMTG2Pk4g.png" /><figcaption>docker exec -it deimos /bin/sh (Terminal 4.0)</figcaption></figure><p>Then try to ping phobos from <strong>deimos </strong>(Terminal 5.0)</p><pre>ping phobos</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/740/1*PE4bxB0zYfw_omodHB7bMg.png" /><figcaption>ping phobos (Terminal 5.0)</figcaption></figure><p>You should be able to see packets transmitting without any loss.</p><h3>Docker Compose: User-Defined Network</h3><p>So far we didn’t specify any networks in our docker-compose.yaml . Eventhough <strong>phobos</strong> &amp; <strong>deimos</strong> can talk to eachother, It’s not yet in the <strong>Mars network</strong>.</p><p>So far, we haven’t specified any networks in our docker-compose.yaml. Even if the &quot;<strong>phobos</strong>&quot; and &quot;<strong>deimos</strong>&quot; can communicate with each other, they are not yet part of the <strong>Mars network</strong>.</p><p>Before going to connect them to <strong>Mars Network</strong>, run the command docker compose down in your project directory to remove <strong>phobos</strong> &amp; <strong>deimos</strong> from current configuration.</p><pre>$ docker compose down<br>[+] Running 2/2<br> ✔ Container deimos  Removed                                                                                                                                                                      10.3s <br> ✔ Container phobos  Removed  </pre><p>Let’s add “<strong>phobos</strong>” and “<strong>deimos</strong>” to the Mars network. To do that, update the docker-compose.yaml file with the following code snippet:</p><p><a href="https://github.com/kesaralive/docker-networking/tree/main/section-02"><strong>docker-compose.yaml</strong></a></p><pre>version: &quot;3&quot;<br>services:<br>  deimos:<br>    container_name: deimos<br>    image: busybox<br>    command: sleep infinity<br>    networks:<br>      - mars<br>  phobos:<br>    container_name: phobos<br>    image: busybox<br>    command: sleep infinity<br>    networks:<br>      - mars<br>networks:<br>  mars:<br>    name: mars<br>    driver: bridge</pre><p>Then run the command docker compose up -d again in the terminal inside your project directory.</p><pre>$ docker compose up -d<br>[+] Running 3/3<br> ✔ Network mars      Created                                                                                                                                                                       0.1s <br> ✔ Container phobos  Started                                                                                                                                                                       0.6s <br> ✔ Container deimos  Started  </pre><p>Now, If you run docker network ls command, You’ll see our newly created mars network there (Terminal 6.0).</p><pre>docker network ls</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/739/1*2k7bJn21XOS4ujIq6_cDvQ.png" /><figcaption>docker network ls (Terminal 6.0)</figcaption></figure><p>If we look at the mars network using docker network inspect mars you’ll see that <strong>phobos</strong> &amp; <strong>deimos</strong> inside that network.</p><pre>$ docker network inspect mars<br>[<br>    {<br>        &quot;Name&quot;: &quot;mars&quot;,<br>        &quot;Id&quot;: &quot;9dc9407778df9b2bc336a9d4c912c869592889aeb46ae08050a09c81d7e1828d&quot;,<br>        &quot;Created&quot;: &quot;2023-07-22T13:42:23.084152643+05:30&quot;,<br>        &quot;Scope&quot;: &quot;local&quot;,<br>        &quot;Driver&quot;: &quot;bridge&quot;,<br>        &quot;EnableIPv6&quot;: false,<br>        &quot;IPAM&quot;: {<br>            &quot;Driver&quot;: &quot;default&quot;,<br>            &quot;Options&quot;: null,<br>            &quot;Config&quot;: [<br>                {<br>                    &quot;Subnet&quot;: &quot;172.21.0.0/16&quot;,<br>                    &quot;Gateway&quot;: &quot;172.21.0.1&quot;<br>                }<br>            ]<br>        },<br>        &quot;Internal&quot;: false,<br>        &quot;Attachable&quot;: false,<br>        &quot;Ingress&quot;: false,<br>        &quot;ConfigFrom&quot;: {<br>            &quot;Network&quot;: &quot;&quot;<br>        },<br>        &quot;ConfigOnly&quot;: false,<br>        &quot;Containers&quot;: {<br>            &quot;a82fa15fa31548cf3f92e6c90174f1d4a78d7b743186e6f05922c0d1a7d58eb7&quot;: {<br>                &quot;Name&quot;: &quot;phobos&quot;,<br>                &quot;EndpointID&quot;: &quot;1045e43d6c87e9e245dadaa7d62aed56268320820848b2fd330e9dfe4cd2b4bc&quot;,<br>                &quot;MacAddress&quot;: &quot;02:42:ac:15:00:03&quot;,<br>                &quot;IPv4Address&quot;: &quot;172.21.0.3/16&quot;,<br>                &quot;IPv6Address&quot;: &quot;&quot;<br>            },<br>            &quot;aadefe0c9ada96fbf61e1f0f5c3d68dfc4e3685f500f771f147c46bb1e37fb16&quot;: {<br>                &quot;Name&quot;: &quot;deimos&quot;,<br>                &quot;EndpointID&quot;: &quot;a4dbcb28acc4025438201ce55133a3effe1f6695583d92882a721f6a255dadfa&quot;,<br>                &quot;MacAddress&quot;: &quot;02:42:ac:15:00:02&quot;,<br>                &quot;IPv4Address&quot;: &quot;172.21.0.2/16&quot;,<br>                &quot;IPv6Address&quot;: &quot;&quot;<br>            }<br>        },<br>        &quot;Options&quot;: {},<br>        &quot;Labels&quot;: {<br>            &quot;com.docker.compose.network&quot;: &quot;mars&quot;,<br>            &quot;com.docker.compose.project&quot;: &quot;docker-networking&quot;,<br>            &quot;com.docker.compose.version&quot;: &quot;2.18.1&quot;<br>        }<br>    }<br>]</pre><p>If you try to check the connectivity between phobos and deimos . It will work like before we tested in the default bridge network.</p><p>If you check the connectivity between “<strong>phobos</strong>” and “<strong>deimos</strong>” now, it will work just like before when we tested in the default bridge network. This is because both containers are now part of the “<strong>mars</strong>” bridge network, which enables them to communicate with each other within this designated network.</p><h4>Earth Network</h4><p>Let’s add “<strong>Moon</strong>” and “<strong>Earth</strong>” networks to our docker-compose.yaml file. Below is the updated code snippet:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/540/1*nkiY4Oe9CgWeAiZH5rOaGQ.png" /><figcaption>Moon Network</figcaption></figure><p><a href="https://github.com/kesaralive/docker-networking/tree/main/section-03"><strong>docker-compose.yaml</strong></a></p><pre>version: &quot;3&quot;<br>services:<br>  deimos:<br>    container_name: deimos<br>    image: busybox<br>    command: sleep infinity<br>    networks:<br>      - mars<br>  phobos:<br>    container_name: phobos<br>    image: busybox<br>    command: sleep infinity<br>    networks:<br>      - mars<br>  moon:<br>    container_name: moon <br>    image: busybox <br>    command: sleep infinity<br>    networks:<br>      - earth<br>networks:<br>  mars:<br>    name: mars<br>    driver: bridge<br>  earth:<br>    name: earth <br>    driver: bridge</pre><ol><li>Use docker compose down to stop &amp; remove previous configuration.</li><li>RUN docker compose up -d to build and run all the containers.</li><li>RUN docker compose ps see the running containers.</li></ol><pre>$ docker compose ps<br>NAME                IMAGE               COMMAND             SERVICE             CREATED             STATUS              PORTS<br>deimos              busybox             &quot;sleep infinity&quot;    deimos              7 seconds ago       Up 5 seconds        <br>moon                busybox             &quot;sleep infinity&quot;    moon                7 seconds ago       Up 5 seconds        <br>phobos              busybox             &quot;sleep infinity&quot;    phobos              7 seconds ago       Up 5 seconds    </pre><p>4. RUN docker network ls to list all the docker networks. You’ll see that our <strong>mars</strong> &amp; <strong>earth</strong> networks listed there.</p><pre>$ docker network ls<br>NETWORK ID     NAME                        DRIVER    SCOPE<br>1d540324eaff   bridge                      bridge    local<br>1bdb95b66268   docker-networking_default   bridge    local<br>ad19e866e8ad   earth                       bridge    local<br>f04d3c0691be   host                        host      local<br>31a102c179b7   mars                        bridge    local<br>cea56a468e79   none                        null      local</pre><p>5. Let’s try to talk to the <strong>moon (Earth Network) </strong>from the <strong>phobos (Mars Network)</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/618/0*Y5wjPyrMPd9i1wak.png" /></figure><p>Create a interactive shell session in <strong>phobos</strong> and ping to the <strong>moon. </strong>(Terminal 7.0)</p><pre>docker exec -it phobos /bin/sh</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/739/1*noNFpKklT06qoHKBctIAsA.png" /><figcaption>Terminal 7.0</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qCtOQ3B3fHPirn8g2twFbw.png" /><figcaption>Phobos -&gt; Moon</figcaption></figure><p>You will notice that you can’t communicate with “<strong>moon</strong>” from “<strong>phobos</strong>,” but you can still communicate with “<strong>deimos</strong>.” This is because “<strong>deimos</strong>” is part of the same “<strong>Mars Network</strong>” as “<strong>phobos</strong>,” while “<strong>moon</strong>” is in a separate network, “<strong>Earth Network</strong>,” which is not directly accessible from “<strong>Mars Network.</strong>”</p><p>5. Let’s inspect the “<strong>Earth Network</strong>”<strong> </strong>using docker network inspect earth<strong> </strong>command.</p><pre>$ docker network inspect earth<br>[<br>    {<br>        &quot;Name&quot;: &quot;earth&quot;,<br>        &quot;Id&quot;: &quot;ad19e866e8add2936f047fd8ba6315cbded2b5ba8ca544afbc6df0cb5e79ecf4&quot;,<br>        &quot;Created&quot;: &quot;2023-07-22T14:26:47.928073948+05:30&quot;,<br>        &quot;Scope&quot;: &quot;local&quot;,<br>        &quot;Driver&quot;: &quot;bridge&quot;,<br>        &quot;EnableIPv6&quot;: false,<br>        &quot;IPAM&quot;: {<br>            &quot;Driver&quot;: &quot;default&quot;,<br>            &quot;Options&quot;: null,<br>            &quot;Config&quot;: [<br>                {<br>                    &quot;Subnet&quot;: &quot;172.23.0.0/16&quot;,<br>                    &quot;Gateway&quot;: &quot;172.23.0.1&quot;<br>                }<br>            ]<br>        },<br>        &quot;Internal&quot;: false,<br>        &quot;Attachable&quot;: false,<br>        &quot;Ingress&quot;: false,<br>        &quot;ConfigFrom&quot;: {<br>            &quot;Network&quot;: &quot;&quot;<br>        },<br>        &quot;ConfigOnly&quot;: false,<br>        &quot;Containers&quot;: {<br>            &quot;7cc84d8f2854565ab2a63d7aee508861b73539494187b504b4b70f520f7d7af3&quot;: {<br>                &quot;Name&quot;: &quot;moon&quot;,<br>                &quot;EndpointID&quot;: &quot;86f87b3f0c6a79ade3f301d3811ea4c916e9104821bb7636914172a6f1194960&quot;,<br>                &quot;MacAddress&quot;: &quot;02:42:ac:17:00:02&quot;,<br>                &quot;IPv4Address&quot;: &quot;172.23.0.2/16&quot;,<br>                &quot;IPv6Address&quot;: &quot;&quot;<br>            }<br>        },<br>        &quot;Options&quot;: {},<br>        &quot;Labels&quot;: {<br>            &quot;com.docker.compose.network&quot;: &quot;earth&quot;,<br>            &quot;com.docker.compose.project&quot;: &quot;docker-networking&quot;,<br>            &quot;com.docker.compose.version&quot;: &quot;2.18.1&quot;<br>        }<br>    }<br>]</pre><p>6. Let’s inspect <strong>“Mars Network”</strong> too. command: docker network inspect mars</p><pre>$ docker network inspect mars<br>[<br>    {<br>        &quot;Name&quot;: &quot;mars&quot;,<br>        &quot;Id&quot;: &quot;31a102c179b70a6d9b855da6422e0797fa527069f0c324bdc622de5f88805e0c&quot;,<br>        &quot;Created&quot;: &quot;2023-07-22T14:26:47.842542312+05:30&quot;,<br>        &quot;Scope&quot;: &quot;local&quot;,<br>        &quot;Driver&quot;: &quot;bridge&quot;,<br>        &quot;EnableIPv6&quot;: false,<br>        &quot;IPAM&quot;: {<br>            &quot;Driver&quot;: &quot;default&quot;,<br>            &quot;Options&quot;: null,<br>            &quot;Config&quot;: [<br>                {<br>                    &quot;Subnet&quot;: &quot;172.22.0.0/16&quot;,<br>                    &quot;Gateway&quot;: &quot;172.22.0.1&quot;<br>                }<br>            ]<br>        },<br>        &quot;Internal&quot;: false,<br>        &quot;Attachable&quot;: false,<br>        &quot;Ingress&quot;: false,<br>        &quot;ConfigFrom&quot;: {<br>            &quot;Network&quot;: &quot;&quot;<br>        },<br>        &quot;ConfigOnly&quot;: false,<br>        &quot;Containers&quot;: {<br>            &quot;3a20082a7fafca313d04c397ef2389831dc4cf5224e3ca9c0c05bd062e631d9d&quot;: {<br>                &quot;Name&quot;: &quot;deimos&quot;,<br>                &quot;EndpointID&quot;: &quot;c61ea9206b1f2406402401ee0be8b1ba7e6d540116d21a51cff0c061371a1d10&quot;,<br>                &quot;MacAddress&quot;: &quot;02:42:ac:16:00:03&quot;,<br>                &quot;IPv4Address&quot;: &quot;172.22.0.3/16&quot;,<br>                &quot;IPv6Address&quot;: &quot;&quot;<br>            },<br>            &quot;89ebfa3426dd31e6b466a86e8f26796e736f093dbef13f10799c869b242a8202&quot;: {<br>                &quot;Name&quot;: &quot;phobos&quot;,<br>                &quot;EndpointID&quot;: &quot;ecf6b66097b19985ed8467dc6056911c0fb16e0eb0938c0dfa66b6e2f10bb025&quot;,<br>                &quot;MacAddress&quot;: &quot;02:42:ac:16:00:02&quot;,<br>                &quot;IPv4Address&quot;: &quot;172.22.0.2/16&quot;,<br>                &quot;IPv6Address&quot;: &quot;&quot;<br>            }<br>        },<br>        &quot;Options&quot;: {},<br>        &quot;Labels&quot;: {<br>            &quot;com.docker.compose.network&quot;: &quot;mars&quot;,<br>            &quot;com.docker.compose.project&quot;: &quot;docker-networking&quot;,<br>            &quot;com.docker.compose.version&quot;: &quot;2.18.1&quot;<br>        }<br>    }<br>]</pre><p>You’ll clearly see that these two networks are two different subnets.</p><p><strong>Mars Network</strong>: 172.22.0.0/16</p><p><strong>Earth Network</strong>: 172.23.0.0/16</p><p><strong>Mars</strong> doesn’t know how to talk to <strong>Earth</strong>. That’s why we couldn’t talk to the <strong>moon</strong> from <strong>phobos</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WpJ2-3lBp9ij9JsbM_w78Q.png" /><figcaption>Mars doesn’t know how to talk to Earth</figcaption></figure><p><strong>Phobos</strong> sends ICMP requests to the <strong>Deimos</strong> through the <strong>Mars Network</strong>, Since there’s no connection between <strong>Earth Network</strong> and <strong>Mars Network</strong> <strong>Phobos</strong> can’t send any ICMP requests to the <strong>Moon</strong>.</p><p>To communicate between two networks, It’s better to use <strong>overlay network</strong> mode.</p><h4>By using a user-defined bridge network, You can achieve the isolation.</h4><p>Containers connected to a user-defined bridge network are isolated from other networks by default. This ensures that communication between containers is limited to the network they belong to, enhancing security and preventing unintended access.</p><h3>Host Network Explanation (Only works on Linux)</h3><p>⚠️ Note:<a href="https://docs.docker.com/network/network-tutorial-host/#prerequisites:~:text=The%20host%20networking%20driver%20only%20works%20on%20Linux%20hosts%2C%20and%20is%20not%20supported%20on%20Docker%20Desktop%20for%20Mac%2C%20Docker%20Desktop%20for%20Windows%2C%20or%20Docker%20EE%20for%20Windows%20Server."> Host network driver only works on Linux Hosts</a><br>✅ 1st Part will work in any OS.</p><p>Before going to talk about host networks, Let’s see how we mapped our container ports to the host machine’s ports previously in a default bridge network.</p><p>To understand the docker Host Network, we’ll use the <a href="https://hub.docker.com/_/nginx">nginx docker image</a> in this tutorial.</p><h4>Nginx Image</h4><p>Nginx (pronounced “engine-x”) is an open source reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer, HTTP cache, and a web server (origin server).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/936/1*inDr0SKJgZCgTWMy6FsP1w.png" /><figcaption>Nginx server is listening for incoming HTTP requests on port 80</figcaption></figure><p>By default, Nginx server will listen for incoming HTTP requests on port 80. When a client (such as a web browser) sends an HTTP request to the server’s IP address or domain name, Nginx will receive the request on port 80, process it, and return the appropriate response back to the client.</p><p>Like that in nginx docker image also listens for incoming HTTP requests on port 80. So when we build an nginx container in a docker bridge network, we have to map it’s port 80 to a port in our host machine (your computer).</p><h4>In the below example, I’m going to map nginx container’s port 80 to the host machine’s port 8000.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/704/1*XSMON2gWQ293n5PzEoEoQg.png" /><figcaption>nginx port 80 -&gt; host machine port 8000 mapping</figcaption></figure><ol><li>Create a new project folder. (You can name it as you want, I’m naming it docker-host-network)</li><li>Create a new docker-compose.yaml file and add the bellow code snippet to it.</li></ol><p><a href="https://github.com/kesaralive/docker-networking/tree/main/section-04"><strong>docker-compose.yaml</strong></a></p><pre>version: &quot;3&quot;<br>services:<br>  web:<br>    container_name: hello_world<br>    image: nginx<br>    ports:<br>      - 8000:80</pre><p>3. RUN the command docker compose up -d to build and run the new container.</p><p>As we learned before, this will create a default bridge network for the nginx container. Our container will be named “<strong>hello_world</strong>”. In the port mapping, we’re exposing our container’s port 80 on the Host machine’s port 8000.</p><p>If you run docker compose ps in your terminal, you’ll see that port mapping there.</p><pre>$ docker compose ps<br>NAME                IMAGE               COMMAND                  SERVICE             CREATED             STATUS              PORTS<br>hello_world         nginx               &quot;/docker-entrypoint.…&quot;   web                 5 seconds ago       Up 3 seconds        0.0.0.0:8000-&gt;80/tcp, :::8000-&gt;80/tcp</pre><p>If you visit <a href="http://localhost:8000/">http://localhost:8000</a> you’ll see the nginx welcome page there.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/714/1*XCywme1oV4yV_7m-mIuT3A.png" /><figcaption>nginx default welcome page.</figcaption></figure><p>By using port mapping we can access containers in bridge networks from our local computer (Host machine).</p><h4>Bridge network container -&gt; Host network container</h4><p>Now, let’s see what happens when we move our “<strong>hello_world</strong>” container (Nginx container) to a <strong>host network.</strong></p><ol><li>First, Let’s stop and remove the previous container by running docker compose down command.</li><li>Then, You have to update your docker-compose.yaml like in the below code snippet.</li></ol><p><a href="https://github.com/kesaralive/docker-networking/tree/main/section-05"><strong>docker-compose.yaml</strong></a></p><pre>version: &quot;3&quot;<br>services:<br>  web:<br>    container_name: hello_world<br>    image: nginx<br>    network_mode: host</pre><p>3. By setting network_mode to <strong>host </strong>you can add a container to the host network in docker compose.</p><p>4. Let’s build &amp; run the “hello_world” container again by running docker compose up -d.</p><pre>docker compose up -d</pre><p>5. If you run the command docker compose ps now, You won’t see the port mapping there.</p><pre>$ docker compose ps<br>NAME                IMAGE               COMMAND                  SERVICE             CREATED             STATUS              PORTS<br>hello_world         nginx               &quot;/docker-entrypoint.…&quot;   web                 31 seconds ago      Up 29 seconds       </pre><p>6. But if you access the <a href="http://localhost/">http://localhost/</a> or <a href="http://127.0.0.1/">http://127.0.0.1</a>/ in your computer, You’ll be able to access the same page that we’ve accessed before in port 8000.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/714/1*xtLTt8HxbFHNlpu-1iITlw.png" /><figcaption>Nginx default welcome page on <a href="http://127.0.0.1/">http://127.0.0.1</a></figcaption></figure><p>In a <strong>Host Network</strong>, It allows a Docker container to share the network namespace with the host system like in the below figure. When a container is run in host network mode, it bypasses Docker’s virtual network interface and directly uses the network interfaces of the host machine. This means that the container will have the same network stack as the host, including the same IP address and network interfaces.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/704/1*_qI1K4L5jsa63xELf8MaMg.png" /><figcaption>nginx container (hello_world container) in the Host Network</figcaption></figure><p>You might be new to the networking world, and if you don’t have a networking background, you might not know that port 80 is a well-known port for the HTTP protocol. In the networking realm, ports can be categorized into three main types based on their numbers.</p><ol><li>Well-Known Ports (0–1023)</li><li>Registered Ports (1024–49151)</li><li>Dynamic or Private Ports (49152–65535)</li></ol><p>Well-Known ports are standardized ports that have been assigned by the <a href="https://iana.org/">Internet Assigned Numbers Authority</a> (IANA) to specific services and applications. As mentioned earlier, they include ports like 21 (FTP), 22 (SSH), 80 (HTTP), 443 (HTTPS), etc. They are commonly used for widely recognized protocols and services on the internet.</p><p>That’s why we didn’t have to enter it like <a href="http://localhost/">http://localhost:80/</a> or <a href="http://127.0.0.1/">http://127.0.0.1:80/</a> in the browser. If you do that, it will automatically redirects you to <a href="http://localhost/">http://localhost/</a> or <a href="http://127.0.0.1/">http://127.0.0.1</a>/ respectively.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/714/1*MWquL4wMfYSsgiL8zgn-bg.png" /><figcaption><a href="http://localhost/">http://localhost:80</a> -&gt; http://localhost</figcaption></figure><p>If you run the command docker network inspect host you’ll see that our <strong>“hello_world”</strong> container there.</p><pre>$ docker network inspect host<br>[<br>    {<br>        &quot;Name&quot;: &quot;host&quot;,<br>        &quot;Id&quot;: &quot;f04d3c0691be5113708f0facee709111d1a34567287e0fd13811349ff67fd599&quot;,<br>        &quot;Created&quot;: &quot;2023-06-19T00:21:55.411437884+05:30&quot;,<br>        &quot;Scope&quot;: &quot;local&quot;,<br>        &quot;Driver&quot;: &quot;host&quot;,<br>        &quot;EnableIPv6&quot;: false,<br>        &quot;IPAM&quot;: {<br>            &quot;Driver&quot;: &quot;default&quot;,<br>            &quot;Options&quot;: null,<br>            &quot;Config&quot;: []<br>        },<br>        &quot;Internal&quot;: false,<br>        &quot;Attachable&quot;: false,<br>        &quot;Ingress&quot;: false,<br>        &quot;ConfigFrom&quot;: {<br>            &quot;Network&quot;: &quot;&quot;<br>        },<br>        &quot;ConfigOnly&quot;: false,<br>        &quot;Containers&quot;: {<br>            &quot;f071b8951047855ab9a3225455e0fd4d459e60a510f41ba921e7277ac886b41c&quot;: {<br>                &quot;Name&quot;: &quot;hello_world&quot;,<br>                &quot;EndpointID&quot;: &quot;0fc2921ec1503464a153459c470f3a35247a5cdbe323553f44bb1199f099c37f&quot;,<br>                &quot;MacAddress&quot;: &quot;&quot;,<br>                &quot;IPv4Address&quot;: &quot;&quot;,<br>                &quot;IPv6Address&quot;: &quot;&quot;<br>            }<br>        },<br>        &quot;Options&quot;: {},<br>        &quot;Labels&quot;: {}<br>    }<br>]</pre><h3>That’s how the Docker Host Network Magic works!</h3><p>If you like to take a further step and try to change the Nginx’s default welcome page, You can do it by using <strong>volumes.</strong></p><ol><li>RUN docker compose down to stop and remove the previous container.</li><li>UPDATE your docker-compose.yaml by adding a <strong>volumes:</strong> field like in the below code snippet.</li></ol><p><a href="https://github.com/kesaralive/docker-networking/tree/main/section-06"><strong>docker-compose.yaml</strong></a></p><pre>version: &quot;3&quot;<br>services:<br>  web:<br>    container_name: hello_world<br>    image: nginx<br>    network_mode: host<br>    volumes:<br>      - ./src:/usr/share/nginx/html/</pre><p>3. Then add a folder called src inside your project directory.</p><p>4. Add a index.html file inside it.</p><p><strong>index.html</strong></p><pre>&lt;html lang=&quot;en&quot;&gt;<br>  &lt;head&gt;<br>    &lt;meta charset=&quot;UTF-8&quot; /&gt;<br>    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;<br>    &lt;title&gt;Hello, World!&lt;/title&gt;<br>  &lt;/head&gt;<br>  &lt;body&gt;<br>    &lt;h1&gt;Hello, World&lt;/h1&gt;<br>  &lt;/body&gt;<br>&lt;/html&gt;</pre><p>5. Now you should have a project directory like below.</p><pre>.<br>├── src                # source directory. (.html/.js/.css)<br>│   ├── index.html          # html page <br>└── docker-compose.yml</pre><p>6. RUN the command docker compose up -d and see the changes in your browser.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/714/1*x5TkGEfHzDc3XU9hkAW9WA.png" /><figcaption>hello, world! page in http://localhost/</figcaption></figure><p>Cheers!!! ✨</p><h3>Conclusion</h3><p>In conclusion, this article has focused on two fundamental Docker networking modes: the bridge network and the host network. By exploring the unique characteristics of each, we gained valuable insights into how containers communicate and connect within these network configurations. Whether you’re looking for isolated container communication using bridge networks or opting for direct host network integration, understanding these concepts is crucial in building efficient and secure containerized applications. I hope this article has provided you with the foundational knowledge to navigate Docker networking effectively and confidently.</p><p>Happy networking! 🚀</p><h3>Resources:</h3><p><strong>GitHub Repository: </strong><a href="https://github.com/kesaralive/docker-networking">https://github.com/kesaralive/docker-networking</a></p><p><strong>Find me on LinkedIn:</strong> <a href="https://www.linkedin.com/in/kesaralive">https://www.linkedin.com/in/kesaralive</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=737e3b8a3c8c" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building a Simple 3-Tier Architecture with Docker Compose: A Hands-On Project Journey]]></title>
            <link>https://medium.com/@kesaralive/getting-started-with-docker-compose-hands-on-project-experience-e562ab07e24c?source=rss-d60194e755a2------2</link>
            <guid isPermaLink="false">https://medium.com/p/e562ab07e24c</guid>
            <category><![CDATA[docker-image]]></category>
            <category><![CDATA[docker-compose]]></category>
            <category><![CDATA[dockerfiles]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[devops]]></category>
            <dc:creator><![CDATA[Kesara Karannagoda]]></dc:creator>
            <pubDate>Sun, 09 Jul 2023 17:31:21 GMT</pubDate>
            <atom:updated>2023-07-10T02:15:15.485Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4289uHm7FsVia2ILxTeFIA.png" /></figure><p>It’s been a long time since I posted my last article about Docker. If you haven’t had the chance to read it yet, I would like to extend an invitation for you to explore it (<a href="https://medium.com/@kesaralive/from-os-hassles-to-team-harmony-how-docker-saved-our-group-project-e37c52ce8509">From OS Hassles to Team Harmony: How Docker Saved Our Group Project</a>). This article offers valuable insights into Docker and how it played a pivotal role in the successful containerization of our university group project.</p><blockquote>Initially, my intention was to provide a tutorial outlining the process of containerizing our project. However, I have taken this opportunity to incorporate additional details that were not available to me at the time of doing my project. I hope you find this enhanced version enjoyable and informative as you navigate through it.</blockquote><h3>Prerequisites:</h3><p>Before proceeding with this tutorial, please ensure that you have met the following prerequisites:</p><ol><li><strong>An active Internet connection:</strong> You will need a reliable Internet connection to access the necessary resources and documentation throughout the tutorial.</li><li><strong>Docker installation:</strong> Make sure that Docker is installed on your machine. Docker enables the creation and management of containers, which is crucial for following along with the tutorial. <strong>You can follow the below instructions to install it.</strong></li></ol><h3><strong>Docker Installation</strong></h3><p>Please run the following commands in your terminal (for Linux and MacOS) or PowerShell (for windows) in order to check the presence of Docker installation on your computer.</p><pre>$ docker version<br>$ docker compose version</pre><p><strong>Docker </strong>is a platform that allows you to create and manage containers, while <strong>Docker Compose</strong> is a tool specifically designed to simplify the management of multi-container applications.</p><blockquote><strong><em>Docker Compose comes with the docker installation. You don’t have to install it separately.</em></strong></blockquote><p>In case Docker is not installed on your computer, kindly refer to the instructions provided in the official Docker documentation for proper installation guidance. <a href="https://docs.docker.com/engine/install/">Docker Installation (Official Docker Documentation)</a></p><h4><strong>A Simple 3-Tier Architecture</strong></h4><p>Today, our objective is to build a basic 3-tier architecture using Docker containers. This architecture has three components.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tqwqXKLrfgxJlQSf9FlnjA.png" /><figcaption>A simple 3-tier architecture using docker containers.</figcaption></figure><ol><li>A frontend container</li><li>A backend container (With an API layer)</li><li>A MySQL database container</li></ol><h3>Frontend App Container</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N4F8TUyVl_yjeH63FeBv1A.png" /><figcaption>frontend container</figcaption></figure><p>To start our project, Let’s create a new folder to maintain it as our main project folder (You can name it as you want. Ex: <strong><em>docker-tutorial</em></strong>). Within the main folder, we’ll create another folder called <strong>frontend</strong> to handle our frontend application. After that we can add a simple <strong>index.html</strong> file inside the frontend folder. This will help us to check whether the frontend app is working or not once it is deployed.</p><p><strong>index.html</strong></p><pre>&lt;html lang=&quot;en&quot;&gt;<br>  &lt;head&gt;<br>    &lt;meta charset=&quot;UTF-8&quot; /&gt;<br>    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;<br>    &lt;title&gt;HTML Web Page&lt;/title&gt;<br>  &lt;/head&gt;<br>  &lt;body&gt;<br>    &lt;h1&gt;Hello, World!&lt;/h1&gt;<br>  &lt;/body&gt;<br>&lt;/html&gt;</pre><p>Next, we will create a new file named <strong>docker-compose.yml</strong> in our main project directory as our configuration file for deploying the frontend application within a Docker container.</p><p><strong>docker-compose.yml</strong></p><pre>version: &quot;3&quot;<br>services:<br>  frontend:<br>    image: httpd:latest<br>    volumes:<br>      - &quot;./frontend:/usr/local/apache2/htdocs&quot;<br>    ports:<br>      - 3000:80</pre><p>Let’s discuss a little bit about our <strong>docker-compose.yml</strong> configuration.</p><ul><li><strong>version: “3”</strong> : This tells us the version of the Docker Compose file format.</li><li><strong>services</strong> : This section contains the services to be deployed. (In our case, frontend container)</li><li><strong>frontend</strong> : This is the name of the service. You can choose any name you prefer. Ex: frontend-service</li><li><strong>image: httpd:latest</strong> : This line tells the Docker image to be used for the frontend service. In our case, we use the httpd image with the latest tag. httpd image is the official image for the Apache HTTP Server. You can find more images in <a href="https://hub.docker.com/">Docker Hub</a>.</li><li><strong>volumes:</strong> : This section is used to mount the local filesystem directory (File directory in your computer) to the container.</li><li><strong>- “./frontend:/usr/local/apache2/htdocs”</strong> : This line specifies the volume mapping. It maps the ./frontend directory from the local filesystem to the /usr/local/apache2/htdocs directory inside the container. This allows the container to access the files in the frontend directory.</li><li><strong>ports: </strong>: This section defines the port mapping between the host (Your computer) and the container.</li><li><strong>- 3000:80</strong> : This line maps port 3000 of the host machine (Your computer) to port 80 of the container. It means that when the container is running, you can access the frontend application on <a href="http://localhost:3000/">http://localhost:3000</a> in your web browser.</li></ul><h4>Main Project Directory</h4><p>After all the settings, you should have a folder structure like this.</p><pre>.<br>├── frontend                # Frontend files. (.html/.js/.css)<br>│   ├── index.html          # html page <br>└── docker-compose.yml</pre><h4>What is docker-compose?</h4><p>Docker compose is a tool that was developed to help define and share multi-container applications. docker-compose reads configuration data from a <strong>YAML</strong> file. You can learn more about docker-compose from <a href="https://docs.docker.com/compose/compose-file/compose-file-v3/">here.</a></p><p><strong>Next, Open up the terminal(Linux, Mac), or powershell(Windows) in your project directory, and run the command.</strong></p><pre>$ docker compose up</pre><blockquote><strong><em>Note!</em></strong></blockquote><blockquote>You need to have an Internet connection to perform above command for the first time.<br>First time setup will take some time because it has to pull the httpd container image from the <a href="https://hub.docker.com/">docker hub.</a></blockquote><p>At this point, you should be able to see your frontend application at <a href="http://localhost:3000/">http://localhost:3000/</a>. We’ve used <a href="https://hub.docker.com/_/httpd">httpd docker container image</a> to serve this simple frontend app.</p><p>You can press Ctrl + c in the terminal to stop all the containers.</p><h4>What is Docker Hub?</h4><p><a href="https://hub.docker.com/">Docker Hub</a> is the world’s largest container image library and community for container images. It has over 100,000 <strong>container images</strong> from various software vendors. And It allows developers to store, manage, and distribute Docker container images.</p><h4><strong>What is a Docker Image?</strong></h4><p>A Docker image is a lightweight, standalone, and executable package that contains all the necessary components, including the code, runtime environment, system tools, libraries, and dependencies, required to run a specific application. <strong>It serves as a blueprint for creating Docker containers.</strong></p><h3>Backend API Container</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*acZJtFH56Zv0VkUMsTGSrA.png" /><figcaption>backend container</figcaption></figure><p>Now Let’s create our backend api container,<br>To create the backend API container, we will follow these steps:</p><ol><li>Create a new folder named api inside the main project directory.</li><li>Inside the api folder, create a new PHP file named index.php.</li><li>COPY &amp; PASTE the below code snippet to your index.php file.</li></ol><p>Then let’s add a simple todos api endpoint in our index.php file.</p><p><strong>index.php</strong></p><pre>&lt;?php<br>// Set the response content type to JSON<br>header(&#39;Content-Type: application/json&#39;);<br>header(&#39;Access-Control-Allow-Origin: http://localhost:3000&#39;);<br>header(&#39;Access-Control-Allow-Methods: GET, POST, OPTIONS&#39;);<br><br>// Create a response array<br>$response = [<br>    [<br>        &quot;_id&quot; =&gt; 1,<br>        &quot;todo&quot; =&gt; &quot;Todo 1&quot;<br>    ],<br>    [<br>        &quot;_id&quot; =&gt; 2,<br>        &quot;todo&quot; =&gt; &quot;Todo 2&quot;<br>    ],<br>    [<br>        &quot;_id&quot; =&gt; 3,<br>        &quot;todo&quot; =&gt; &quot;Todo 3&quot;<br>    ],<br>    [<br>        &quot;_id&quot; =&gt; 4,<br>        &quot;todo&quot; =&gt; &quot;Todo 4&quot;<br>    ]<br>];<br><br>// Encode the response array as JSON<br>$jsonResponse = json_encode($response);<br><br>// Output the JSON response<br>echo $jsonResponse;</pre><h4>Dockerfile</h4><p>In this case, I’m not going to make the Backend API container using the approach we’ve used to create our frontend container. We used a pre-existing Docker image directly from the Docker Hub to create the frontend container.</p><p>Instead, we will create the Backend API container by writing the Docker Build process in a <strong>Dockerfile</strong> located in the main project directory.</p><h4>What is a Dockerfile?</h4><p>Dockerfiles are used in Docker to define the configuration and instructions necessary to build a Docker image. Dockerfiles provide a simple and declarative way to automate the creation of Docker images, ensuring consistency and reproducibility across different environments.</p><p>We’ll talk more about Dockerfiles in my upcoming articles. For now, Let’s add the below Dockerfile inside our main project directory by naming it Dockerfile and <strong>you don&#39;t need to have an file extension for that file.</strong></p><blockquote><strong>Note!</strong><br>If you’re a beginner to <strong>Docker</strong>, You don’t have to worry about Dockerfile implementation.<br>For the purpose of this tutorial, you can simply copy and paste the provided code, and it will be sufficient to follow along.</blockquote><p><strong>Dockerfile</strong></p><pre>FROM ubuntu:20.04<br><br>LABEL maintainer=&quot;kesaralive@gmail.com&quot;<br>LABEL description=&quot;Apache / PHP development environment&quot;<br><br>ARG DEBIAN_FRONTEND=newt<br>RUN apt-get update &amp;&amp; apt-get install -y lsb-release &amp;&amp; apt-get clean all<br>RUN apt install ca-certificates apt-transport-https software-properties-common -y<br>RUN add-apt-repository ppa:ondrej/php<br><br>RUN apt-get -y update &amp;&amp; apt-get install -y \<br>apache2 \<br>php8.0 \<br>libapache2-mod-php8.0 \<br>php8.0-bcmath \<br>php8.0-gd \<br>php8.0-sqlite \<br>php8.0-mysql \<br>php8.0-curl \<br>php8.0-xml \<br>php8.0-mbstring \<br>php8.0-zip \<br>mcrypt \<br>nano<br><br>RUN apt-get install locales<br>RUN locale-gen fr_FR.UTF-8<br>RUN locale-gen en_US.UTF-8<br>RUN locale-gen de_DE.UTF-8<br><br># config PHP<br># we want a dev server which shows PHP errors<br>RUN sed -i -e &#39;s/^error_reporting\s*=.*/error_reporting = E_ALL/&#39; /etc/php/8.0/apache2/php.ini<br>RUN sed -i -e &#39;s/^display_errors\s*=.*/display_errors = On/&#39; /etc/php/8.0/apache2/php.ini<br>RUN sed -i -e &#39;s/^zlib.output_compression\s*=.*/zlib.output_compression = Off/&#39; /etc/php/8.0/apache2/php.ini<br><br># to be able to use &quot;nano&quot; with shell on &quot;docker exec -it [CONTAINER ID] bash&quot;<br>ENV TERM xterm<br><br># Apache conf<br># allow .htaccess with RewriteEngine<br>RUN a2enmod rewrite<br># to see live logs we do : docker logs -f [CONTAINER ID]<br># without the following line we get &quot;AH00558: apache2: Could not reliably determine the server&#39;s fully qualified domain name&quot;<br>RUN echo &quot;ServerName localhost&quot; &gt;&gt; /etc/apache2/apache2.conf<br># autorise .htaccess files<br>RUN sed -i &#39;/&lt;Directory \/var\/www\/&gt;/,/&lt;\/Directory&gt;/ s/AllowOverride None/AllowOverride All/&#39; /etc/apache2/apache2.conf<br><br>RUN chgrp -R www-data /var/www<br>RUN find /var/www -type d -exec chmod 775 {} +<br>RUN find /var/www -type f -exec chmod 664 {} +<br><br>EXPOSE 80<br><br># start Apache2 on image start<br>CMD [&quot;/usr/sbin/apache2ctl&quot;,&quot;-DFOREGROUND&quot;]</pre><p>After above change, your folder structure should look like this.</p><pre>.<br>├── api                     # Backend api files. <br>│   ├── index.php           # php page <br>├── frontend                # Frontend files. (.html/.js/.css)<br>│   ├── index.html          # html page <br>├── docker-compose.yml<br>└── Dockerfile</pre><h4>Adding the backend service to the docker-compose.yml</h4><p>Now Let’s update the docker-compose.yml file with below code.</p><p>You should add the below code snippet under the services section in your docker-compose.yml file. If you have any difficulties, feel free to visit the Github repository to the finalized version. <strong>You can find the GitHub repository link and all other resource links in the end of this article.</strong></p><pre>  backend:<br>    container_name: simple-backend<br>    build:<br>      context: ./<br>      dockerfile: Dockerfile<br>    volumes:<br>      - &quot;./api:/var/www/html/&quot;<br>    ports:<br>      - 5000:80</pre><ul><li><strong>backend:</strong>: This is the name of the service. It can be any name you choose.</li><li><strong>container_name: simple-backend</strong>: This line sets the name of the container to be created as &quot;simple-backend&quot;. You can use any desired name for your container.</li><li><strong>build:</strong>: This section indicates that we will be building the Docker image for the backend service.</li><li><strong>context: ./</strong>: It specifies the build context, which is the path to the directory containing the Dockerfile. Since we have our docker file in the main project directory it is set to the current directory (./).</li><li><strong>dockerfile: Dockerfile</strong>: This line specifies the filename of the Dockerfile to be used for building the image. In this case, it is named &quot;Dockerfile&quot;.</li><li><strong>volumes:</strong>: This section is used to mount the local filesystem directory to the container.</li><li><strong>- &quot;./api:/var/www/html/&quot;</strong>: This line maps the ./api directory from the local filesystem (Your computer) to the /var/www/html/ directory inside the container. (/var/www/html/ path is the default web root in apache web servers) It allows the container to access the files in the api directory.</li><li><strong>ports:</strong>: This section defines the port mapping between the host and the container.</li><li><strong>- 5000:80</strong>: This line maps port 5000 of the host machine to port 80 of the container. It means that when the container is running, you can access the backend API on <a href="http://localhost:5000/">http://localhost:5000</a> in your application.</li></ul><p>After adding it, Let’s rerun the terminal command docker compose up to build and run our frontend and backend containers.</p><p>If everything works well, You should be able to see your simple todos API at <a href="http://localhost:5000/">http://localhost:5000/</a> .</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/850/1*_RiObEJCcxL0Qc7adbkSMg.png" /><figcaption>todos API endpoint at http://localhost:5000</figcaption></figure><h3>Accessing the Backend from the Frontend</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iyn0_OMQQGdgG0wotvH8rg.png" /></figure><p>In order to call our backend API from frontend. We should allow access to our frontend in our backend code. We have already put it in the code. Below line helps frontend to call our simple todo api.</p><p><strong>./api/index.php</strong></p><pre>header(&#39;Access-Control-Allow-Origin: http://localhost:3000&#39;);</pre><blockquote><strong>Note!</strong><br>If you’re trying to expose the frontend from a different port instead of port 3000. Please change the above line in below example.<br><strong>Example: </strong>If you&#39;re trying to expose it in port 3001.<br>Update it as below.</blockquote><pre>header(&#39;Access-Control-Allow-Origin: http://localhost:3001&#39;);</pre><h4>Let’s update our frontend code as below to call the backend API</h4><pre>&lt;html lang=&quot;en&quot;&gt;<br>  &lt;head&gt;<br>    &lt;meta charset=&quot;UTF-8&quot; /&gt;<br>    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;<br>    &lt;title&gt;HTML Web Page&lt;/title&gt;<br>  &lt;/head&gt;<br>  &lt;body&gt;<br>    &lt;h1&gt;Hello, World!&lt;/h1&gt;<br>    &lt;div id=&quot;todos&quot;&gt;&lt;/div&gt;<br><br>    &lt;script&gt;<br>      fetch(&quot;http://localhost:5000/&quot;)<br>        .then((response) =&gt; response.json())<br>        .then((data) =&gt; {<br>          const todos = document.getElementById(&quot;todos&quot;);<br><br>          data?.forEach((item, index) =&gt; {<br>            const todo = document.createElement(&quot;h2&quot;);<br>            todo.textContent = item._id + &quot;. &quot; + item.todo;<br>            todos.appendChild(todo);<br>          });<br>        })<br>        .catch((error) =&gt; {<br>          console.log(&quot;Error: &quot;, error);<br>        });<br>    &lt;/script&gt;<br>  &lt;/body&gt;<br>&lt;/html&gt;</pre><p>Now you can check the result at <a href="http://localhost:3000/">http://localhost:3000/</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/850/1*p3obHMc_j4SmpWU2PGJycA.png" /><figcaption>frontend at http://localhost:3000/</figcaption></figure><p>Upto this point we’ve created our frontend container, backend container and called the backend API from the frontend. Next, Let’s see how we can add a database container to this.</p><h3>Adding a MySQL Database container</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FQODSAVC3557OFSDgnD9DQ.png" /><figcaption>database container</figcaption></figure><p>First, Let’s add the sql dump file that I’ve created for this tutorial. Create a folder named db in your main project directory and dump.sql file inside it. Then pase the below code to the dump.sql file and save it.</p><p><strong>dump.sql</strong></p><pre>-- phpMyAdmin SQL Dump<br>-- version 5.2.1<br>-- https://www.phpmyadmin.net/<br>--<br>-- Host: database:3306<br>-- Generation Time: Jul 08, 2023 at 05:46 PM<br>-- Server version: 8.0.33<br>-- PHP Version: 8.1.17<br><br>SET SQL_MODE = &quot;NO_AUTO_VALUE_ON_ZERO&quot;;<br>START TRANSACTION;<br>SET time_zone = &quot;+00:00&quot;;<br><br><br>/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;<br>/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;<br>/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;<br>/*!40101 SET NAMES utf8mb4 */;<br><br>--<br>-- Database: `todo_app`<br>--<br><br>-- --------------------------------------------------------<br><br>--<br>-- Table structure for table `todos`<br>--<br><br>CREATE TABLE `todos` (<br>  `_id` int NOT NULL,<br>  `todo` varchar(255) NOT NULL<br>) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;<br><br>--<br>-- Dumping data for table `todos`<br>--<br><br>INSERT INTO `todos` (`_id`, `todo`) VALUES<br>(1, &#39;I will wake up at 4 in the morining.&#39;),<br>(2, &#39;I will practice docker for 1 hour.&#39;),<br>(3, &#39;I will give time for 2 hours javascript.&#39;),<br>(4, &#39;Then I will have breakfast.&#39;),<br>(5, &#39;I will give time for 3 hours php.&#39;);<br><br>--<br>-- Indexes for dumped tables<br>--<br><br>--<br>-- Indexes for table `todos`<br>--<br>ALTER TABLE `todos`<br>  ADD PRIMARY KEY (`_id`);<br><br>--<br>-- AUTO_INCREMENT for dumped tables<br>--<br><br>--<br>-- AUTO_INCREMENT for table `todos`<br>--<br>ALTER TABLE `todos`<br>  MODIFY `_id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;<br>COMMIT;</pre><p>After above change, your folder structure should look like this.</p><pre>.<br>├── api                     # Backend api files. <br>│   ├── index.php           # php page <br>├── db                      # SQL dump files. <br>│   ├── dump.sql            # sql dump file <br>├── frontend                # Frontend files. (.html/.js/.css)<br>│   ├── index.html          # html page <br>├── docker-compose.yml<br>└── Dockerfile</pre><p>Let’s append below code snippet to our docker-compose.yml. You should append it under services section.</p><pre>  database:<br>    image: mysql:latest<br>    environment:<br>      MYSQL_DATABASE: todo_app<br>      MYSQL_USER: todo_admin<br>      MYSQL_PASSWORD: password<br>      MYSQL_ALLOW_EMPTY_PASSWORD: 1<br>    volumes:<br>      - &quot;./db:/docker-entrypoint-initdb.d&quot;</pre><ul><li><strong>database:</strong>: This is the name of the service. You can choose any name you prefer.</li><li><strong>image: mysql:latest</strong>: This line specifies the Docker image to be used for the database service. In this case, it uses the mysql image with the latest tag. The mysql image is the official image for MySQL.</li><li><strong>environment:</strong>: This section allows you to specify environment variables for the container.</li><li><strong>MYSQL_DATABASE: todo_app</strong>: This line sets the name of the MySQL database inside the container as &quot;todo_app&quot;. You can change it to any desired database name.</li><li><strong>MYSQL_USER: todo_admin</strong>: This line specifies the username for the MySQL user inside the container as &quot;todo_admin&quot;. You can modify it to any preferred username.</li><li><strong>MYSQL_PASSWORD: password</strong>: This line sets the password for the MySQL user as &quot;password&quot;. You can choose a different password for your MySQL user.</li><li><strong>MYSQL_ALLOW_EMPTY_PASSWORD: 1</strong>: This line allows an empty password for the MySQL user. Setting it to 1 enables this behavior.</li><li><strong>volumes:</strong>: This section is used to mount the local filesystem directory to the container.</li><li><strong>- &quot;./db:/docker-entrypoint-initdb.d&quot;</strong>: This line maps the ./db directory from the local filesystem to the /docker-entrypoint-initdb.d directory inside the container. It is typically used for initializing the database with SQL scripts or other initialization files.</li></ul><p>Now rerun the terminal command docker compose up. Then open a new terminal and type docker compose ps in your project directory. If you&#39;ve done everything correctly, you should be able to see all three containers running on your docker server.</p><h3>PhpMyAdmin to Access the MySQL Database Container</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vChkxEBwhMD6kQxcMlh6EA.png" /></figure><p>Next, Let’s add a browser interface to access your database. In this tutorial we use phpmyadmin to access our database. Let&#39;s add below code snippet to our docker-compose.yml file.</p><pre>  phpmyadmin:<br>    image: phpmyadmin/phpmyadmin<br>    ports:<br>      - 8080:80<br>    environment:<br>      - PMA_HOST=database<br>      - PMA_PORT=3306<br>    depends_on:<br>      - database</pre><p>Rerun the terminal command docker compose up</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/850/1*bqpXjYYQ3fTB5mC0iEsG3A.png" /><figcaption>phpMyAdmin at http://localhost:8080</figcaption></figure><p>Now you can access the database container through <a href="http://localhost:8080/">http://localhost:8080/</a> by using credentials that we&#39;ve given when creating our database container.</p><h3>Wiring up the Backend API with Database container</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qq5Ha3nezCgX080BLKt5vw.png" /></figure><h4>To make it work you need to do some changes in your api folder.</h4><ol><li>Create a folder name app inside the api folder.</li><li>Add below 3 files inside that app folder.</li></ol><p><strong>config.php</strong></p><pre>&lt;?php <br>define(&quot;DB_HOST&quot;, &quot;database&quot;);<br>define(&quot;DB_USERNAME&quot;, &quot;todo_admin&quot;);<br>define(&quot;DB_PASSWORD&quot;, &quot;password&quot;);<br>define(&quot;DB_NAME&quot;, &quot;todo_app&quot;);</pre><p><strong>Database.php</strong></p><pre>&lt;?php<br>class Database<br>{<br>    protected $connection = null;<br>    <br>    public function __construct()<br>    {<br>        try {<br>            $this-&gt;connection = new mysqli(DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME);<br>            if (mysqli_connect_errno()) {<br>                throw new Exception(&quot;Database connection failed!&quot;);<br>            }<br>        } catch (Exception $e) {<br>            throw new Exception($e-&gt;getMessage());<br>        }<br>    }<br><br>    private function executeStatement($query = &quot;&quot;, $params = [])<br>    {<br>        try {<br>            $stmt = $this-&gt;connection-&gt;prepare($query);<br>            if ($stmt === false) {<br>                throw new Exception(&quot;Statement preparation failure: &quot; . $query);<br>            }<br>            if ($params) {<br>                $stmt-&gt;bind_param($params[0], $params[1]);<br>            }<br>            $stmt-&gt;execute();<br>            return $stmt;<br>        } catch (Exception $e) {<br>            throw new Exception($e-&gt;getMessage());<br>        }<br>    }<br><br>    public function select($query = &quot;&quot;, $params = [])<br>    {<br>        try {<br>            $stmt = $this-&gt;executeStatement($query, $params);<br>            $result = $stmt-&gt;get_result()-&gt;fetch_all(MYSQLI_ASSOC);<br>            $stmt-&gt;close();<br>            return $result;<br>        } catch (Exception $e) {<br>            throw new Exception($e-&gt;getMessage());<br>        }<br>        return false;<br>    }<br>}</pre><p><strong>todos.php</strong></p><pre>&lt;?php <br>require_once &quot;./app/Database.php&quot;;<br><br>class Todo extends Database<br>{<br>    public function getTodos($limit)<br>    {<br>        return $this-&gt;select(&quot;SELECT * FROM todos&quot;);<br>    }<br>}</pre><p>To connect all together, let’s update our index.php by replacing with the below code snippet.</p><p><strong>index.php</strong></p><pre>&lt;?php<br>// Set the response content type to JSON<br>header(&#39;Content-Type: application/json&#39;);<br>header(&#39;Access-Control-Allow-Origin: http://localhost:3000&#39;);<br>header(&#39;Access-Control-Allow-Methods: GET, POST, OPTIONS&#39;);<br><br>require &quot;./app/config.php&quot;;<br>require_once &quot;./app/todos.php&quot;;<br><br>$todoModel = new Todo();<br>$response = $todoModel-&gt;getTodos(10);<br>// Create a response array<br><br>// Encode the response array as JSON<br>$jsonResponse = json_encode($response);<br><br>// Output the JSON response<br>echo $jsonResponse;</pre><p>Now, execute the command docker compose up in your terminal to see the changes. This will enable you to see the successful retrieval of todos from the backend to the frontend, as well as the backend’s seamless interaction with the database for data retrieval.</p><h4>Docker Compose Network</h4><p>If you’ve followed the tutorial correctly, you should have a docker-compose.yml file like this.</p><pre>version: &quot;3&quot;<br>services:<br>  frontend:<br>    image: httpd:latest<br>    volumes:<br>      - &quot;./frontend:/usr/local/apache2/htdocs&quot;<br>    ports:<br>      - 3000:80<br>  backend:<br>    container_name: simple-backend<br>    build:<br>      context: ./<br>      dockerfile: Dockerfile<br>    volumes:<br>      - &quot;./api:/var/www/html/&quot;<br>    ports:<br>      - 5000:80<br>  database:<br>    image: mysql:latest<br>    environment:<br>      MYSQL_DATABASE: todo_app<br>      MYSQL_USER: todo_admin<br>      MYSQL_PASSWORD: password<br>      MYSQL_ALLOW_EMPTY_PASSWORD: 1<br>    volumes:<br>      - &quot;./db:/docker-entrypoint-initdb.d&quot;<br>  phpmyadmin:<br>    image: phpmyadmin/phpmyadmin<br>    ports:<br>      - 8080:80<br>    environment:<br>      - PMA_HOST=database<br>      - PMA_PORT=3306<br>    depends_on:<br>      - database</pre><p><strong>Docker Compose understands the idea behind running services for one application on one network.</strong> When you deploy an app using Docker Compose file, even when there’s no mention of specific networking parameters, Docker Compose will create a new bridge network and deploy the container over that network.</p><p>Let’s discuss about docker networking in upcoming articles.</p><h3>Summary</h3><p>In this article, we covered several key concepts related to <strong>Docker</strong> and <strong>Docker Compose</strong>. We explored the fundamentals of Docker Compose and how it helps in managing multi-container applications. We discussed the role of <strong>Docker Hub</strong> as a centralized repository for <strong>Docker images</strong>. Additionally, we had a closer look into <strong>Dockerfiles</strong>, which allow us to define the configuration of a Docker image.</p><p>Furthermore, we examined how to add containers using Docker Compose, demonstrating how to define services within the docker-compose.yml file. We also explored how to establish connections between multiple containers in Docker Compose, enabling communication and interaction between them.</p><p>Lastly, we learned how to construct a simple 3-tier architecture using Docker Compose, incorporating frontend, backend API, and database containers. By leveraging the power of Docker Compose, we can easily orchestrate and manage the deployment of this architecture.</p><p>By gaining an understanding of these concepts and following the examples provided, you are now equipped to utilize Docker Compose effectively in building and deploying multi-container applications.</p><h3>Resources</h3><ul><li><strong>Docker Introduction Article:</strong> <a href="https://medium.com/@kesaralive/from-os-hassles-to-team-harmony-how-docker-saved-our-group-project-e37c52ce8509">https://medium.com/@kesaralive/from-os-hassles-to-team-harmony-how-docker-saved-our-group-project-e37c52ce8509</a></li><li><strong>Docker Installation Guide: </strong><a href="https://docs.docker.com/engine/install/">https://docs.docker.com/engine/install/</a></li><li><strong>Docker Compose :</strong> <a href="https://docs.docker.com/compose/compose-file/compose-file-v3/">https://docs.docker.com/compose/compose-file/compose-file-v3/</a></li><li><strong>GitHub Repository:</strong> <a href="https://github.com/kesaralive/docker-compose-101.git">https://github.com/kesaralive/docker-compose-101.git</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e562ab07e24c" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[From OS Hassles to Team Harmony: How Docker Saved Our Group Project]]></title>
            <link>https://medium.com/@kesaralive/from-os-hassles-to-team-harmony-how-docker-saved-our-group-project-e37c52ce8509?source=rss-d60194e755a2------2</link>
            <guid isPermaLink="false">https://medium.com/p/e37c52ce8509</guid>
            <category><![CDATA[how-to]]></category>
            <category><![CDATA[docker-container]]></category>
            <category><![CDATA[containerization]]></category>
            <category><![CDATA[docker-image]]></category>
            <category><![CDATA[docker]]></category>
            <dc:creator><![CDATA[Kesara Karannagoda]]></dc:creator>
            <pubDate>Sat, 17 Jun 2023 21:25:12 GMT</pubDate>
            <atom:updated>2023-06-17T21:25:12.761Z</atom:updated>
            <content:encoded><![CDATA[<p>Hey there, fellow IT enthusiast! If you’re part of the tech industry, chances are you’ve come across the buzzword <strong>“Docker”</strong> at some point. And let me tell you, it’s not just another fancy tech term — it’s actually a game-changer for developers all around the world!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*9Lxi_P20_wp7dQFU" /><figcaption>Photo by <a href="https://unsplash.com/@rubaitulazad?utm_source=medium&amp;utm_medium=referral">Rubaitul Azad</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h4><strong>First Let’s talk about the world before docker’s existence.</strong></h4><p>Back in the day, developers would create a system and send it to the testers to make sure everything worked perfectly. But there was a problem. The code they sent sometimes didn’t work properly on the tester’s computer because of differences in their computer setups.</p><p>It was really annoying! The developers would scratch their heads, trying to figure out why the code worked fine on their own computers but not on the tester’s. The testers had different operating systems, configurations, and software versions, and it caused issues with the code.</p><h4><strong>Why docker is the solution for this problem?</strong></h4><p>Docker is a tool which is used to automate the deployment of applications in lightweight containers so that applications can work efficiently in different environments. It’s an open-source platform that enables you to build, package, and run applications in containers.</p><h3>Don’t you understand what I’ve said above? Don’t worry I’ll tell you my side of the story.</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*40RYRdwY3VYQ4lCV" /><figcaption>Photo by <a href="https://unsplash.com/@jonasleupe?utm_source=medium&amp;utm_medium=referral">Jonas Leupe</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>Once upon a time, I had a group project with my awesome university friends. We were four undergraduate students working together on this exciting project. The only catch was that each one of us had a different operating system on our laptops. My friend had a fancy Mac Book with mac-OS, two others had Windows, and I was rocking Ubuntu.</p><p>Our project was a web application that included a front-end, a restful API service, and a MySQL database. To set up everything on our laptops, we had to go through the hassle of installing MySQL and the Apache Web server separately, but in different ways for each operating system. You can imagine the headaches we faced due to these differences in our environments.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*nZ2QO8bDimQwm1Nf" /><figcaption>Photo by <a href="https://unsplash.com/@punttim?utm_source=medium&amp;utm_medium=referral">Tim Gouw</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>But then, something magical happened! I came across Docker, and I had an idea. I suggested to my friends that we all install Docker on our laptops. They were curious but willing to give it a shot. So, we went ahead and installed Docker on each of our machines.</p><p>With Docker, we were able to containerize our front-end, back-end (the amazing Restful API service), and even the MySQL database separately. It was like putting each part of our application into its own little box, called a container.</p><h4>What is a container?</h4><p>Container is a software package that consists of all the dependencies required to run an application. Think of a container as a lightweight, standalone virtual environment that contains everything an application needs to run, including its code, system libraries.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*NVZrHtzaRp9yXwX6" /><figcaption>Photo by <a href="https://unsplash.com/@ventiviews?utm_source=medium&amp;utm_medium=referral">Venti Views</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>And guess what? We didn’t encounter any of those pesky problems we faced before! Docker made our lives so much easier because it allowed us to run our application in the exact same way, no matter what operating system we were using. It was like having a universal translator for our project.</p><p>No more worries about compatibility issues or struggling with different installation methods. Docker provided us with a smooth and consistent experience across all our laptops.</p><p>We were able to focus on building our web application together, knowing that it would work flawlessly on any machine. Docker truly saved the day and made our group project journey a whole lot smoother.</p><h4>What I’ve learned using Docker?</h4><p>The Major advantage of docker is its ability to ensure consistency across different environments. By packaging an application and its dependencies into a container, you can be confident that it will run the same way on any machine, regardless of the underlying operating system or infrastructure. Docker also allows for easy scalability, as you can run multiple containers simultaneously without any conflicts.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*v8X44rqgm3yZm3aw" /><figcaption>Photo by <a href="https://unsplash.com/@miabaker?utm_source=medium&amp;utm_medium=referral">Mia Baker</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>So, if you ever find yourself in a situation where you and your team have different operating systems, remember the magic of Docker. It can make your life so much easier, just like it did for us.</p><p>Let’s see how exactly docker helped me to solve the issue in the next article. Until then, Happy coding!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e37c52ce8509" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Art of learning anything better.]]></title>
            <link>https://medium.com/@kesaralive/art-of-learning-anything-better-cc9caa1e1816?source=rss-d60194e755a2------2</link>
            <guid isPermaLink="false">https://medium.com/p/cc9caa1e1816</guid>
            <category><![CDATA[learning]]></category>
            <category><![CDATA[self-learning]]></category>
            <category><![CDATA[self-improvement]]></category>
            <category><![CDATA[self]]></category>
            <category><![CDATA[understanding]]></category>
            <dc:creator><![CDATA[Kesara Karannagoda]]></dc:creator>
            <pubDate>Sat, 25 Sep 2021 18:02:55 GMT</pubDate>
            <atom:updated>2021-09-25T18:02:55.916Z</atom:updated>
            <content:encoded><![CDATA[<h4>This will help you to understand new things…</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*88Hj1fTpU5VeNNISWSmPFQ.jpeg" /></figure><p>This might help you to learn something well. Some of my friends were asking the question from me.</p><p>“How did you learn these things?” and Here I’m going to answer it.</p><p>Their question was even if I were not doing much academic work. How do I learn new things? I was thinking about the question for a long time and, I realized something that I can share with you. I will explain how I’m managing it.</p><h3><strong>What you have to do is…</strong></h3><p>Let’s say you want to learn something new.</p><p>First , what you have to do is. Find a problem in the domain area that you want to learn. Or you can imagine a new problem and then try to solve it.</p><p>When you are trying to solve the problem, you may struggle with it. On the other hand, you may get the answer by the first try. If you’re getting it by few attempts. you may lucky or good at the relevant domain area. If it happens, try to find a different problem. A harder one.</p><p>In simple words, what I have to tell you is if you’re struggling to solve something. That means you’re learning it well. but to get things done. you have to keep trying to solve it no matter what. <strong>Struggle</strong> is the key.</p><p>You need to understand the learning is not about how fast you take the solution to your problem. It’s about struggling to do something and figuring it out. In this case, time doesn’t matter at all. The more time you take to solve the problem. The more you learn.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ki3uBF8ujqjTUjYngO6X4Q.jpeg" /></figure><p>So far, we can say that struggle, learning and the time you take to solve the problem is equally proportional.</p><p>Let’s say if some is using this type of technique to learn things. Then they will be able to solve more problems in the same domain area. And also, they’ll be able to memorize things related to the domain faster than others. Since it’s an experienced-based learning technique. Your knowledge will last longer for decades.</p><h3><strong>Be honest to yourself.</strong></h3><p>Let’s say you’re a typical curriculum learner who’s learning things just to pass an exam. I bet you will forget a lot of things in months or years because you’re training your brain only for the exam. If you’re truly want to learn something you have to do it in a way like this. This is what I’m using to get it done.</p><blockquote>Never lie to yourself.</blockquote><p>When you’re at a problem always try to figure it on your own. If you need some help, you can read a book or simply you can find help on the internet. But make sure not to get the final answer directly from the reference.</p><h3>Tree Example</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3zg8J6tsDrI38BsuGS9uqw.jpeg" /></figure><p>Let’s imagine there’s an infected tree and we have to get rid of it. The tree is very large and it has so many branches and leaves. To get rid of this infection, you have to find a specific leaf in the tree.</p><p>First you have to start from the trunk and go to a branch by branch and search every leaf on it. And again, you may have to get down when you tired and try a different branch to find that specific leaf. Let’s assume the leaf you want to find won’t fall.</p><p>While you’re doing this you will get so much knowledge on this tree. If you took so much time to find the leaf. You will get to know about so many branches and its leaves. The more time you take to search it the more you learn.</p><p>Also, there’re more things to be considered as well.</p><p>When you’re doing this, you may take so much time. may be weeks, months or years. because this tree is huge. at that period, the tree can have new leaves and, also it may lose some older leaves.</p><p><em>It describes the changing of the world of knowledge. The world is changing faster and, new things are coming and old ones are going. So, you will have to study those things as well. Also, it describes that you can’t finish learning in a particular domain. You have to understand that as well.</em></p><p><em>If you’re thinking you don’t know everything about something. It’s okay to be like that. Because you can’t learn everything about anything. As a human being, our lifespan is smaller than what we’re trying to fulfill. but you can do many things by applying what you know.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*h4iCvni-XZu14IKvdeOlCA.jpeg" /></figure><p>As I said previously, let’s take the tree as the domain. The problem is the infection. The solution is one specific leaf and to get it. You have to go through many leaves and branches, which means you’re learning about the problem. In the end, you will gain a large amount of knowledge in the domain. You will solve your next in a lesser time.</p><p>To get the answer. There may be many paths, After finding it to get the solution. Try to find the best fit for it.</p><p>That’s how I’m managing to understand new things. Hope someone will get something from my words. Try to collect more trees. But I think If you can learn one tree well, it would be enough for your success than collecting more trees for your life.</p><blockquote>If you can study one tree better, you will be a master of that tree.</blockquote><p><em>Make a Forrest. wink!</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cc9caa1e1816" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>