<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Daniel</title>
    <description>The latest articles on DEV Community by Daniel (@onticdani).</description>
    <link>https://dev.to/onticdani</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1296107%2F4940219c-b30c-411a-b5bf-0bb6a56d45f4.jpeg</url>
      <title>DEV Community: Daniel</title>
      <link>https://dev.to/onticdani</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/onticdani"/>
    <language>en</language>
    <item>
      <title>The perfect Git strategy for continuous improvement / deployment</title>
      <dc:creator>Daniel</dc:creator>
      <pubDate>Mon, 24 Mar 2025 12:00:00 +0000</pubDate>
      <link>https://dev.to/onticdani/the-perfect-git-strategy-for-continuous-improvement-deployment-2cf6</link>
      <guid>https://dev.to/onticdani/the-perfect-git-strategy-for-continuous-improvement-deployment-2cf6</guid>
      <description>&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;1. Quick Overview&lt;/li&gt;
&lt;li&gt;2. Patching branches&lt;/li&gt;
&lt;li&gt;Conclusions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;For a long time, I struggled finding the perfect Git strategy for my projects.&lt;/p&gt;

&lt;p&gt;Many times I just felt overwhelmed by how complicated some of these strategies felt. "There must be a better way" I thought.&lt;/p&gt;

&lt;p&gt;Then I stumbled into the "Release Branching Strategy" on &lt;a href="http://releaseflow.org/" rel="noopener noreferrer"&gt;releaseflow&lt;/a&gt; and it was like a breath of fresh air.&lt;/p&gt;

&lt;p&gt;I will explain a simplified overview of this approach that just works for me, for a more detailed one feel free to check out their website.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Quick Overview
&lt;/h2&gt;

&lt;p&gt;Here are the main steps of this strategy if you want to add a new feature:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You open a new &lt;code&gt;feature/&lt;/code&gt; branch from the &lt;code&gt;main&lt;/code&gt; branch and commit changes on it&lt;/li&gt;
&lt;li&gt;You merge that branch directly into the &lt;code&gt;main&lt;/code&gt; branch&lt;/li&gt;
&lt;li&gt;You create a version tag on the &lt;code&gt;main&lt;/code&gt; branch i.e.: &lt;code&gt;1.0.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You can then trigger any automated actions (GitHub actions, Jenkins, CI/CD...) when this tag is pushed. For instance I &lt;a href="https://daniel.es/blog/automatically-build-docker-images-with-github-actions/" rel="noopener noreferrer"&gt;have an automated deployment using GitHub actions to build and push my docker images to the registry&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The version tags follow the &lt;a href="https://semver.org/" rel="noopener noreferrer"&gt;semver&lt;/a&gt; convention.&lt;br&gt;
&lt;code&gt;major.minor.patch&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;major&lt;/code&gt; when you create breaking changes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;minor&lt;/code&gt; when you create backward compatible changes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;patch&lt;/code&gt; when you need to fix a bug&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;This would be the sweet spot of this strategy:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvacnnm77eahcynj0tpu4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvacnnm77eahcynj0tpu4.png" alt="The sweet spot" width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But of course life is never this easy, and wheneve a feature is deployed it often comes with its bugs that we need to fix.&lt;/p&gt;

&lt;p&gt;What then?&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Patching branches
&lt;/h2&gt;

&lt;p&gt;So what happens when you need to patch a released version? And how do your roll those changes back into the &lt;code&gt;main&lt;/code&gt; branch?&lt;/p&gt;

&lt;p&gt;Here are the steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go back to the commit where you created the version tag and create a &lt;code&gt;release/1.0.x&lt;/code&gt; branch from there. We want to leave the &lt;code&gt;x&lt;/code&gt; to indicate that all patches will come from this branch.&lt;/li&gt;
&lt;li&gt;Develop any bug fixes or patches directly on this branch or feel free to open new &lt;code&gt;bugfix/&lt;/code&gt; branches from it. Ideally &lt;code&gt;bugfix/&lt;/code&gt; branches should only be created on &lt;code&gt;release/&lt;/code&gt; branches.&lt;/li&gt;
&lt;li&gt;Once you're done with your fixes, create a new version tag on the &lt;code&gt;release/&lt;/code&gt; branch, i.e.: &lt;code&gt;1.0.1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Then you can get those changes back into &lt;code&gt;main&lt;/code&gt; 2 ways:

&lt;ol&gt;
&lt;li&gt;If it's a simple thing you can just cherry pick the changes into &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If it's something more complicated you can merge those changes into &lt;code&gt;main&lt;/code&gt; and handle any merge conflicts you might have&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;It's all much clearer with a diagram:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;I've been using this Git flow since I found it and it's been great.&lt;/p&gt;

&lt;p&gt;Paired with a &lt;a href="https://daniel.es/blog/automatically-build-docker-images-with-github-actions/" rel="noopener noreferrer"&gt;GitHub action to build and push my docker images to a registry&lt;/a&gt; it's worked wonders.&lt;/p&gt;

&lt;p&gt;You can get crazy and create staging/production tags to deploy to different servers and test, or create snapshot tags to keep track of when new changes were started to be made.&lt;/p&gt;

&lt;p&gt;All while still keeping everything super simple.&lt;/p&gt;

</description>
      <category>git</category>
      <category>webdev</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Proxmox: How to use a USB drive as storage for your containers</title>
      <dc:creator>Daniel</dc:creator>
      <pubDate>Fri, 07 Feb 2025 20:31:48 +0000</pubDate>
      <link>https://dev.to/onticdani/proxmox-how-to-mount-a-usb-drive-n36</link>
      <guid>https://dev.to/onticdani/proxmox-how-to-mount-a-usb-drive-n36</guid>
      <description>&lt;p&gt;So you have a USB drive that you want to mount to a specific container.&lt;/p&gt;

&lt;p&gt;In my case, I want to mount a 10TB USB hard drive (a Seagate Expansion) where I store my media for radarr, sonnar and jellyfin.&lt;/p&gt;

&lt;p&gt;Let's get started!&lt;/p&gt;

&lt;h2&gt;
  
  
  Permanently mount your USB drive
&lt;/h2&gt;

&lt;p&gt;There are several steps you need to follow for a USB device to be permanently mounted to Proxmox. This is, to have it mounted even if the node restarts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Find the USB device by going to your node -&amp;gt; Disks
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk2i2w42v985vofla2gwj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk2i2w42v985vofla2gwj.png" alt="Disks in Proxmox node" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In my case it's easily identifiable, as I can see a USB device with 10TB of space: &lt;code&gt;/dev/sdb2&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Edit the &lt;code&gt;/etc/fstab&lt;/code&gt; file
&lt;/h3&gt;

&lt;p&gt;You can use &lt;code&gt;nano&lt;/code&gt; or your favorite editor and add the following line to the end of the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;file system&amp;gt; &amp;lt;mount point&amp;gt; &amp;lt;type&amp;gt; &amp;lt;options&amp;gt; &amp;lt;dump&amp;gt; &amp;lt;pass&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/dev/sdb2 /mnt/expansion xfs defaults,noatime,nofail 0 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means that I'm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mounting the device in &lt;code&gt;/dev/sdb2&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;Mounting it to the route &lt;code&gt;/mnt/expansion&lt;/code&gt; in my Proxmox host&lt;/li&gt;
&lt;li&gt;Mounting it as an &lt;code&gt;xfs&lt;/code&gt; file system&lt;/li&gt;
&lt;li&gt;With the options

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;defaults&lt;/code&gt; (enables several standard mount options)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;noatime&lt;/code&gt; (disables updating of file access times on the filesystem)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nofail&lt;/code&gt; (tells the system to continue booting even if this device cannot be mounted)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Dump &lt;code&gt;0&lt;/code&gt; means the filesystem will not be backed up by the dump utility&lt;/li&gt;

&lt;li&gt;File system check &lt;code&gt;2&lt;/code&gt; indicates this filesystem should be checked after the root filesystem&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: I previously formatted my external drive as &lt;code&gt;xfs&lt;/code&gt; before plugging it in to make my life easier since that format is directly compatible with my Proxmox host.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Mount it!
&lt;/h3&gt;

&lt;p&gt;You can either reboot your system or run &lt;code&gt;mount -a&lt;/code&gt; to finish mounting your device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Passing the device to the container
&lt;/h2&gt;

&lt;p&gt;Start by editing the configuration file for your container. Identify your container ID and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nano /etc/pve/lxc/&amp;lt;continer id&amp;gt;.conf 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this line to the end of the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Mount point id&amp;gt; &amp;lt;Host path&amp;gt;,mp=&amp;lt;container path&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my case I have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mp0: /mnt/expansion,mp=/home/media
mp1: /mnt/downloads,mp=/home/downloads
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;As you can see the mount points are sequential, so be mindful of the id you're choosing if you already have other mount points in your container&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Restart your container and you're good to go!&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify mount points
&lt;/h3&gt;

&lt;p&gt;If you now go to your container resources in the Proxmox GUI you should see your mountpoints being specified there:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0p0s3lv1s6331995r8vb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0p0s3lv1s6331995r8vb.png" alt="Container mount points" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>proxmox</category>
    </item>
    <item>
      <title>Set a VLAN interface in your Proxmox node</title>
      <dc:creator>Daniel</dc:creator>
      <pubDate>Fri, 07 Feb 2025 16:27:01 +0000</pubDate>
      <link>https://dev.to/onticdani/set-a-vlan-interface-in-your-proxmox-node-492n</link>
      <guid>https://dev.to/onticdani/set-a-vlan-interface-in-your-proxmox-node-492n</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;Proxmox network basics&lt;/li&gt;
&lt;li&gt;Making the Linux Bridge VLAN aware&lt;/li&gt;
&lt;li&gt;The VLAN interface&lt;/li&gt;
&lt;/ul&gt;



&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;First of all, install Proxmox. &lt;/p&gt;

&lt;p&gt;If you haven't and need a guide on how to do so, I can't recommend Network Chuck's video enough:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/_u8qTN3cCnQ?start=815"&gt;
&lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  Proxmox network basics
&lt;/h2&gt;

&lt;p&gt;Once that's out of the way, you need to know some basics about your Proxmox node networking stuff.&lt;/p&gt;

&lt;p&gt;Go to your Node -&amp;gt; System -&amp;gt; Network&lt;/p&gt;

&lt;p&gt;It will probably look something like this (don't worry if your IP address is different):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flf0in8sdbpvbffv9wi2x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flf0in8sdbpvbffv9wi2x.png" alt="Default node network tab" width="800" height="201"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What are those?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;enp1s0&lt;/code&gt; -&amp;gt; This is your actual, physical, network device in the hardware you installed Proxmox on. Note that you might have more physical interfaces if your hardware has several ethernet ports or a wifi card.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vmbr0&lt;/code&gt; -&amp;gt; This is what's called "Linux Bridge", it's like a virtual switch, that bridges (thus the name) the physical interface with all the virtual networking within Proxmox.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One important thing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;vmbr0&lt;/code&gt; interface is &lt;strong&gt;NOT&lt;/strong&gt; VLAN aware. Which means that it has no clue on how to handle VLAN packets and thus not know where to route stuff.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Making the Linux Bridge VLAN aware
&lt;/h2&gt;

&lt;p&gt;Edit the &lt;code&gt;vmbr0&lt;/code&gt; interface, enabling VLAN aware:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6fxkvq3i6kzv416sbaud.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6fxkvq3i6kzv416sbaud.png" alt="Enabling VLAN aware on vmbr0" width="800" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then hit "OK" and then "Apply Configuration".&lt;/p&gt;

&lt;p&gt;This will make &lt;code&gt;vmbr0&lt;/code&gt; a trunking bridge, meaning that it will pass all VLAN for all VLANs.&lt;/p&gt;

&lt;p&gt;Editing the network interfaces in the GUI, like we just did, makes changes in &lt;code&gt;/etc/network/interfaces&lt;/code&gt;. There, we can what changes have been made:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iface enp1s0 inet manual

auto vmbr0
iface vmbr0 inet static
        address 192.168.1.17/24
        gateway 192.168.1.1
        bridge-ports enp1s0
        bridge-stp off
        bridge-fd 0
        bridge-vlan-aware yes   &amp;lt;-- new line
        bridge-vids 2-4094      &amp;lt;-- new line
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, our &lt;code&gt;vmbr0&lt;/code&gt; now passes traffic for all vlans in the 2 to 4094 range. Great!&lt;/p&gt;

&lt;p&gt;If you wanted, you can set what specific vlans you want to pass by modifying the last line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bridge-vids 2,12,24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will now pass traffic for vlans 2, 12 and 24.&lt;/p&gt;

&lt;p&gt;This configuration will work out of the bat. You can now create containers and VMs with the vmbr0 as the bridge and set VLAN IP addresses to them.&lt;/p&gt;

&lt;p&gt;But what if you &lt;strong&gt;ALSO&lt;/strong&gt; want your node to have an IP address in one of those VLANs? Keep reading!&lt;/p&gt;

&lt;h2&gt;
  
  
  The VLAN interface
&lt;/h2&gt;

&lt;p&gt;The previous setup allows us to assign VLAN IPs to containers and VMs, but you might also want to assign a specific VLAN IP to the Proxmox Node?&lt;/p&gt;

&lt;p&gt;It's easy!&lt;/p&gt;

&lt;p&gt;First we need to edit the &lt;code&gt;vmbr0&lt;/code&gt; interface and remove the static IP information, like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ioo6vzyns8t5iac5t4x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ioo6vzyns8t5iac5t4x.png" alt="Removing vmbr0 static ip info" width="800" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then we hit "Create" and create a VLAN interface:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnru7ko7xf2qb2wu0ditu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnru7ko7xf2qb2wu0ditu.png" alt="Creating a new VLAN interface" width="586" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frljah2pingixuj2r2dlx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frljah2pingixuj2r2dlx.png" alt="Inserting VLAN interface data" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Insert your VLAN data&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the name, by setting &lt;code&gt;vlanX&lt;/code&gt;, where "X" is the VLAN number, Proxmox already detects it and puts it into the VLAN field&lt;/li&gt;
&lt;li&gt;Then just fill out the IP you want to assign to this Proxmox node in your VLAN and the gateway&lt;/li&gt;
&lt;li&gt;Finally, set the VLAN raw device to your linux bridge &lt;code&gt;vmbr0&lt;/code&gt; in my case&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then hit "Apply Configuration" to set the changes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note that this will immediately configure your node on the VLAN IP you just set, so you might want to navigate to that new IP to get to the GUI again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If we now have a look at &lt;code&gt;/etc/network/interfaces&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iface enp1s0 inet manual

auto vmbr0
iface vmbr0 inet manual
        bridge-ports enp1s0
        bridge-stp off
        bridge-fd 0
        bridge-vlan-aware yes
        bridge-vids 2-4094

auto vlan2
iface vlan2 inet static
        address 192.168.2.12/24
        gateway 192.168.2.1
        vlan-raw-device vmbr0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can see how we have a new interface that lets the Proxmox node live under the VLAN 2 IP address &lt;code&gt;192.168.2.12&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And that's pretty much it!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One last note: When creating containers and VMs, the interface you need to select is still &lt;code&gt;vmbr0&lt;/code&gt;, not the VLAN interface!&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>proxmox</category>
      <category>networking</category>
      <category>network</category>
      <category>virtualmachine</category>
    </item>
    <item>
      <title>Master Docker logging with Loki and Grafana</title>
      <dc:creator>Daniel</dc:creator>
      <pubDate>Tue, 15 Oct 2024 16:31:06 +0000</pubDate>
      <link>https://dev.to/onticdani/centralize-and-visualize-docker-logs-in-grafana-with-loki-27c9</link>
      <guid>https://dev.to/onticdani/centralize-and-visualize-docker-logs-in-grafana-with-loki-27c9</guid>
      <description>&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
Introduction

&lt;ul&gt;
&lt;li&gt;My approach&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;How does it work?&lt;/li&gt;

&lt;li&gt;

1. Setting up Loki

&lt;ul&gt;
&lt;li&gt;1.1 Create a directory for our configuration files&lt;/li&gt;
&lt;li&gt;1.2 Create a &lt;code&gt;loki-config.yaml&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;1.3 Create a &lt;code&gt;docker-compose.yaml&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;1.4 Start Loki!&lt;/li&gt;
&lt;li&gt;1.5 Exposing Loki to the internet&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

2. Setting up the Loki Docker plugin

&lt;ul&gt;
&lt;li&gt;2.1 Install the Docker loki plugin&lt;/li&gt;
&lt;li&gt;2.2 Configure the Docker daemon&lt;/li&gt;
&lt;li&gt;2.3 Restart Docker&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

3 Grafana!

&lt;ul&gt;
&lt;li&gt;3.1 Install Grafana&lt;/li&gt;
&lt;li&gt;3.2 Connect Grafana to Loki&lt;/li&gt;
&lt;li&gt;3.3 Query the logs&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Conclusion&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I've previously written about how to centralize and visualize logs in Grafana with Loki for apps that write logs to files &lt;a href="https://daniel.es/blog/how-to-centralize-and-visualize-your-app-logs-in-grafana/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Doing the same with Docker is &lt;strong&gt;SO MUCH SIMPLER&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  My approach
&lt;/h3&gt;

&lt;p&gt;You will see other guides showing you how to set up everything in a single machine. While this might be convenient for people running all their Docker stuff in a single server, it's not the best practice for a production environment.&lt;/p&gt;

&lt;p&gt;I will show you how to set up everything in a way that you can scale horizontally. With Grafana, Loki and Promtail each running in isolation of each other.&lt;/p&gt;

&lt;p&gt;I.e.: You can have promtail in a server running Docker containers for a web app, sending the logs to a Loki instance running in another server and then visualize everything in Grafana running in a third server.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;p&gt;The set up is really similar to the one I wrote about in the previous article, but with a few differences:&lt;/p&gt;

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

&lt;p&gt;The main difference is that, with this setup, we have the Docker daemon sending the logs directly to Loki, without having to configure Promtail at all!&lt;/p&gt;

&lt;p&gt;This has a few advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You don't need to mess around with log files and their locations&lt;/li&gt;
&lt;li&gt;Docker takes care of sending the logs to Loki&lt;/li&gt;
&lt;li&gt;Querying the logs becomes &lt;strong&gt;SO MUCH EASIER&lt;/strong&gt; in Grafana as we can query by container name, image, compose project, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Setting up Loki
&lt;/h2&gt;

&lt;p&gt;If you already have a Loki instance and are only interested in setting up the Docker Loki plugin, you can skip this section.&lt;/p&gt;

&lt;p&gt;As I said in the introduction, I like to set up everything in separate servers (I use Proxmox LXCs for this), so here's how I set up Loki in a Ubuntu + Docker server in my home lab.&lt;/p&gt;

&lt;p&gt;Note that this setup also works just fine if you want to do it all in one machine. Just set up the Loki and Grafana services in the same docker compose file.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.1 Create a directory for our configuration files
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;h3&gt;
  
  
  1.2 Create a &lt;code&gt;loki-config.yaml&lt;/code&gt; file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano loki-config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the configuration code for loki. I explain what each part does below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;auth_enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;http_listen_port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3100&lt;/span&gt;

&lt;span class="na"&gt;common&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;path_prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp/loki&lt;/span&gt;
  &lt;span class="na"&gt;instance_addr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;127.0.0.1&lt;/span&gt;
  &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;filesystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;chunks_directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp/loki/chunks&lt;/span&gt;
      &lt;span class="na"&gt;rules_directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp/loki/rules&lt;/span&gt;
  &lt;span class="na"&gt;replication_factor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;ring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kvstore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inmemory&lt;/span&gt;

&lt;span class="na"&gt;query_range&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;results_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;embedded_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;max_size_mb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;

&lt;span class="na"&gt;schema_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2020-05-15&lt;/span&gt;
      &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tsdb&lt;/span&gt;
      &lt;span class="na"&gt;object_store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;filesystem&lt;/span&gt;
      &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v13&lt;/span&gt;
      &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;index_&lt;/span&gt;
        &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;24h&lt;/span&gt;

&lt;span class="na"&gt;ruler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;alertmanager_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:9093&lt;/span&gt;

&lt;span class="na"&gt;analytics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;reporting_enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="na"&gt;limits_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;retention_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30d&lt;/span&gt;

&lt;span class="na"&gt;compactor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;working_directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp/loki/retention&lt;/span&gt;
  &lt;span class="na"&gt;delete_request_store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;filesystem&lt;/span&gt;
  &lt;span class="na"&gt;retention_enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;retention_delete_delay&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2h&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  What does each part do?
&lt;/h4&gt;

&lt;p&gt;-&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;auth_enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Loki requires authentication or not. If you're exposing Loki through a reverse proxy or something similar to the internet, you should enable this.&lt;/p&gt;

&lt;p&gt;-&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;http_listen_port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The port where Loki will receive logs from Docker (or Promtail) and also make them accessible to Gragana.&lt;/p&gt;

&lt;p&gt;-&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;common&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path_prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp/loki&lt;/span&gt;
    &lt;span class="na"&gt;instance_addr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;127.0.0.1&lt;/span&gt;
    &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;filesystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;chunks_directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp/loki/chunks&lt;/span&gt;
        &lt;span class="na"&gt;rules_directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp/loki/rules&lt;/span&gt;
    &lt;span class="na"&gt;replication_factor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;ring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;kvstore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inmemory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This section tells Loki to store everything in the &lt;code&gt;/tmp/loki&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;That it is running in the local machine (127.0.0.1) and that it should use the &lt;code&gt;filesystem&lt;/code&gt; (which just means the local disk) as the storage engine.&lt;/p&gt;

&lt;p&gt;We also tell it to just save 1 replica of the data and that it should use an in-memory ring to store the data until it's saved to disk.&lt;/p&gt;

&lt;p&gt;-&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;query_range&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;results_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;embedded_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;max_size_mb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This section tells Loki to cache the results of queries in memory. This is optional, but I like to have it enabled since it improves performance when querying the logs from Grafana.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;max_size_mb&lt;/code&gt; is the maximum size of the cache in megabytes. The docs state 100 MB is a good starting point, adjust this as needed.&lt;/p&gt;

&lt;p&gt;-&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;schema_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2020-05-15&lt;/span&gt;
        &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tsdb&lt;/span&gt;
        &lt;span class="na"&gt;object_store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;filesystem&lt;/span&gt;
        &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v13&lt;/span&gt;
        &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;index_&lt;/span&gt;
          &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;24h&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the schema for the logs. It tells Loki to accept logs from a specific date onwards (I usually just set a random date in the past since all my logs will be from the here on out).&lt;/p&gt;

&lt;p&gt;It tells it to use the &lt;code&gt;tsdb&lt;/code&gt; storage engine (time series database). We also tell it to use the &lt;code&gt;filesystem&lt;/code&gt; to store other data.&lt;/p&gt;

&lt;p&gt;In the index section, we tell Loki to use the &lt;code&gt;index_&lt;/code&gt; prefix for the index files and to do so every 24 hours.&lt;/p&gt;

&lt;p&gt;-&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;ruler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;alertmanager_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:9093&lt;/span&gt;

  &lt;span class="na"&gt;analytics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;reporting_enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we're telling Loki to use the local alertmanager instance and to not report any analytics since I'm not going to use them.&lt;/p&gt;

&lt;p&gt;-&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;limits_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;retention_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30d&lt;/span&gt;

  &lt;span class="na"&gt;compactor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;working_directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp/loki/retention&lt;/span&gt;
    &lt;span class="na"&gt;delete_request_store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;filesystem&lt;/span&gt;
    &lt;span class="na"&gt;retention_enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;retention_delete_delay&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2h&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we tell Loki to keep the logs for 30 days and to delete older logs. We set a delay of 2 hours for the deletion.&lt;/p&gt;

&lt;p&gt;Note that &lt;code&gt;limits_config&lt;/code&gt; won't work unless we set up the compactor. The compactor does what the name sugests, it compacts the logs into chunks and is in charge of deleting old data.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.3 Create a &lt;code&gt;docker-compose.yaml&lt;/code&gt; file
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;p&gt;Paste the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;loki&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana/loki:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3100:3100'&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./loki-config.yml:/etc/loki/local-config.yaml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;loki-data:/loki&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;-config.file=/etc/loki/local-config.yaml&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;loki-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we expose the http port for Loki and tell Docker to use the &lt;code&gt;loki-config.yaml&lt;/code&gt; file for the configuration.&lt;/p&gt;

&lt;p&gt;We also create a volume for the data generated by Loki so it can persist even if the container is removed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Even though you could run this single container with &lt;code&gt;docker run&lt;/code&gt;, I like to use &lt;code&gt;docker-compose&lt;/code&gt; so I don't have to remember the command to start the container.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  1.4 Start Loki!
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;p&gt;If everything went well, you should be able to access Loki at &lt;code&gt;http://your-server-ip:3100&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you have servers in multiple locations (aws, digital ocean, home lab, etc), you can create a reverse proxy with NGINX or a similar service to expose Loki to the internet. If so, I recommend either enabling authentication in the &lt;code&gt;loki-config.yaml&lt;/code&gt; file or allowing only your servers to access Loki via the reverse proxy or firewall.&lt;/p&gt;

&lt;p&gt;Now you can point all your Docker servers to point to this Loki instance!&lt;/p&gt;

&lt;h3&gt;
  
  
  1.5 Exposing Loki to the internet
&lt;/h3&gt;

&lt;p&gt;If all your Docker servers are running in the same network, just note down the IP address of the server running Loki.&lt;/p&gt;

&lt;p&gt;If your Docker servers are running in an external server to where you installed Loki (i.e. Loki is in your Home Lab at home and your Docker server is in a cloud provider), you can expose Loki to the internet by running a reverse proxy.&lt;/p&gt;

&lt;p&gt;I won't go into detail on how to do this, there are many guides on using NGINX, Traefic, Cloudflare tunnels, etc. Just use what you're comfortable with.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Setting up the Loki Docker plugin
&lt;/h2&gt;

&lt;p&gt;I'll assume you already have one or multiple Docker containers running in a server somewhere.&lt;/p&gt;

&lt;p&gt;You can repeat this step for every server running Docker.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1 Install the Docker loki plugin
&lt;/h3&gt;

&lt;p&gt;This is the easiest part. Run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker plugin &lt;span class="nb"&gt;install &lt;/span&gt;grafana/loki-docker-driver:3.3.2-amd64 &lt;span class="nt"&gt;--alias&lt;/span&gt; loki &lt;span class="nt"&gt;--grant-all-permissions&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Change &lt;code&gt;-amd64&lt;/code&gt; to &lt;code&gt;-arm64&lt;/code&gt; in the image tag for ARM64 hosts.&lt;/p&gt;

&lt;p&gt;Check &lt;a href="https://grafana.com/docs/loki/latest/send-data/docker-driver/" rel="noopener noreferrer"&gt;here&lt;/a&gt; for the latest version of this command.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the command was successful, you should see the plugin listed when you run &lt;code&gt;docker plugin ls&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2 Configure the Docker daemon
&lt;/h3&gt;

&lt;p&gt;Now we need to tell Docker to use the Loki plugin to send the logs to Loki.&lt;/p&gt;

&lt;p&gt;We need to create a &lt;code&gt;daemon.json&lt;/code&gt; file in &lt;code&gt;/etc/docker/&lt;/code&gt; with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"log-driver"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"loki"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"log-opts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"loki-url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:3100/loki/api/v1/push"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"loki-batch-size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"400"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Docker that it should use the loki log driver instead of the default one and sends the logs to the Loki instance.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Again, change the &lt;code&gt;loki-url&lt;/code&gt; field to the correct URL for you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;loki-batch-size&lt;/code&gt; is optional, but I like to set it to 400, meaning it will send 400 logs at a time to Loki. Not too many, not too few.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwkf1fhr1accp6ml2vh2x.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwkf1fhr1accp6ml2vh2x.gif" alt="Not Great Not Terrible" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2.3 Restart Docker
&lt;/h3&gt;

&lt;p&gt;This is the command for Ubuntu/Debian. If you're on a different OS, just Google how to restart the Docker service for your OS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;IMPORTANT!&lt;/p&gt;

&lt;p&gt;Seems that the Loki plugin won't send logs for existing docker containers.&lt;/p&gt;

&lt;p&gt;You will have to re-create existing containers for them to start sending logs to loki. Not just restart them. You can use the docker flag &lt;code&gt;--force-recreate&lt;/code&gt; for this task.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Great! Now we have all the Docker containers in this machine sending their logs to our Loki instance!&lt;/p&gt;

&lt;h2&gt;
  
  
  3 Grafana!
&lt;/h2&gt;

&lt;p&gt;This is the final and coolest part!&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 Install Grafana
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;If you already have Grafana set up, you can skip ahead to 3.3 where I show how easy is to query the logs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's as simple as running a container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;grafana &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 grafana/grafana
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can access Grafana at &lt;code&gt;http://your-server-ip:3000&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 Connect Grafana to Loki
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;In the left panel, go to "Connections" &amp;gt; "Data Sources".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click on "+ Add new data source".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Search for "Loki":&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz6h1vkbr01z7scxln965.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz6h1vkbr01z7scxln965.png" alt="Loki data source" width="698" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fill in a name and the url of your Loki instance:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fplmefuvsr010kjv11c4k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fplmefuvsr010kjv11c4k.png" alt="Loki data source details" width="800" height="839"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click on "Save &amp;amp; Test". You should see a message saying everything is working correctly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsw7oqw32ws4spylp0b67.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsw7oqw32ws4spylp0b67.png" alt="Loki data source test" width="800" height="123"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 Query the logs
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;In the left panel, go to "Explore".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select your Loki source at the top:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fey878m3g0ricvki5wlm5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fey878m3g0ricvki5wlm5.png" alt="Select Loki source" width="664" height="708"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;🎉 Now comes the super cool part of this setup. You can query the logs by container name, compose project, etc.:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frpc5gaarqwkdymdeyxhb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frpc5gaarqwkdymdeyxhb.png" alt="Query logs" width="209" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the best part of this setup, as you can really easily see the logs of a specific container without having to sort through hundreds of files and weird configurations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I love this setup, I deploy my stuff mainly with Docker and having all the logs centralized in a single place makes my life so much easier to debug and monitor.&lt;/p&gt;

&lt;p&gt;It's also SUPER simple to query specific logs, if I need to see the logs of a full compose project, or a specific service or container, I can do it with a simple query.&lt;/p&gt;

&lt;p&gt;Hope you found this guide useful! If you have any questions or suggestions, feel free to reach out to me on &lt;a href="https://x.com/onticdani" rel="noopener noreferrer"&gt;X&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>docker</category>
      <category>grafana</category>
      <category>loki</category>
    </item>
    <item>
      <title>How to properly redirect www to non-www with Cloudflare</title>
      <dc:creator>Daniel</dc:creator>
      <pubDate>Tue, 15 Oct 2024 14:06:41 +0000</pubDate>
      <link>https://dev.to/onticdani/how-to-properly-redirect-www-to-non-www-with-cloudflare-2865</link>
      <guid>https://dev.to/onticdani/how-to-properly-redirect-www-to-non-www-with-cloudflare-2865</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;Step by step&lt;/li&gt;
&lt;li&gt;Why do we need a DNS record for www pointing to a random IP address?&lt;/li&gt;
&lt;li&gt;Manually adding the redirect rule&lt;/li&gt;
&lt;li&gt;Why do I get a &lt;code&gt;DNS_PROBE_FINISHED_NXDOMAIN&lt;/code&gt; error?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I've read countless tutorials and watched many YouTube videos until I found the correct way to implement a www redirect in Cloudflare.&lt;/p&gt;

&lt;p&gt;When you have a website, you may want to redirect the www version of your domain to the non-www version. This is important for SEO, since search engines prefer a single source, and to avoid duplicate content issues. In this article, we will show you how to properly redirect www to non-www with Cloudflare.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step by step
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Login to Cloudflare&lt;/li&gt;
&lt;li&gt;Go to the domain you want to set up the redirect&lt;/li&gt;
&lt;li&gt;Go to "DNS" &amp;gt; "Records" and remove any &lt;code&gt;www&lt;/code&gt; record that you have&lt;/li&gt;
&lt;li&gt;Now add a new &lt;code&gt;A&lt;/code&gt; record with the name &lt;code&gt;www&lt;/code&gt; and the IPv4 address &lt;code&gt;192.0.2.1&lt;/code&gt; (this is the step that everyone is missing)&lt;/li&gt;
&lt;li&gt;Make sure to leave the "Proxied" option on&lt;/li&gt;
&lt;li&gt;Go to "Rules" &amp;gt; "Redirect Rules"&lt;/li&gt;
&lt;li&gt;Hit "+ Create Rule"&lt;/li&gt;
&lt;li&gt;Under "Create new Single Redirect" there is a "Redirect from WWW to Root" snippet. Click on "Create a Rule" in that snippet&lt;/li&gt;
&lt;li&gt;Change the Rule name if you want and hit "Deploy"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it! You have successfully redirected www to non-www with Cloudflare.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;IMPORTANT!&lt;/strong&gt; You might see that these rules aren't working in your browser. This is because the browser caches the redirects so it might take a while for your browser to update.&lt;/p&gt;

&lt;p&gt;To make sure that it's working:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Try it from a different browser/laptop/phone&lt;/li&gt;
&lt;li&gt;Use this command: &lt;code&gt;curl -I https://www.example.com&lt;/code&gt; and you should see a &lt;code&gt;301 Moved Permanently&lt;/code&gt; and a &lt;code&gt;location:...&lt;/code&gt; with the destination of the redirect in the response&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why do we need a DNS record for www pointing to a random IP address?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;192.0.2.1&lt;/code&gt; is just a dummy IP address, you could literally use any IP address you want.&lt;/p&gt;

&lt;p&gt;This particular address is commonly used in local networks so, as a public IP, it won't really go anywhere.&lt;/p&gt;

&lt;p&gt;The reason we need to add this record is because Cloudflare needs to have a record for the &lt;code&gt;www&lt;/code&gt; subdomain in order to create the redirect rule. Otherwise it will give a &lt;code&gt;DNS_PROBE_FINISHED_NXDOMAIN&lt;/code&gt; error as the 'Internet' will think that there is nothing matching that DNS record (&lt;a href="http://www.example.com" rel="noopener noreferrer"&gt;www.example.com&lt;/a&gt;).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It's like going to the movies but not even having the movie you want to watch.&lt;/p&gt;

&lt;p&gt;The other way around, if you go and there's the movie, the guy at the entrance will point you to the correct room number.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you want to learn more about this you can check &lt;a href="https://developers.cloudflare.com/dns/manage-dns-records/reference/proxied-dns-records/" rel="noopener noreferrer"&gt;https://developers.cloudflare.com/dns/manage-dns-records/reference/proxied-dns-records/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Manually adding the redirect rule
&lt;/h2&gt;

&lt;p&gt;If you prefer to manually add the redirect rule, you can do so by creating a new rule under "Rules" &amp;gt; "Redirect Rules" and filling in the following fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If incoming requests match...&lt;/strong&gt; - Select "Wildcard pattern"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request URL&lt;/strong&gt;: &lt;code&gt;https://www.*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target URL&lt;/strong&gt;: &lt;code&gt;https://${1}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status Code&lt;/strong&gt;: &lt;code&gt;301&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What this does is get the asterisks part of www request and use it in the target URL. This way, if someone goes to &lt;code&gt;www.example.com&lt;/code&gt;, they will be redirected to &lt;code&gt;example.com&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do I get a &lt;code&gt;DNS_PROBE_FINISHED_NXDOMAIN&lt;/code&gt; error?
&lt;/h2&gt;

&lt;p&gt;If you get this error, it means that there is no DNS record for the &lt;code&gt;www&lt;/code&gt; subdomain.&lt;/p&gt;

&lt;p&gt;Make sure you have added the &lt;code&gt;A&lt;/code&gt; record with the name &lt;code&gt;www&lt;/code&gt; and a dummy IP address like &lt;code&gt;192.0.1.2&lt;/code&gt; (See step 4 of the guide above).&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>cloudflare</category>
      <category>seo</category>
      <category>domains</category>
    </item>
    <item>
      <title>How to deploy Django in a subdirectory with Docker, NGINX and Whitenoise</title>
      <dc:creator>Daniel</dc:creator>
      <pubDate>Sat, 21 Sep 2024 09:41:05 +0000</pubDate>
      <link>https://dev.to/onticdani/how-to-deploy-django-in-a-subdirectory-with-docker-nginx-and-whitenoise-4hc0</link>
      <guid>https://dev.to/onticdani/how-to-deploy-django-in-a-subdirectory-with-docker-nginx-and-whitenoise-4hc0</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;My setup&lt;/li&gt;
&lt;li&gt;The problem&lt;/li&gt;
&lt;li&gt;
The solution

&lt;ul&gt;
&lt;li&gt;1. Configure django&lt;/li&gt;
&lt;li&gt;2. Configure Static Files&lt;/li&gt;
&lt;li&gt;3. Admin login&lt;/li&gt;
&lt;li&gt;4. Configure Nginx&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Conclusion&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;If you are here, you've probably faced the same challenged as I did and spent countless hours on google trying to find how to make a django application work in a subdirectory.&lt;/p&gt;

&lt;p&gt;Look no further, here's all you need to know to make it work.&lt;/p&gt;

&lt;h2&gt;
  
  
  My setup
&lt;/h2&gt;

&lt;p&gt;I want my frontend to be served in the root &lt;code&gt;/&lt;/code&gt; of my domain and the backend to be served in the &lt;code&gt;/api&lt;/code&gt; subdirectory of the same domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/ -&amp;gt; Frontend
https://example.com/api/ -&amp;gt; Backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm going to quickly run you through my setup so you can understand the context of this article.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I have a monorepo with two folders: &lt;code&gt;backend&lt;/code&gt; and &lt;code&gt;frontend&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;backend&lt;/code&gt;: django REST API that serves data and an admin page&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;frontend&lt;/code&gt;: React frontend that consumes the API&lt;/li&gt;
&lt;li&gt;Docker to containerize the application&lt;/li&gt;
&lt;li&gt;Nginx to serve the Frontend application &lt;strong&gt;and&lt;/strong&gt; serve the django API in a subdirectory
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
├── backend/
│   ├── Dockerfile
│   └── other django stuff...
├── frontend/
│   ├── Dockerfile
│   ├── default-nginx.conf
│   └── other frontend stuff...
└── docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is my sample docker compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./backend&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000:8000"&lt;/span&gt;

  &lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./frontend&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;When I tried to deploy the application to a subdirectory, I faced a few challenges:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The API was not working&lt;/li&gt;
&lt;li&gt;The static files were not being served&lt;/li&gt;
&lt;li&gt;The admin page was not working&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Configure django
&lt;/h3&gt;

&lt;p&gt;First, we need to configure Django to let it know that we are running in a subdirectory.&lt;/p&gt;

&lt;p&gt;Add the following to your &lt;code&gt;settings.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;FORCE_SCRIPT_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/api&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Important!&lt;/p&gt;

&lt;p&gt;Having the leading slash is important to make the static files settings below work correctly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;FORCE_SCRIPT_NAME&lt;/code&gt; is a setting that tells Django to prepend the given value to all URLs generated by Django.&lt;/p&gt;

&lt;p&gt;You can find more documentation &lt;a href="https://docs.djangoproject.com/en/5.1/ref/settings/#force-script-name" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Configure Static Files
&lt;/h3&gt;

&lt;p&gt;We need to configure Django to serve the static files correctly in the subdirectory, otherwise, they won't be found.&lt;/p&gt;

&lt;p&gt;You're probably familiar with the following settings in &lt;code&gt;settings.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;STATIC_ROOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BASE_DIR&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;staticfiles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;STATIC_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/static&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need to modify it to include the subdirectory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;STATIC_ROOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BASE_DIR&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;staticfiles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;STATIC_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{}/static/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FORCE_SCRIPT_NAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are using &lt;strong&gt;Whitenoise&lt;/strong&gt; you will also need to include the following setting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;WHITENOISE_STATIC_PREFIX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/static/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;There is a PR that has been merged that allows you to not have to set this setting, but it's not released as of the time of writing this article.&lt;/p&gt;

&lt;p&gt;You can find the PR &lt;a href="https://github.com/Archmonger/ServeStatic/pull/21" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. Admin login
&lt;/h3&gt;

&lt;p&gt;The settings above should make the admin page work, but some people have reported encountering the admin page redirecting them to the root of the domain after login.&lt;/p&gt;

&lt;p&gt;There's a setting that allows you to override the default admin login redirect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;LOGIN_REDIRECT_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/api/admin/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;You can find more information about this setting &lt;a href="https://docs.djangoproject.com/en/5.1/ref/settings/#login-redirect-url" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4. Configure Nginx
&lt;/h3&gt;

&lt;p&gt;Here comes to most tricky part in my search, it took me a while to figure out how to configure Nginx to serve the backend in a subdirectory.&lt;/p&gt;

&lt;p&gt;I will explain the configuration in detail, but here's the final configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/usr/share/nginx/html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;index&lt;/span&gt; &lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# React frontend&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Django API&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/api/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://backend:8000/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;I will assume you have basic knowledge of how nginx works so I will ignore the basic configuration and go straight to the relevant parts.&lt;/p&gt;

&lt;p&gt;For context, I have built my frontend to &lt;code&gt;/usr/share/nginx/html&lt;/code&gt; previously. Nginx is serving those files in the root of the domain.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let's break it down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;location /api/&lt;/code&gt;: This is the location block that will serve the Django API in the subdirectory &lt;code&gt;/api/&lt;/code&gt;.
&lt;strong&gt;IMPORTANT&lt;/strong&gt;! Note that it has both a trailing and leading slash. This is important to make django work correctly in a subdirectory.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;proxy_pass http://backend:8000/&lt;/code&gt;: This is the most important part. It tells Nginx to proxy all requests to &lt;code&gt;/api/&lt;/code&gt; to the backend service running with Docker on port 8000. Cool thing about docker is that we can reference the service by its name, in this case &lt;code&gt;backend&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now to the headers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Host $host&lt;/code&gt;: This sets the Host header to the value of the host header of the request. It basically passes the host header from the client to the backend.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X-Real-IP $remote_addr&lt;/code&gt;: This sets the X-Real-IP header to the value of the remote address of the client. This is useful for the backend to know the real IP of who is making the request.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X-Forwarded-For $proxy_add_x_forwarded_for&lt;/code&gt;: This header maintains a chain of IP addresses a request has traversed. It's similar to X-Real-IP but provides more comprehensive information.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X-Forwarded-Proto $scheme&lt;/code&gt;: This header informs the backend about the protocol (HTTP or HTTPS) used by the client to connect to Nginx. Without this, your Django application wouldn't know if the original request was secure (HTTPS) or not.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;That's it! You should now have your Django application running in a subdirectory with Docker and Whitenoise.&lt;/p&gt;

&lt;p&gt;I really hope this article saves you some time and headaches!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>django</category>
      <category>docker</category>
      <category>nginx</category>
    </item>
    <item>
      <title>How to create an API endpoint in astro</title>
      <dc:creator>Daniel</dc:creator>
      <pubDate>Fri, 14 Jun 2024 14:32:12 +0000</pubDate>
      <link>https://dev.to/onticdani/how-to-create-an-api-endpoint-in-astro-4b0h</link>
      <guid>https://dev.to/onticdani/how-to-create-an-api-endpoint-in-astro-4b0h</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;
What's the setup?

&lt;ul&gt;
&lt;li&gt;1. Ready your Astro project&lt;/li&gt;
&lt;li&gt;2. SSR&lt;/li&gt;
&lt;li&gt;2.1 Adapter&lt;/li&gt;
&lt;li&gt;2.2 &lt;code&gt;server&lt;/code&gt; or &lt;code&gt;hybrid&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;2.3 A note on environment variables&lt;/li&gt;
&lt;li&gt;2.4 A note on &lt;code&gt;console.log&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;3. File structure&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

The API endpoint

&lt;ul&gt;
&lt;li&gt;1. Create the file&lt;/li&gt;
&lt;li&gt;2. The code&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;The form&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Gone are the days when you had to create a separate API server to serve your frontend application. With Astro, you can create API endpoints directly in your app. Which means you can even create a full-stack application with just one codebase.&lt;/p&gt;

&lt;p&gt;Personally, I use these endpoints for simple actions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contact Forms&lt;/li&gt;
&lt;li&gt;Newsletter subscriptions&lt;/li&gt;
&lt;li&gt;User registration&lt;/li&gt;
&lt;li&gt;Even user authentication sometimes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's also useful for when you need to fetch and process data from a source that requires authentication (I.e. an API key) and you don't want to expose that key in the frontend.&lt;/p&gt;

&lt;p&gt;In this article we'll go through the steps to create an API endpoint in Astro.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the setup?
&lt;/h2&gt;

&lt;p&gt;In this article we'll create a &lt;strong&gt;simple API endpoint that creates a contact in Brevo&lt;/strong&gt;, which is mostly what I use these endpoints for. You can replace this with any other service you want to interact with.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Ready your Astro project
&lt;/h3&gt;

&lt;p&gt;If you're new to Astro or haven't set up an Astro project yet, I recommend you to check out the &lt;a href="https://docs.astro.build/en/getting-started/" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can also start with one of their &lt;a href="https://astro.build/themes/" rel="noopener noreferrer"&gt;themes&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. SSR
&lt;/h3&gt;

&lt;p&gt;In order to have a working API endpoint that works at runtime, you need to enable &lt;a href="https://docs.astro.build/en/guides/server-side-rendering/" rel="noopener noreferrer"&gt;SSR (Server Sider Rendering)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;SSR allows you to have the app run on the server before it gets to the client.&lt;/p&gt;

&lt;h4&gt;
  
  
  2.1 Adapter
&lt;/h4&gt;

&lt;p&gt;For this, you will need an adapter.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What an adapter does is it allows you to run your SSR Astro App in different environments. For example, you can run it in a serverless environment like Vercel or Netlify, or in a Node.js server.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can see a list of official and community adapters &lt;a href="https://docs.astro.build/en/guides/server-side-rendering/#official-adapters" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For this article, I'll use the &lt;a href="https://docs.astro.build/en/guides/integrations-guide/node/" rel="noopener noreferrer"&gt;&lt;code&gt;@astrojs/adapter-node&lt;/code&gt; adapter&lt;/a&gt; since I host my side in a Node docker container.&lt;/p&gt;

&lt;p&gt;Super easy to install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx astro add node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2.2 &lt;code&gt;server&lt;/code&gt; or &lt;code&gt;hybrid&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Astro &lt;a href="https://docs.astro.build/en/guides/server-side-rendering/#enable-on-demand-server-rendering" rel="noopener noreferrer"&gt;allows you to run SSR in 2 ways&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;server&lt;/code&gt;: On-demand rendered by default. Basically uses the server for everything. Use this when most of your site should be dynamic. You can opt-out of SSR for individual pages or endpoints.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;hybrid&lt;/code&gt;: Pre-rendered to HTML by default. It does not pre-render the page on the server. Use this when most of your site should be static. You can opt-in to SSR for individual pages or endpoints.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For my usecase where most of my site is static (it's a landing page after all) I use &lt;code&gt;hybrid&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// astro.config.mjs&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;astro/config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@astrojs/node&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hybrid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;node&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;standalone&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2.3 A note on environment variables
&lt;/h4&gt;

&lt;p&gt;If you're used to Astro, you know that you can use environment variables by calling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MY_VARIABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VARIABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, because Astro is static by default, what this really does is get the environment variable at build time. Then, the variable is hardcoded into the build code, which means that if you change the environment variable after the build, it won't be reflected in the code.&lt;/p&gt;

&lt;p&gt;If you use SSR it works differently, &lt;code&gt;import.meta.env&lt;/code&gt; won't work since it's available at build time but not at runtime on the server.&lt;/p&gt;

&lt;p&gt;You will need to use &lt;code&gt;process.env&lt;/code&gt; instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MY_VARIABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VARIABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;BUT WAIT!&lt;/p&gt;

&lt;p&gt;There's another catch, and that's that &lt;code&gt;process.env&lt;/code&gt; is not available with &lt;code&gt;npm run dev&lt;/code&gt;, which means your code will crash when you try to run it locally.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MY_VARIABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VARIABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VARIABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code will try to get the environment variable from &lt;code&gt;import.meta.env&lt;/code&gt; first, and if it's not available it will try to get it from &lt;code&gt;process.env&lt;/code&gt;. This way, your code will work both in development and production.&lt;/p&gt;

&lt;h4&gt;
  
  
  2.4 A note on &lt;code&gt;console.log&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;If you're used to using &lt;code&gt;console.log&lt;/code&gt; to debug your code, you'll know that it will show up in the browser console when you're running the app in development mode.&lt;/p&gt;

&lt;p&gt;When using &lt;code&gt;console.log&lt;/code&gt; in an SSR component, because it runs on the server, the logs will show up in the terminal where you're running the app.&lt;/p&gt;

&lt;p&gt;So if you're looking for your logs and can't find them, check the terminal where you're running the app.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. File structure
&lt;/h3&gt;

&lt;p&gt;The full functionality needs 2 files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;.js&lt;/code&gt; or &lt;code&gt;.ts&lt;/code&gt; API endpoint file that lives in the &lt;code&gt;src/pages/api&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt;A form that gets the user input and sends it to the API endpoint. I personally like to do this in a &lt;code&gt;.tsx&lt;/code&gt; file because I can then use the full power of react (&lt;code&gt;react-hook-form&lt;/code&gt; and &lt;code&gt;zod&lt;/code&gt;) to handle the form.
Place this form wherever you like, I like having all my forms in &lt;code&gt;src/components/forms&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's pretty much it! The form will send the data to our API endpoint, which will then process it and send it to Brevo.&lt;/p&gt;




&lt;h2&gt;
  
  
  The API endpoint
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Create the file
&lt;/h3&gt;

&lt;p&gt;Let's create the API endpoint that will send the data to Brevo.&lt;/p&gt;

&lt;p&gt;You can create this endpoint wherever you want under the &lt;code&gt;src/pages/&lt;/code&gt; directory depending on where you want it to be accessible.&lt;/p&gt;

&lt;p&gt;For instance, I like my endpoints to be accessible under &lt;code&gt;/api/&lt;/code&gt; so I create a &lt;code&gt;src/pages/api/&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;So my endpoint file will be &lt;code&gt;src/pages/api/create-brevo-contact.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This means that I will be able to access it at &lt;code&gt;http://mydomain.com/api/create-brevo-contact&lt;/code&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. The code
&lt;/h3&gt;

&lt;p&gt;Your API endpoint code should have a pretty simple structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SSR API endpoint template&lt;/span&gt;

&lt;span class="c1"&gt;// Tell Astro that this component should run on the server&lt;/span&gt;
&lt;span class="c1"&gt;// You only need to specify this if you're using the hybrid output&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prerender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Import the APIRoute type from Astro&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;APIRoute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;astro&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// This function will be called when the endpoint is hit with a GET request&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Do some stuff here&lt;/span&gt;

  &lt;span class="c1"&gt;// Return a 200 status and a response to the frontend&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Operation successful&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Following the template above, this is a simple POST API endpoint to create a contact in Brevo. Everything is commented so you can understand what's going on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/pages/api/create-brevo-contact.ts&lt;/span&gt;

&lt;span class="c1"&gt;// Because I chose hybrid, I need to specify that this endpoint should run on the server:&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prerender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Import the APIRoute type from Astro&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;APIRoute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;astro&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// This is the function that will be called when the endpoint is hit&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Check if the request is a JSON request&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Get the body of the request&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Get the email from the body&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Declares the Brevo API URL&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BREVO_API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.brevo.com/v3/contacts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Gets the Brevo API Key from an environment variable&lt;/span&gt;
    &lt;span class="c1"&gt;// Check the note on environment variables in the SSR section of this article to understand what is going on here&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BREVO_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BREVO_API_KEY&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BREVO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Just a simple check to make sure the API key is defined in an environment variable&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;BREVO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No BREVO_API_KEY defined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// The payload that will be sent to Brevo&lt;/span&gt;
    &lt;span class="c1"&gt;// This payload will create or update the contact and add it to the list with ID 3&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;updateEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;listIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// Whatever process you want to do in your API endpoint should be inside a try/catch block&lt;/span&gt;
    &lt;span class="c1"&gt;// In this case we're sending a POST request to Brevo&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Make a POST request to Brevo&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BREVO_API_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BREVO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="c1"&gt;// Check if the request was successful&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Request succeeded&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Contact added successfully&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Return a 200 status and the response to our frontend&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Contact added successfully&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;}),&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Request failed&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to add contact to Brevo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Return a 400 status to our frontend&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// An error occurred while doing our API operation&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;An unexpected error occurred while adding contact:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;error&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Return a 400 status to our frontend&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// If the POST request is not a JSON request, return a 400 status to our frontend&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it, you now have a working API endpoint that will create a contact in Brevo when hit with a POST request!&lt;/p&gt;




&lt;h2&gt;
  
  
  The form
&lt;/h2&gt;

&lt;p&gt;As a bonus, I also want to show you how I code my forms to make them responsive.&lt;/p&gt;

&lt;p&gt;For this example, I'll create a simple form, with only an email field and a submit button, that will send the email a user inputs to the API endpoint we created.&lt;/p&gt;

&lt;p&gt;Here's the code:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note that this code is using shadcn ui components for the HTML, you might need to replace them with your own components.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WaitlistForm.tsx&lt;/span&gt;

&lt;span class="c1"&gt;// Zod validation stuff&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WaitlistFormSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please enter a valid email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please enter a valid email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WaitlistFormValues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;WaitlistFormSchema&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WaitlistForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Hooks to check the status of the form&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isSubmitting&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsSubmitting&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isSuccess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// React Hook Form stuff&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useForm&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WaitlistFormValues&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;zodResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;WaitlistFormSchema&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;defaultValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Function that sends the data to the API endpoint when the form is submitted&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WaitlistFormValues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setIsSubmitting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Ping out API endpoint&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/create-brevo-contact&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// If successful, reset the form and show a success message&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;setIsSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// If failed, show error message&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to add contact&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setIsSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;There's been an error. Please try again.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;setIsSubmitting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isSuccess&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Alert&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mb-3 md:mb-8 bg-green-100 border-green-300"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AlertTitle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Thanks!&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AlertTitle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AlertDescription&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            We've added you to the waitlist!
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;br&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AlertDescription&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Alert&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isSuccess&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Alert&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mb-8 bg-red-100 border-red-300"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AlertTitle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Error&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AlertTitle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AlertDescription&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AlertDescription&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Alert&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Form&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;
          &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"space-y-4 md:space-y-8"&lt;/span&gt;
        &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FormField&lt;/span&gt;
            &lt;span class="na"&gt;control&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;control&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
            &lt;span class="na"&gt;render&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FormItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FormLabel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;FormLabel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FormControl&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Input&lt;/span&gt;
                    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"bg-transparent"&lt;/span&gt;
                    &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email@gmail.com"&lt;/span&gt;
                    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                  &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;FormControl&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FormMessage&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;FormItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isSubmitting&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Loader2&lt;/span&gt;
              &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`w-6 h-6 mr-2 animate-spin &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;
                &lt;span class="nx"&gt;isSubmitting&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
              &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
            Submit
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;WaitlistForm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>astro</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Fixing Most TypeScript Intellisense issues in VS Code</title>
      <dc:creator>Daniel</dc:creator>
      <pubDate>Mon, 13 May 2024 14:34:40 +0000</pubDate>
      <link>https://dev.to/onticdani/fixing-most-typescript-intellisense-issues-in-vs-code-nc6</link>
      <guid>https://dev.to/onticdani/fixing-most-typescript-intellisense-issues-in-vs-code-nc6</guid>
      <description>&lt;p&gt;I'm having this issue constantly, specially with devcontainers or other virtual environments, where I suddenly have hundreds, if not thousands of intellisense errors all over my TS code while the app works just fine.&lt;/p&gt;

&lt;p&gt;This is mostly due to VSCode not selecting the correct TS Version automatically. Even if you have &lt;code&gt;"typescript.tsdk": "node_modules/typescript/lib"&lt;/code&gt; in your &lt;code&gt;.vscode/settings.json&lt;/code&gt; it mostly fails. &lt;/p&gt;

&lt;p&gt;To solve this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open any typescript (&lt;code&gt;*.ts&lt;/code&gt;) file.&lt;/li&gt;
&lt;li&gt;Hit &lt;code&gt;F1&lt;/code&gt; or &lt;code&gt;Ctrl + Shift + P&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Search for "TypeScript: Select TypeScript Version..." and hit enter&lt;/li&gt;
&lt;li&gt;Select "Use Workspace Version" (even if the version number is the same as VSCode's version)&lt;/li&gt;
&lt;li&gt;Reload VSCode (close it and reopen it) for it to take full effect and for it to reprocess all open tabs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is usually how I fix the issue.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>vscode</category>
    </item>
    <item>
      <title>My Docker stack to deploy a Django + Celery web app</title>
      <dc:creator>Daniel</dc:creator>
      <pubDate>Thu, 18 Apr 2024 09:48:50 +0000</pubDate>
      <link>https://dev.to/onticdani/my-docker-stack-to-deploy-a-django-celery-web-app-4b13</link>
      <guid>https://dev.to/onticdani/my-docker-stack-to-deploy-a-django-celery-web-app-4b13</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;Project Structure&lt;/li&gt;
&lt;li&gt;Stack Diagram&lt;/li&gt;
&lt;li&gt;
Docker images

&lt;ul&gt;
&lt;li&gt;Django image&lt;/li&gt;
&lt;li&gt;Nginx image&lt;/li&gt;
&lt;li&gt;Redis image&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Docker Compose

&lt;ul&gt;
&lt;li&gt;Environment Variables&lt;/li&gt;
&lt;li&gt;The file&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Deploying

&lt;ul&gt;
&lt;li&gt;Building&lt;/li&gt;
&lt;li&gt;Deploying with Docker Compose&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;br&gt;
After many iterations, this is my current process to deploy a django application.&lt;/p&gt;

&lt;p&gt;The cool thing is that I'm now deploying with only an &lt;code&gt;.env&lt;/code&gt; file and nothing else.&lt;/p&gt;

&lt;p&gt;Note that this is just a single instance of the stack, without Kubernetes or any kind of load balancer.&lt;/p&gt;
&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;My stack consists on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Postgres database&lt;/li&gt;
&lt;li&gt;A Redis database&lt;/li&gt;
&lt;li&gt;A django instance&lt;/li&gt;
&lt;li&gt;Celery beat and workers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I won't go into details of how to set up anything like this here, if you want to learn more about celery &lt;a href="https://docs.celeryq.dev/en/stable/index.html" rel="noopener noreferrer"&gt;in their docs&lt;/a&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;

&lt;p&gt;To understand the &lt;code&gt;docker-compose.yml&lt;/code&gt; file below, it's important to see how I structure my django project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MY-DJANGO-PROJECT/
├── core  # settings.py lives here
├── app1/
│   ├── migrations
│   ├── models.py
│   └── ...
├── app2/
│   ├── migrations
│   ├── models.py
│   └── ...
├── data  # A data directory where I store stuff like logs
├── nginx/
│   ├── certs/
│   │   ├── fullchain.pem
│   │   └── privkey.pem
│   ├── conf/
│   │   ├── default.conf
│   │   ├── prod.conf
│   │   └── staging.conf
│   └── Dockerfile
├── Dockerfile
├── entrypoint-django.sh
├── entrypoint-beat.sh
├── entrypoint-worker.sh
├── Pipfile
└── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Stack Diagram
&lt;/h2&gt;

&lt;p&gt;To visualize the flow better here's a diagram that describes how everything is interconnected:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ylnht5ckfpa13wgd6lj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ylnht5ckfpa13wgd6lj.png" alt="Docker Stack Diagram" width="800" height="709"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Docker images
&lt;/h2&gt;

&lt;p&gt;The stack, though it might seem complicated, is only composed of 3 images, 2 of which are custom:&lt;/p&gt;

&lt;h3&gt;
  
  
  Django image
&lt;/h3&gt;

&lt;p&gt;This is a custom image built from python.&lt;/p&gt;

&lt;p&gt;This image will be used for django, celery workers and celery beat containers.&lt;/p&gt;

&lt;p&gt;Here's the &lt;code&gt;Dockerfile&lt;/code&gt; for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Python as the base image&lt;/span&gt;
&lt;span class="c"&gt;# I use bullseye because I'm more comfortable with it&lt;/span&gt;
&lt;span class="c"&gt;# but you can use Alpine for a more lightweight container&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11-bullseye&lt;/span&gt;

&lt;span class="c"&gt;# Exposes port 8000&lt;/span&gt;
&lt;span class="c"&gt;# Make sure to change this to your used port&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8000&lt;/span&gt;

&lt;span class="c"&gt;# Keeps Python from generating .pyc files in the container&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONDONTWRITEBYTECODE=1&lt;/span&gt;

&lt;span class="c"&gt;# Turns off buffering for easier container logging&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONUNBUFFERED=1&lt;/span&gt;

&lt;span class="c"&gt;# Working directory&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app/backend&lt;/span&gt;

&lt;span class="c"&gt;# Install pipenv&lt;/span&gt;
&lt;span class="c"&gt;# This is not necessary if you use pip in your code&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; pipenv

&lt;span class="c"&gt;# Install pipenv requirements&lt;/span&gt;
&lt;span class="c"&gt;# Turns the Pipfile to a requirements.txt&lt;/span&gt;
&lt;span class="c"&gt;# so it can be installed globally with pip&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; Pipfile Pipfile.lock /app/backend/&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pipenv requirements &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; requirements.txt
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; ./Pipfile ./Pipfile.lock

&lt;span class="c"&gt;# Copy all the code over&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# Create the media directory&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /app/backend/media

&lt;span class="c"&gt;# Create a volume for the media directory&lt;/span&gt;
&lt;span class="k"&gt;VOLUME&lt;/span&gt;&lt;span class="s"&gt; /app/backend/media&lt;/span&gt;

&lt;span class="c"&gt;# Create a volume for the static directory&lt;/span&gt;
&lt;span class="k"&gt;VOLUME&lt;/span&gt;&lt;span class="s"&gt; /app/backend/django_static&lt;/span&gt;

&lt;span class="c"&gt;# Make the entrypoint scripts executable&lt;/span&gt;
&lt;span class="c"&gt;# There's one entrypoint for each service that uses this image&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /app/backend/entrypoint-django.sh
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /app/backend/entrypoint-worker.sh
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /app/backend/entrypoint-beat.sh

&lt;span class="c"&gt;# Set the default entrypoint in case this Dockerfile is run&lt;/span&gt;
&lt;span class="c"&gt;# by itself&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/app/backend/entrypoint-django.sh"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are the entry point files for each service:&lt;/p&gt;

&lt;h4&gt;
  
  
  django entry point
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# Migrate any new migrations to the database on deployment&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Migrating..."&lt;/span&gt;

python manage.py migrate &lt;span class="nt"&gt;--no-input&lt;/span&gt;

&lt;span class="c"&gt;# Collect static files&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Collecting static files..."&lt;/span&gt;

python manage.py collectstatic &lt;span class="nt"&gt;--no-input&lt;/span&gt;

&lt;span class="c"&gt;# Ensure the data directory exists&lt;/span&gt;
&lt;span class="c"&gt;# I use the data directory to store files such as logs&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; data

&lt;span class="c"&gt;# Start gunicorn&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Starting server..."&lt;/span&gt;

gunicorn core.wsgi:application &lt;span class="nt"&gt;--forwarded-allow-ips&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt; &lt;span class="nt"&gt;--bind&lt;/span&gt; 0.0.0.0:8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Worker entry point
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;

&lt;span class="c"&gt;# Wait until the backend directory is created&lt;/span&gt;
&lt;span class="k"&gt;until &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /app/backend
&lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Waiting for server volume..."&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# run a worker&lt;/span&gt;
&lt;span class="c"&gt;# I like having only one task per worker but you can change it&lt;/span&gt;
&lt;span class="c"&gt;# by increasing the concurrency&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Starting celery worker..."&lt;/span&gt;
celery &lt;span class="nt"&gt;-A&lt;/span&gt; core worker &lt;span class="nt"&gt;-l&lt;/span&gt; info &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 1 &lt;span class="nt"&gt;-E&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Beat entry point
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;

&lt;span class="c"&gt;# Wait until the server volume is available&lt;/span&gt;
&lt;span class="k"&gt;until &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /app/backend
&lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Waiting for server volume..."&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# run celery beat&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Starting celery beat..."&lt;/span&gt;
celery &lt;span class="nt"&gt;-A&lt;/span&gt; core beat &lt;span class="nt"&gt;-l&lt;/span&gt; info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Nginx image
&lt;/h3&gt;

&lt;p&gt;This container serves the application.&lt;/p&gt;

&lt;p&gt;I create a custom nginx image that includes my certificates and configuration, so I don't have to copy them over to the server.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: I don't use certbot, as I find it more straightfoward to generate the certificates from cloudflare and just store them in the custom image&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means that the image should be secure in a private registry with authentication, otherwise you risk security of your web app.&lt;/p&gt;

&lt;p&gt;Here's the &lt;code&gt;Dockerfile&lt;/code&gt; for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; nginx:stable-bullseye&lt;/span&gt;

&lt;span class="c"&gt;# Export ports 80 and 443&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 80&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 443&lt;/span&gt;

&lt;span class="c"&gt;# Copy the nginx configuration files to the image&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./conf/default.conf /etc/nginx/conf.d/default.conf&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./conf/prod.conf /etc/nginx/conf.d/prod.conf&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./conf/staging.conf /etc/nginx/conf.d/staging.conf&lt;/span&gt;

&lt;span class="c"&gt;# Copy the CloudFlare Origin CA certificate to the image&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./certs/fullchain.pem /etc/nginx/certs/fullchain.pem&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./certs/privkey.pem /etc/nginx/certs/privkey.pem&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Redis image
&lt;/h3&gt;

&lt;p&gt;I just use the default Redis image for this.&lt;/p&gt;

&lt;p&gt;Just want to note that, because this is a single instance deployment, I like deploying Redis directly here as I find it's enough.&lt;/p&gt;

&lt;p&gt;It is recommended, though, to spin up a Redis database somewhere more centralized.&lt;/p&gt;




&lt;h2&gt;
  
  
  Docker Compose
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Environment Variables
&lt;/h3&gt;

&lt;p&gt;Before I get into the gist of the Docker Compose file here are some environment variables I put in my &lt;code&gt;.env&lt;/code&gt; file for deployment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DOCKER_REGISTRY&lt;/code&gt;: My private, authentication enabled, docker registry where I upload the build images&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DJANGO_DOCKER_IMAGE&lt;/code&gt;: The name I give the django image&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NGINX_DOCKER_IMAGE&lt;/code&gt;: The name I give the NGINX image&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DOCKER_TAG&lt;/code&gt;: Usually the version I want to deploy, i.e.: &lt;code&gt;1.5&lt;/code&gt; or &lt;code&gt;latest&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3"&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7.2.0-alpine&lt;/span&gt;
    &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;6379&lt;/span&gt;

  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DOCKER_REGISTRY}/${DJANGO_DOCKER_IMAGE}:${DOCKER_TAG}&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.env&lt;/span&gt;
    &lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app/backend/entrypoint-django.sh&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;8000:8000&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data:/app/backend/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8000/healthcheck/"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20s&lt;/span&gt; 

  &lt;span class="na"&gt;worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DOCKER_REGISTRY}/${DJANGO_DOCKER_IMAGE}:${DOCKER_TAG}&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.env&lt;/span&gt;
    &lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app/backend/entrypoint-worker.sh&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data:/app/backend/data&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="s"&gt;backend&lt;/span&gt;
        &lt;span class="s"&gt;condition&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="s"&gt;redis&lt;/span&gt;
        &lt;span class="s"&gt;condition&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_started&lt;/span&gt;

  &lt;span class="na"&gt;beat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DOCKER_REGISTRY}/${DJANGO_DOCKER_IMAGE}:${DOCKER_TAG}&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.env&lt;/span&gt;
    &lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app/backend/entrypoint-beat.sh&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data:/app/backend/data&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="s"&gt;backend&lt;/span&gt;
        &lt;span class="s"&gt;condition&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="s"&gt;redis&lt;/span&gt;
        &lt;span class="s"&gt;condition&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_started&lt;/span&gt;

  &lt;span class="na"&gt;nginx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DOCKER_REGISTRY}/${NGINX_DOCKER_IMAGE}:${DOCKER_TAG}&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;80:80&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;443:443&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="s"&gt;backend&lt;/span&gt;
        &lt;span class="s"&gt;condition&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the compose file has the 5 services, Redis, django, celery worker, celery beat and NGINX.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deploying
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Building
&lt;/h3&gt;

&lt;p&gt;First I build the images and push them to the registry. Before, I did this manually, now I use a GitHub action. You &lt;a href="https://dev.to/onticdani/automatically-build-docker-images-with-github-actions-3n8e"&gt;can learn more about this automation here.&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying with Docker Compose
&lt;/h3&gt;

&lt;p&gt;Then I head to the server where I want to deploy this. Make sure that the &lt;code&gt;.env&lt;/code&gt; file is updated and then just:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;docker compose down&lt;/code&gt;: Spin the old instance down&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker system prune -a -f&lt;/code&gt;: This makes sure I remove the &lt;code&gt;latest&lt;/code&gt; image to force the download of the new one from the registry.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker compose up --scale worker=5 -d&lt;/code&gt;: Spin the new instance up
That's it!&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>docker</category>
      <category>django</category>
      <category>python</category>
      <category>deploy</category>
    </item>
    <item>
      <title>Automatically build Docker images with GitHub Actions</title>
      <dc:creator>Daniel</dc:creator>
      <pubDate>Mon, 08 Apr 2024 12:53:01 +0000</pubDate>
      <link>https://dev.to/onticdani/automatically-build-docker-images-with-github-actions-3n8e</link>
      <guid>https://dev.to/onticdani/automatically-build-docker-images-with-github-actions-3n8e</guid>
      <description>&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;Setting up a Self Hosted Runner&lt;/li&gt;
&lt;li&gt;
Structure of a GitHub Action

&lt;ul&gt;
&lt;li&gt;A (simple) GitHub action has 4 main parts:&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

GitHub Action to build Docker Images

&lt;ul&gt;
&lt;li&gt;Process overview &amp;amp; requirements&lt;/li&gt;
&lt;li&gt;1. Environment variables and secrets&lt;/li&gt;
&lt;li&gt;2. Triggers&lt;/li&gt;
&lt;li&gt;4. Job Setup&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Full GitHub Actions template to release a docker image&lt;/li&gt;

&lt;li&gt;Conclusion&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;When I released a new version of a web app, this was the process I followed, which might sound familiar to you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I merged everything into the master branch&lt;/li&gt;
&lt;li&gt;SSH into the deployment server&lt;/li&gt;
&lt;li&gt;git fetch&lt;/li&gt;
&lt;li&gt;git checkout&lt;/li&gt;
&lt;li&gt;git pull&lt;/li&gt;
&lt;li&gt;Build docker images (this took loooong and once the web app was big enough, it would fail)&lt;/li&gt;
&lt;li&gt;Then I would have to build the image in my computer or another specific server to avoid the production server crashing&lt;/li&gt;
&lt;li&gt;Push that to a registry&lt;/li&gt;
&lt;li&gt;Go back to the server and pull from the registry&lt;/li&gt;
&lt;li&gt;Realize you did not update an environment variable&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AGHHHJJJ!!!&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdontic%2Fdontic.github.io%2Fmain%2Fsrc%2Fcontent%2Fblog%2Fautomatically-build-docker-images-with-github-actions%2Ftableflip.webp" alt="Flip Table Gif" width="342" height="100"&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Here's how I do it now:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Push a new semantic versioning (&lt;a href="https://semver.org/" rel="noopener noreferrer"&gt;semver&lt;/a&gt;) release tag. i.e. &lt;code&gt;1.0.1&lt;/code&gt; from any branch I like&lt;/li&gt;
&lt;li&gt;That's it!&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;This works perfectly with &lt;a href="https://daniel.es/blog/the-perfect-git-strategy/" rel="noopener noreferrer"&gt;the perfect Git strategy&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fangglqss8j21doz9zrkj.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fangglqss8j21doz9zrkj.gif" alt="Happy Programer" width="400" height="400"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting up a Self Hosted Runner
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;If you already know everything about GitHub actions and just want to know how the structure to build docker images, proceed to GitHub Action to build Docker Images&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Definition&lt;/strong&gt;: A runner is an instance that &lt;em&gt;runs&lt;/em&gt; whatever you want in your GitHub action. From making it print &lt;code&gt;Hello World&lt;/code&gt; to building and deploying apps, to making you coffee (seriously). Think of it as a user that runs whatever you tell it to in a remote server (linux, windows...).&lt;/p&gt;

&lt;p&gt;GitHub gives you 2 options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use one of their runners for free up to X minutes every month and then pay for it&lt;/li&gt;
&lt;li&gt;Spin up a self hosted runner and use that for free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you go with the first option skip this, otherwise continue reading.&lt;/p&gt;

&lt;p&gt;Since I already have a home lab and setting up a runner is quite easy, self hosted was the way to save a few bucks. You can also do this with an old computer or even a raspberry pi you have laying around.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you want to learn more about self hosted runners look into &lt;a href="https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners" rel="noopener noreferrer"&gt;GitHub's documentation&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;This is what you need to set up a self hosted runner:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select the OS you want your runner to be. I'm a fan of Ubuntu and I can pretty much do everything I need with it, so I went with a Linux runner.&lt;/li&gt;
&lt;li&gt;Make sure you have any necessary packages installed in the runner. For my use case I would need to install Docker first.&lt;/li&gt;
&lt;li&gt;Go to your GitHub repo, then Settings
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4lcn9yua00a1di1469nu.png" alt="GitHub repo settings icon" width="133" height="58"&gt;
&lt;/li&gt;
&lt;li&gt;In the left panel hit &lt;strong&gt;Actions&lt;/strong&gt; and then &lt;strong&gt;Runners&lt;/strong&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9g59giwy9sshtnu4bvn9.png" alt="GitHub actions location in side menu" width="396" height="481"&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;New self-hosted Runner&lt;/strong&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr1gwaqhdd28nc9wa76q1.png" alt="New self-hosted runner button" width="220" height="54"&gt;
&lt;/li&gt;
&lt;li&gt;Select the OS you want to use and architecture
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwfz10qw7zbwy9zfkskqv.png" alt="OS runner Options" width="800" height="184"&gt;
&lt;/li&gt;
&lt;li&gt;Follow the terminal commands displayed below your selection to finish setting up your runner. I do not want to post the commands here since GitHub updates them every once in a while, so just follow their instructions over there.&lt;/li&gt;
&lt;li&gt;Once you finish setting the runner up in your Linux machine you should see it in the Runners page from before and you're ready to use it!
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5dhnx1tb9i1p6iv9eiww.png" alt="Runner idle card" width="800" height="102"&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Structure of a GitHub Action
&lt;/h2&gt;

&lt;p&gt;GitHub actions are defined in &lt;code&gt;.yml&lt;/code&gt; files. They are basically a set of instructions telling the runner what to do.&lt;/p&gt;

&lt;p&gt;They live under &lt;code&gt;.github/workflows/&lt;/code&gt; in your repository and &lt;strong&gt;will be live and working once you push them to your main branch&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You can either write them directly with GitHub's web interface or with your favorite code editor and push them to your main branch.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you use VSCode they have a GitHub Actions extension that comes in really handy to edit the workflows and see what's running&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  A (simple) GitHub action has 4 main parts:
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. &lt;code&gt;name&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Easy, this is just the name you want to give the action to track it later on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;My GitHub action name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. &lt;code&gt;on&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;This is what you want to trigger the action on. The most common case is to tricker an action on a push to a specific branch or when opening a pull request. But there are many triggers.&lt;/p&gt;

&lt;p&gt;You can see documentation on all the triggers here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows" rel="noopener noreferrer"&gt;https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's an example code to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Be able to trigger the GitHub action manually. For more information on how to trigger this from the GitHub web interface, see "&lt;a href="https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow" rel="noopener noreferrer"&gt;Manually running a workflow&lt;/a&gt;."&lt;/li&gt;
&lt;li&gt;Trigger the action when there's a push to the &lt;code&gt;my-branch-name&lt;/code&gt; and &lt;code&gt;my-other-branch-name&lt;/code&gt; branches.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Trigger the action manually from the UI&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Trigger the action when pushing to certain branches&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my-branch-name'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my-other-branch-name'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. &lt;code&gt;env&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Here you can set environment variables and secrets for a specific Action. This can be used together with the repository Variables and Secrets in the GitHub website.&lt;/p&gt;

&lt;p&gt;If there's something you use in multiple steps or jobs it is useful to set it up here so that you only need to change one line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;DOCKER_IMAGE_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-image&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  4. &lt;code&gt;jobs&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;These are the individual series of steps you want to execute. Each job has a series of sequential steps and every job can be run asynchronously. You can also create a dependency so a job doesn't run until a previous job has finished.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Differences between Jobs and Steps&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Jobs&lt;/strong&gt; are individual, asynchronous tasks. This means that if you have several tasks that can happen asynchronously (at the same time) (i.e.: deploying an app and uploading new documentation) you can put them in separate jobs. If you have several runners you can execute several jobs at the same time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steps&lt;/strong&gt; are actions that happen synchronously. If you have tasks that need to run one after the other (i.e.: Building a docker image and then pushing it to a Docker registry) set them up in steps instead of jobs.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sample code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build_docker_images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Job name that shows in the GitHub UI&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build Docker Images&lt;/span&gt;
    &lt;span class="c1"&gt;# Runner to use&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;self-hosted&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

   &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can set up as many jobs and steps as you like in a single action file.&lt;/p&gt;




&lt;h2&gt;
  
  
  GitHub Action to build Docker Images
&lt;/h2&gt;

&lt;p&gt;In this section I will show you how to build a docker image from a &lt;code&gt;Dockerfile&lt;/code&gt; and push it to Dockerhub or your private Docker Registry. All by just creating a version tag.&lt;/p&gt;

&lt;h3&gt;
  
  
  Process overview &amp;amp; requirements
&lt;/h3&gt;

&lt;p&gt;First, if you're using a self-hosted runner, check that it's running and available in the repository you want to have this on. And that you understand the structure of a GitHub action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here are the requirements:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I push a version tag, I want to get the version name (i.e.: &lt;code&gt;1.0.1&lt;/code&gt;), then build the Docker image and tag it with that version number as well as the &lt;code&gt;latest&lt;/code&gt; tag (i.e.: &lt;code&gt;my-image:1.0.1&lt;/code&gt; and &lt;code&gt;my-image:latest&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;After this, I want to push the image to a private Docker Registry I self-host and remove all data from the runner. But you can also do this to push the docker image to DockerHub.&lt;/p&gt;

&lt;p&gt;I also want to store the build cache so that future images build faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's the process overview:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Trigger the action manually or by pushing a version tag&lt;/li&gt;
&lt;li&gt;Git checkout the code from that specific tag&lt;/li&gt;
&lt;li&gt;Get the version number from the tag&lt;/li&gt;
&lt;li&gt;Build the Docker image&lt;/li&gt;
&lt;li&gt;Once the image is built, tag it with the version number and the docker registry of my choosing&lt;/li&gt;
&lt;li&gt;Push the image to the registry&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  1. Environment variables and secrets
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Environment variables&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In case I ever need to change the registry URL or image name I set the following environment variables to be able to do so easily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Workflow environment variables&lt;/span&gt;
&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;DOCKER_IMAGE_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-image&lt;/span&gt;
  &lt;span class="na"&gt;DOCKER_REGISTRY_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myregistry.domain.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Secrets&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I also want to create these github action secrets in my reo to be able to access the docker registry. I like to create a comment in my workflow file so I don't forget if I move the repository or something.&lt;/p&gt;

&lt;p&gt;Please, for the love of God, DO NOT PUT THESE IN YOUR WORKFLOW FILE.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Required actions secrets:&lt;/span&gt;
&lt;span class="c1"&gt;# - DOCKER_USERNAME: Username for docker registry login&lt;/span&gt;
&lt;span class="c1"&gt;# - DOCKER_PASSWORD: Password for docker registry login&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Triggers
&lt;/h3&gt;

&lt;p&gt;I want not only for the action to be triggered when I push a version tag, but also manually.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build release Docker image&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Trigger the action manually from the UI&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Trigger the action when a version tag is pushed&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[0-9]+.[0-9]+.[0-9]+'&lt;/span&gt; &lt;span class="c1"&gt;# Push events to matching numeric semver tags, i.e., 1.0.0, 20.15.10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Job Setup
&lt;/h3&gt;

&lt;p&gt;I will only require a single job since all my steps need to be sequential.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build_docker_images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Job name that shows in the GitHub Website:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build Docker Images&lt;/span&gt;
    &lt;span class="c1"&gt;# Runner to use:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;self-hosted&lt;/span&gt;

    &lt;span class="c1"&gt;# Begin the steps&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;step1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;step2&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  4.1 Get the code from GitHub
&lt;/h4&gt;

&lt;p&gt;GitHub, and many other services like Docker, provide ready-to-use steps. In this case we're going to use the &lt;code&gt;actions/checkout@v3&lt;/code&gt; step from GitHub, which just fetches the code from the repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Checkout the code&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="c1"&gt;# Do not get extra git branches to save time&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  4.2 Set up docker builder
&lt;/h4&gt;

&lt;p&gt;Like GitHub, Docker provides their own set of ready-to-use steps, in this case there is a builder setup action&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Docker Buildx&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/setup-buildx-action@v3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  4.3 Login into the Docker Registry
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login to Docker Registry&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v3&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.DOCKER_REGISTRY }}&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKER_USERNAME }}&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKER_PASSWORD }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  4.4 Get the version number
&lt;/h4&gt;

&lt;p&gt;Here I just get the tag name (&lt;code&gt;X.Y.Z&lt;/code&gt;). Since we constrained the triggers to only trigger on numeric semver tags, we confidently know that the tag will be in the right format: &lt;code&gt;X.Y.Z&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Get the tag name&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;get_version&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "VERSION=${GITHUB_REF#refs/tags/}" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  4.5 Build and push the docker images
&lt;/h4&gt;

&lt;p&gt;Docker provides a super convenient ready-to-use action for this step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and Push the Docker Image&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v6&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE_NAME }}:${{ steps.get_version.outputs.VERSION }}&lt;/span&gt;
      &lt;span class="s"&gt;${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE_NAME }}:latest&lt;/span&gt;
    &lt;span class="na"&gt;cache-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=registry,ref=${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE_NAME }}:buildcache&lt;/span&gt;
    &lt;span class="na"&gt;cache-to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=registry,ref=${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE_NAME }}:buildcache,mode=max&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since this step is not as straightforward let me explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;context: .&lt;/code&gt;: We're telling the builder to use the root folder of the repository as the build context. This is useful for instance if you have a monorepo with a backend and a frontend directory. You can create two jobs for each where the context changes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;file: ./Dockerfile&lt;/code&gt;: We're telling the builder where the Dockerfile is&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;push: true&lt;/code&gt;: We want to push the images as well&lt;/li&gt;
&lt;li&gt;The tags section indicates that we want to push two tags of this image. This is also useful if we want to push to multiple directories for instance. Here I use the environment variables we defined at the begining of the workflow and I'm pushing the version tag as well as the latest tag.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cache-from:&lt;/code&gt;: This means where we're pulling the build cache from, to make consequent builds faster. In this case we get it from the registry.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cache-to:&lt;/code&gt;: Similar to &lt;code&gt;chache-from&lt;/code&gt;, we're indicating where the build cache should be stored, in this case it's the registry.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Full GitHub Actions template to release a docker image
&lt;/h2&gt;

&lt;p&gt;Here's the full action code. You can copy and paste it into your YML file and modify it as needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This workflow will build a Docker image&lt;/span&gt;
&lt;span class="c1"&gt;# and push them to a private docker registry when a release tag (i.e.: 1.0.1)&lt;/span&gt;
&lt;span class="c1"&gt;# is pushed to the repository.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# It runs on a self hosted runner.&lt;/span&gt;
&lt;span class="c1"&gt;# You can change the runner to 'ubuntu-latest' if you want to use a GitHub hosted runner.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Required actions secrets in the repository:&lt;/span&gt;
&lt;span class="c1"&gt;# - DOCKER_USERNAME: Username for docker registry login&lt;/span&gt;
&lt;span class="c1"&gt;# - DOCKER_PASSWORD: Password for docker registry login&lt;/span&gt;

&lt;span class="c1"&gt;# Script environment variables&lt;/span&gt;
&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;DOCKER_IMAGE_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-image&lt;/span&gt;
  &lt;span class="na"&gt;DOCKER_REGISTRY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myregistry.mydomain.com&lt;/span&gt;

&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and Push Docker Image&lt;/span&gt;

&lt;span class="c1"&gt;# Triggers&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Trigger the action manually from the UI&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Trigger the action when a version tag is pushed&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[0-9]+.[0-9]+.[0-9]+'&lt;/span&gt; &lt;span class="c1"&gt;# Push events to matching numeric semver tags, i.e., 1.0.0, 20.15.10&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-and-push-landing-page&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and Push Landing Page Docker Image&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;self-hosted&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Docker Buildx&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/setup-buildx-action@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login to Docker Registry&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.DOCKER_REGISTRY }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKER_USERNAME }}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKER_PASSWORD }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Get the tag name&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;get_version&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "VERSION=${GITHUB_REF#refs/tags/}" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and push Landing Page Docker image&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
          &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./Dockerfile&lt;/span&gt;
          &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE_NAME }}:${{ steps.get_version.outputs.VERSION }}&lt;/span&gt;
            &lt;span class="s"&gt;${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE_NAME }}:latest&lt;/span&gt;
          &lt;span class="na"&gt;cache-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=registry,ref=${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE_NAME }}:buildcache&lt;/span&gt;
          &lt;span class="na"&gt;cache-to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=registry,ref=${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE_NAME }}:buildcache,mode=max&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;With this set up you now automatically will have Docker images ready for use in the Registry of your choice.&lt;/p&gt;

&lt;p&gt;I will create a future article showing how to automatically deploy a docker image once it's built and pushed to a registry.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>github</category>
      <category>webdev</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Connect to WireGuard clients from LAN (with PFSense)</title>
      <dc:creator>Daniel</dc:creator>
      <pubDate>Wed, 27 Mar 2024 17:37:11 +0000</pubDate>
      <link>https://dev.to/onticdani/connect-to-wireguard-clients-from-lan-with-pfsense-1lfl</link>
      <guid>https://dev.to/onticdani/connect-to-wireguard-clients-from-lan-with-pfsense-1lfl</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;Problem Setup and Variables&lt;/li&gt;
&lt;li&gt;Static Routes&lt;/li&gt;
&lt;li&gt;Create a Static Route in PFSense&lt;/li&gt;
&lt;/ul&gt;



&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Here's my particular situation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I'm developing &lt;a href="https://getemil.io" rel="noopener noreferrer"&gt;Emilio&lt;/a&gt;. Which requires to have an accessible endpoint so that I can query stuff from a Gmail Addon.&lt;/li&gt;
&lt;li&gt;I have Nginx Proxy Manager at home routing requests made to my development url to my laptop so I can test this endpoint.&lt;/li&gt;
&lt;li&gt;I have a WireGuard VPN server at home that I can use to connect my devices to my local network when I travel.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You might have a similar issue if you have set up a server somewhere, say, your friend's home, and you want that server to be part of your local network. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;:&lt;br&gt;
WireGuard clients (my laptop) have a totally different subnet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your local subnet (probably): &lt;code&gt;192.168.0.0&lt;/code&gt; (&lt;code&gt;255.255.255.0&lt;/code&gt; as the mask or &lt;code&gt;/24&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;WireGuard subnet: &lt;code&gt;10.6.0.0/24&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means that, while I can ping LAN devices from my laptop when connected through WireGuard, I cannot do the opposite.&lt;/p&gt;

&lt;p&gt;Here's a solution that worked for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem Setup and Variables
&lt;/h2&gt;

&lt;p&gt;To make this guide clearer I will create fictional addresses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;My laptop's WireGuard IP: &lt;code&gt;10.6.0.12&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;WireGuard's server internal IP at home: &lt;code&gt;192.168.0.59&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I want&lt;/strong&gt;:&lt;br&gt;
NGINX routing requests from &lt;code&gt;test.domain.com&lt;/code&gt; to &lt;code&gt;10.6.0.12&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static Routes
&lt;/h2&gt;

&lt;p&gt;I want to tell my NGINX host or any device in my LAN for that matter, where to point when I request the IP &lt;code&gt;10.6.0.12&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is a great use case for static routes.&lt;/p&gt;

&lt;p&gt;To create a static route you need 3 things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Gateway: What's the entry point for the subnet you want to connect to.&lt;/li&gt;
&lt;li&gt;The subnet you want to connect to&lt;/li&gt;
&lt;li&gt;The subnet mask&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In my case:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Gateway: &lt;code&gt;192.168.0.59&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Subnet: &lt;code&gt;10.6.0.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Subnet mask: &lt;code&gt;/24&lt;/code&gt; or &lt;code&gt;255.255.255.0&lt;/code&gt; (they're 2 ways of saying the same)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Create a Static Route in PFSense
&lt;/h2&gt;

&lt;p&gt;I use PFSense at home, so I will demonstrate how to do it in there. If you have another router or firewall just search how to add a static route in your specific model.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;code&gt;System&lt;/code&gt; &amp;gt; &lt;code&gt;Routing&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;In the &lt;code&gt;Gateways&lt;/code&gt; tab, add a new gateway.&lt;/li&gt;
&lt;li&gt;Give the gateway the name you want and set the IP, in my case &lt;code&gt;192.168.0.59&lt;/code&gt; (the WireGuard server at home).&lt;/li&gt;
&lt;li&gt;Apply the changes.&lt;/li&gt;
&lt;li&gt;Now in the &lt;code&gt;Routing&lt;/code&gt; menu, go to &lt;code&gt;Static Routes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Hit &lt;code&gt;+ Add&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;In Destination Network add the WireGuard subnet, in my case &lt;code&gt;10.6.0.0/24&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Select the Gateway we just added&lt;/li&gt;
&lt;li&gt;Give it a description if you want&lt;/li&gt;
&lt;li&gt;Save and apply changes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Done!&lt;/p&gt;

&lt;p&gt;Now you should be able to ping your laptop from your LAN. It might take a few minutes though, so be patient.&lt;/p&gt;

&lt;p&gt;In my case I can now create an NGINX proxy that redirects &lt;code&gt;test.domain.com&lt;/code&gt; to my laptop while away at &lt;code&gt;10.6.0.12&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>pfsense</category>
      <category>network</category>
      <category>wireguard</category>
      <category>vpn</category>
    </item>
    <item>
      <title>How to query logs in Grafana Loki</title>
      <dc:creator>Daniel</dc:creator>
      <pubDate>Fri, 22 Mar 2024 09:39:19 +0000</pubDate>
      <link>https://dev.to/onticdani/how-to-query-logs-in-grafana-loki-59jh</link>
      <guid>https://dev.to/onticdani/how-to-query-logs-in-grafana-loki-59jh</guid>
      <description>&lt;p&gt;&lt;strong&gt;In this article:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;Querying your first logs&lt;/li&gt;
&lt;li&gt;Easily filter logs in Grafana&lt;/li&gt;
&lt;li&gt;How to parse Loki logs in Grafana&lt;/li&gt;
&lt;li&gt;Using context in Grafana Loki&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I've previously wrote an article on &lt;a href="https://dev.to/onticdani/how-to-centralize-and-visualize-your-app-logs-in-grafana-483c"&gt;how to centralize and easily query logs with Promtail + Loki + Grafana&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this article I will focus on the querying part. How we're doing it for &lt;a href="https://getemil.io" rel="noopener noreferrer"&gt;Emilio&lt;/a&gt; and how to properly query logs in Grafana and how to parse your log format to easily filter the lines you want to see.&lt;/p&gt;




&lt;h2&gt;
  
  
  Querying your first logs
&lt;/h2&gt;

&lt;p&gt;While you can create fancy dashboards, querying logs really shines in the &lt;strong&gt;Explore&lt;/strong&gt; view in Grafana.&lt;/p&gt;

&lt;p&gt;You can find that view in the left panel:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmaykt7ue57yte37z43bx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmaykt7ue57yte37z43bx.png" alt="Grafana's left panel" width="362" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've followed my previous tutorial you will be able to select either a job or a filename in label filters:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpoh3dwabssoospelfj6j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpoh3dwabssoospelfj6j.png" alt="Loki job and filename selector" width="399" height="213"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now at the top right, hit run query:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ez6codqm7laja2xtsry.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ez6codqm7laja2xtsry.png" alt="Grafana Loki run query button" width="190" height="61"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, you will see something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2d59efjp6hehj3vs2n8r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2d59efjp6hehj3vs2n8r.png" alt="Grafana Loki explore view" width="800" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The UI consists on a bar graph indicating the amount of logs over time and a query of 1000 lines of the latest logs.&lt;/p&gt;

&lt;p&gt;In the top panel you can adjust the timeframe for your query:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq00pamwvm648znc4148w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq00pamwvm648znc4148w.png" alt="Grafana Loki timeframe selector" width="800" height="180"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And in the query panel you can change the limit. Just be aware that increasing the limit too much might crash your Loki instance:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6dvvbiz4vycqbqjktyep.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6dvvbiz4vycqbqjktyep.png" alt="Grafana Loki query limit" width="565" height="457"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Easily filter logs in Grafana
&lt;/h2&gt;

&lt;p&gt;It's quite easy to start filtering logs.&lt;/p&gt;

&lt;p&gt;Start by hitting: &lt;strong&gt;Operations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpwsgq8ib5y7sd1t5iqhf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpwsgq8ib5y7sd1t5iqhf.png" alt="Grafana Loki query operations button" width="176" height="64"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are the current operations available at the time of writing this post:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnotu57o4cljnpzzajxtj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnotu57o4cljnpzzajxtj.png" alt="Grafana Loki query operations available" width="352" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The most useful one when you're just starting is &lt;strong&gt;Line filters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F91366d9n7rq3265bdwqs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F91366d9n7rq3265bdwqs.png" alt="Grafana Loki line filter operator" width="633" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can then choose the filter you'd like to search specific lines. Here's an example to query lines containing "ERROR":&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqy08tf9l30oojjejdws0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqy08tf9l30oojjejdws0.png" alt="Grafana Loki line filter operator example" width="235" height="155"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can add as many operations as you want.&lt;/p&gt;

&lt;p&gt;The bar graph is then a great way to see how many times the lines with your query happened over time. Cool to track errors, warnings, specific errors...&lt;/p&gt;




&lt;h2&gt;
  
  
  How to parse Loki logs in Grafana
&lt;/h2&gt;

&lt;p&gt;This is a really cool thing that Grafana allows you to do and helps filtering a LOT.&lt;/p&gt;

&lt;p&gt;I'll use our logs format as an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2024-03-19 09:48:57,608 - 244843ce48f8 - INFO - views - History ID: 123782
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The log pattern is as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;datetime&amp;gt; - &amp;lt;docker_container_id&amp;gt; - &amp;lt;log_level&amp;gt; - &amp;lt;module&amp;gt; - &amp;lt;log_message&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For you to have more reference, you could also do fancy stuff like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.191.12.2 - - [10/Jun/2021:09:14:29 +0000] "GET /api/plugins/versioncheck HTTP/1.1" 200 2 "-" "Go-http-client/2.0" "13.76.247.102, 34.120.177.193" "TLSv1.2" "US" ""
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;ip&amp;gt; - - &amp;lt;_&amp;gt; "&amp;lt;method&amp;gt; &amp;lt;uri&amp;gt; &amp;lt;_&amp;gt;" &amp;lt;status&amp;gt; &amp;lt;size&amp;gt; &amp;lt;_&amp;gt; "&amp;lt;agent&amp;gt;" &amp;lt;_&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To parse the lines with a specific pattern like ours, go to &lt;strong&gt;+ Operations&lt;/strong&gt;, &lt;strong&gt;Formats&lt;/strong&gt; and then &lt;strong&gt;Patterns&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fme10232wcweqctyz5014.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fme10232wcweqctyz5014.png" alt="Log pattern menu" width="435" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then you just insert the pattern with the same format as above:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbq0knibd54strw79jdbp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbq0knibd54strw79jdbp.png" alt="Log pattern example in Grafana" width="800" height="180"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you query again the logs you will see that nothing special happens, but with this we can now query on specific labels!&lt;/p&gt;

&lt;p&gt;Hit &lt;strong&gt;+ Operations&lt;/strong&gt; and select &lt;strong&gt;Label Filters&lt;/strong&gt; and then &lt;strong&gt;Label Filter Expressions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fltqtagf8t30nslxt0x50.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fltqtagf8t30nslxt0x50.png" alt="Label Filters menu" width="800" height="216"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With these I can now filter the logs really selectively.&lt;/p&gt;

&lt;p&gt;Let's say I want to query ERROR logs for the user "daniel":&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Finnt4vt6b5l6f6fz6sen.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Finnt4vt6b5l6f6fz6sen.png" alt="Loki log query using labels" width="800" height="133"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you can combine labels and filters to really find what you're looking for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using context in Grafana Loki
&lt;/h2&gt;

&lt;p&gt;Grafana released this last year and it's AMAZING.&lt;/p&gt;

&lt;p&gt;Say you found instances of an error. Great, but what information does that give you? You want to see what happened before and after that log line to see the cause and consequence of that error.&lt;/p&gt;

&lt;p&gt;For this, it's really easy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hover above the line you want to see context for&lt;/li&gt;
&lt;li&gt;Hit the &lt;em&gt;show context&lt;/em&gt; icon at the top right of the line&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;A new window will open were you will see the context (logs before and after) of that line:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhzq12h20kb825emh0xdd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhzq12h20kb825emh0xdd.png" alt="Loki log context view" width="800" height="553"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You can now use labels to filter different filenames, jobs or log labels!&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmzx6emfw88fast766e8e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmzx6emfw88fast766e8e.png" alt="Filtering context with labels" width="800" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>grafana</category>
      <category>loki</category>
      <category>webdev</category>
      <category>logging</category>
    </item>
  </channel>
</rss>
