<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Andre Wong's blog]]></title><description><![CDATA[Andre Wong's blog]]></description><link>https://blog.wongandre.com</link><generator>RSS for Node</generator><lastBuildDate>Mon, 13 Apr 2026 13:58:31 GMT</lastBuildDate><atom:link href="https://blog.wongandre.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Openclaw introduction]]></title><description><![CDATA[Setting Up OpenClaw on a Self-Hosted Server
I've been experimenting with OpenClaw for a few weeks, and I'm excited about what it can do for me. OpenClaw is an open-source AI-agent framework that lets ]]></description><link>https://blog.wongandre.com/openclaw-introduction</link><guid isPermaLink="true">https://blog.wongandre.com/openclaw-introduction</guid><category><![CDATA[openclaw]]></category><dc:creator><![CDATA[Andre Wong]]></dc:creator><pubDate>Sun, 08 Mar 2026 16:15:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/64054c5f024dffb6cd378e17/ded46250-34ea-4956-a6ad-068b49d638a0.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Setting Up OpenClaw on a Self-Hosted Server</h2>
<p>I've been experimenting with OpenClaw for a few weeks, and I'm excited about what it can do for me. OpenClaw is an open-source AI-agent framework that lets agents interact with tools and skills while I chat with them on channels like Telegram or Discord. The best part is that it can act as a personal assistant, handling day-to-day activities.</p>
<p>In this post, I will go over how I set up OpenClaw on a Linux VM, connected it to Telegram, and gave it access to Google services like Calendar, Gmail, and Tasks. Since OpenClaw by itself comes with a few security risks, it is best to have an architecture whereby the AI agent does not get too much information/data about my credentials that I provide. The goal is to create an AI agent that can interact with my tools and accounts without exposing my main credentials.</p>
<h2>Installing OpenClaw</h2>
<p>First, follow the official installation guide:</p>
<p><a href="https://docs.openclaw.ai/start/getting-started">https://docs.openclaw.ai/start/getting-started</a></p>
<p>For my setup:</p>
<ul>
<li><p>Created a Proxmox virtual machine and installed Ubuntu Server (ubuntu-22.04.5-live-server-amd64.iso) on it</p>
</li>
<li><p>I am using <code>gpt-3.5-codex-medium</code> as my LLM model, because I have one month free subscription for OpenAI's Plus plan. This gives me an ample amount of credits to use for OpenClaw.</p>
</li>
<li><p>I added a Telegram channel for OpenClaw, the main area where I will be chatting with it. This can be created using the BotFather bot and calling <code>/newbot</code>. After creating it, it will provide you a token as shown below, where you can input it during the OpenClaw setup.</p>
<ul>
<li><img src="https://cdn.hashnode.com/uploads/covers/64054c5f024dffb6cd378e17/f2159469-6c2f-4303-ab2c-aac4863e6a29.png" alt="" style="display:block;margin:0 auto" />

The first chat will probably be blocked by the gateway. Run the following command to authorize the channel: <code>openclaw pairing approve telegram ABCDEFG</code></li>
</ul>
</li>
</ul>
<p>Once installed, you should be able to start the OpenClaw agent and interact with it through the CLI or Telegram. The first time it's being run, it will ask questions about you and itself, where you can provide information about yourself.</p>
<p>After interacting with the AI agent. It will write itself into AGENTS.md, MEMORY.md, SOUL.md, USER.md, so that next time when it runs on a new session again, it can be loaded back in the context and will seem as though it remembers things about you. You can also manually edit these files to your liking as well.</p>
<h2>Configuring Git for the Agent</h2>
<p>Some OpenClaw workflows require Git access (for example, committing code changes or interacting with repositories).</p>
<p>Configure the Git identity used by the agent:</p>
<pre><code class="language-plaintext">git config --global user.name "name"
git config --global user.email "name@gmail.com"
</code></pre>
<p>This ensures that any commits created by the agent are properly attributed.</p>
<h2>Giving OpenClaw Access to Google Services</h2>
<p>One of the more interesting things I wanted the agent to do was interact with:</p>
<ul>
<li><p>Gmail</p>
</li>
<li><p>Google Calendar</p>
</li>
<li><p>Google Tasks</p>
</li>
</ul>
<p>Instead of exposing my personal Google account, I created a separate Gmail account dedicated to OpenClaw. It is free to execute things on its own Google account without having authorization to my personal Google account. This helps further reduce risk, and I will not be surprised to find out one day that all my emails were deleted by AI.</p>
<p>I will be installing the gog skill on <a href="https://clawhub.ai/steipete/gog">Clawdhub.ai</a>. OpenClaw allows you to extend the agent by creating skills. This skill allows me to connect to Gmail, Calendar, Drive and others. It requires the gogcli and oauth to be set up first.</p>
<h3>Installing gog skill</h3>
<p>Create the skill directory:</p>
<pre><code class="language-plaintext">mkdir -p .openclaw/skills/gog
</code></pre>
<p>Then create the skill definition file:</p>
<pre><code class="language-plaintext">vim .openclaw/skills/gog/SKILL.md
</code></pre>
<p>This file tells OpenClaw how to use the tool. Means the AI will read this skill, understand it and then execute tools such as gogcli. <a href="http://SKILL.md">SKILL.md</a> can be directly copied from the <a href="https://clawhub.ai/steipete/gog">clawhub website here</a></p>
<h3>Installing gogcli</h3>
<p><a href="https://github.com/steipete/gogcli">https://github.com/steipete/gogcli</a></p>
<p>Download the binary:</p>
<pre><code class="language-shell">wget https://github.com/steipete/gogcli/releases/download/v0.11.0/gogcli_0.11.0_linux_amd64.tar.gz
</code></pre>
<p>Extract it:</p>
<pre><code class="language-shell">tar -xzf gogcli_0.11.0_linux_amd64.tar.gz
</code></pre>
<p>Move the binary into your path:</p>
<pre><code class="language-shell">sudo mv gog /usr/local/bin/
</code></pre>
<p>Verify the installation:</p>
<pre><code class="language-plaintext">gog --help
</code></pre>
<h3>Creating Google OAuth Credentials</h3>
<p>To allow <code>gogcli</code> to access Google services, we need to create OAuth credentials.</p>
<p>Open the Google Cloud Console credentials page:</p>
<pre><code class="language-plaintext">https://console.cloud.google.com/apis/credentials
</code></pre>
<p>Then follow these steps:</p>
<ol>
<li><p>Create a new project</p>
</li>
<li><p>Enable APIs</p>
<ul>
<li><p>Gmail API</p>
</li>
<li><p>Calendar API</p>
</li>
<li><p>Tasks API</p>
</li>
</ul>
</li>
<li><p>Configure the OAuth consent screen</p>
</li>
<li><p>Add your Gmail account as a test user</p>
</li>
<li><p>Create an OAuth client</p>
</li>
</ol>
<p>Download the credentials file:</p>
<pre><code class="language-plaintext">client_secret_....apps.googleusercontent.com.json
</code></pre>
<h3>Authenticating gogcli</h3>
<p>Once you have the OAuth credentials, store them using:</p>
<pre><code class="language-plaintext">gog auth credentials ./client_secret_.apps.googleusercontent.com.json
</code></pre>
<p>Then authorize your Google account:</p>
<pre><code class="language-plaintext">gog auth add name@gmail.com --services user --manual
</code></pre>
<p>You will be prompted for a keyring password. For simplicity, I used <code>openclaw</code>.</p>
<p>In <code>SKILL.md</code>, update the instructions so the agent knows how to run the tool properly. Running the tool in Linux requires the <code>GOG_KEYRING_PASSWORD</code> environment variable.</p>
<pre><code class="language-plaintext">Notes

- Set `GOG_ACCOUNT=name@gmail.com` to avoid repeating `--account`.
- Always set `GOG_KEYRING_PASSWORD=openclaw`.
- Always add `--no-input` when executing commands.
</code></pre>
<h2>What the Agent Can Do Now</h2>
<p>After completing the setup, the OpenClaw agent can now:</p>
<ul>
<li><p>Read my Google Calendar</p>
</li>
<li><p>Check emails</p>
</li>
<li><p>View tasks</p>
</li>
</ul>
<p>I then subscribed to the calendar of my main Google account. After giving my permission to read and write, I can tell my AI agent to add a schedule for plans and then every night, notify me of all the things I have in my calendar for tomorrow.</p>
<img src="https://cdn.hashnode.com/uploads/covers/64054c5f024dffb6cd378e17/266d996e-2f27-4afe-840d-11c9edb4e674.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/64054c5f024dffb6cd378e17/8eaa3fa8-e00a-4c56-ab7b-9ad09364d8a0.png" alt="" style="display:block;margin:0 auto" />

<p>Here are the things we have done for now:</p>
<img src="https://cdn.hashnode.com/uploads/covers/64054c5f024dffb6cd378e17/881a50f9-b3fe-47cc-b906-a2556a48e694.png" alt="" style="display:block;margin:0 auto" />

<p>more t</p>
]]></content:encoded></item><item><title><![CDATA[Running ollama on Proxmox VM]]></title><description><![CDATA[I am using a Gigabyte GTX1060 G1 Gaming 3GB GDDR5 as my GPU to run ollama models. To do that, I first need to do a GPU passthrough to my virtual machine. Reference for this tutorial
proxmox host setup for GPU passthrough

Verify CPU supports hardware...]]></description><link>https://blog.wongandre.com/running-ollama-on-proxmox-vm</link><guid isPermaLink="true">https://blog.wongandre.com/running-ollama-on-proxmox-vm</guid><category><![CDATA[ollama]]></category><category><![CDATA[proxmox]]></category><category><![CDATA[GPU]]></category><dc:creator><![CDATA[Andre Wong]]></dc:creator><pubDate>Mon, 07 Apr 2025 15:09:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744038485372/72a50d77-7f81-4a95-bd00-65bc3a51f8f9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I am using a Gigabyte GTX1060 G1 Gaming 3GB GDDR5 as my GPU to run ollama models. To do that, I first need to do a GPU passthrough to my virtual machine. <a target="_blank" href="https://pve.proxmox.com/wiki/PCI_Passthrough#GPU_passthrough">Reference for this tutorial</a></p>
<h2 id="heading-proxmox-host-setup-for-gpu-passthrough">proxmox host setup for GPU passthrough</h2>
<ol>
<li><p>Verify CPU supports hardware virtualization and IOMMU</p>
<ul>
<li><p>might have to enable settings in the BIOS</p>
</li>
<li><p>mine was under Asus BIOS → CPU advanced settings → vmx virtualization technology</p>
</li>
</ul>
</li>
<li><p>In the Proxmox host machine shell, add <code>intel_iommu=on</code> to <code>/etc/default/grub</code></p>
<pre><code class="lang-bash"> <span class="hljs-comment"># /etc/default/grub</span>

 GRUB_CMDLINE_LINUX_DEFAULT=<span class="hljs-string">"intel_iommu=on"</span>
</code></pre>
</li>
<li><p><code>update-grub</code> and then <code>sudo reboot</code> to save settings</p>
</li>
<li><p>Verify IOMMU is turned on: <code>dmesg | grep -e DMAR -e IOMMU</code></p>
<pre><code class="lang-bash"> dmesg | grep -e DMAR -e IOMMU

 [    0.007531] ACPI: DMAR 0x000000008EC3B3C0 000070 (v01 INTEL  KBL      00000001 INTL 00000001)
 [    0.007554] ACPI: Reserving DMAR table memory at [mem 0x8ec3b3c0-0x8ec3b42f]
 [    0.027442] DMAR: IOMMU enabled
 [    0.074297] DMAR: Host address width 39
 [    0.074298] DMAR: DRHD base: 0x000000fed90000 flags: 0x1
 [    0.074303] DMAR: dmar0: reg_base_addr fed90000 ver 1:0 <span class="hljs-built_in">cap</span> d2008c40660462 ecap f050da
 [    0.074305] DMAR: RMRR base: 0x0000008eaee000 end: 0x0000008eb0dfff
 [    0.074308] DMAR-IR: IOAPIC id 2 under DRHD base  0xfed90000 IOMMU 0
 [    0.074309] DMAR-IR: HPET id 0 under DRHD base 0xfed90000
 [    0.074310] DMAR-IR: Queued invalidation will be enabled to support x2apic and Intr-remapping.
 [    0.075656] DMAR-IR: Enabled IRQ remapping <span class="hljs-keyword">in</span> x2apic mode
 [    0.262942] DMAR: No ATSR found
 [    0.262943] DMAR: No SATC found
 [    0.262944] DMAR: dmar0: Using Queued invalidation
 [    0.263559] DMAR: Intel(R) Virtualization Technology <span class="hljs-keyword">for</span> Directed I/O
</code></pre>
</li>
<li><p>we do not want the host machine to use the GPU, so we blacklist Nvidia drivers</p>
<pre><code class="lang-bash"> <span class="hljs-comment"># /etc/modprobe.d/blacklist.conf</span>

 blacklist nouveau
 blacklist nvidiafb
</code></pre>
</li>
<li><p>Find our GPU hardware device, <code>lspci -nn</code></p>
<p> We can see 01:00.0 and 01:00.1 here correspond to our graphics card, and lets get the vendor and device ID: <code>10de:1c02</code> and <code>10de:10f1</code></p>
<pre><code class="lang-bash"> lscpi -nn

 01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GP106 [GeForce GTX 1060 3GB] [10de:1c02] (rev a1)
 01:00.1 Audio device [0403]: NVIDIA Corporation GP106 High Definition Audio Controller [10de:10f1] (rev a1)
</code></pre>
</li>
<li><p>Add the graphics card we want to pass through with the vendor and device IDs we found earlier and do a restart</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">echo</span> <span class="hljs-string">"options vfio-pci ids=10de:1c02,10de:10f1 disable_vga=1"</span> &gt; /etc/modprobe.d/vfio.conf
 update-initramfs -u
 sudo reboot
</code></pre>
</li>
</ol>
<h2 id="heading-proxmox-virtual-machine-setup-for-gpu-passthrough">proxmox virtual machine setup for GPU passthrough</h2>
<ol>
<li><p>Most of the steps are similar to creating a normal virtual machine</p>
</li>
<li><p>When selecting a CPU type, be sure to use <code>host</code> instead of a virtual CPU, as it may not support certain instruction sets</p>
</li>
<li><p>Be sure to allocate enough memory for the ollma model that you are using</p>
</li>
<li><p>After the virtual machine is created, do not start it yet, go to hardware → add → PCI device and select our graphics card</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744035697473/04c682c8-e3eb-4c38-b59a-de4ff1e57b6f.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Start our virtual machine, follow the installation, etc</p>
</li>
<li><p>Install GPU drivers in the virtual machine</p>
<pre><code class="lang-bash"> <span class="hljs-comment"># running as su</span>

 add-apt-repository ppa:graphics-drivers/ppa
 apt update
 apt upgrade
 ubuntu-drivers autoinstall
 <span class="hljs-built_in">echo</span> <span class="hljs-string">"blacklist nouveau"</span> &gt; /etc/modprobe.d/blacklist-nouveau.conf
 <span class="hljs-built_in">echo</span> <span class="hljs-string">"options nouveau modeset=0"</span> &gt;&gt; /etc/modprobe.d/blacklist-nouveau.conf
 update-initramfs -u
 reboot
</code></pre>
</li>
<li><p>Check our GPU status</p>
<pre><code class="lang-bash"> +-----------------------------------------------------------------------------------------+
 | NVIDIA-SMI 560.35.03              Driver Version: 560.35.03      CUDA Version: 12.6     |
 |-----------------------------------------+------------------------+----------------------+
 | GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
 | Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
 |                                         |                        |               MIG M. |
 |=========================================+========================+======================|
 |   0  NVIDIA GeForce GTX 1060 3GB    Off |   00000000:01:00.0 Off |                  N/A |
 |  0%   41C    P8              7W /  180W |       5MiB /   3072MiB |      0%      Default |
 |                                         |                        |                  N/A |
 +-----------------------------------------+------------------------+----------------------+

 +-----------------------------------------------------------------------------------------+
 | Processes:                                                                              |
 |  GPU   GI   CI        PID   Type   Process name                              GPU Memory |
 |        ID   ID                                                               Usage      |
 |=========================================================================================|
 |  No running processes found                                                             |
 +-----------------------------------------------------------------------------------------+
</code></pre>
</li>
</ol>
<h2 id="heading-installing-ollama">installing ollama</h2>
<pre><code class="lang-bash">curl -fsSL https://ollama.com/install.sh | sh

ollama run deepseek-r1:14b
</code></pre>
<pre><code class="lang-plaintext"># test ollama inference
&gt;&gt;&gt; what is 2 + 2
&lt;think&gt;
I see the question is asking for the sum of two plus two.

First, I'll identify the numbers involved in the addition.

Next, I'll add them together to find the total.

Finally, I'll present the final answer clearly.
&lt;/think&gt;

Sure! Let's solve \(2 + 2\) step by step:

1. **Identify the Numbers:**
   - We have two numbers to add: 2 and 2.

2. **Add the Numbers:**
   \[
   2 + 2 = 4
   \]

3. **Final Answer:**
   \[
   \boxed{4}
   \]
</code></pre>
<p>Now we can run our model in the command line, we can then connect to a front end to use our model as well.</p>
<h2 id="heading-installing-open-webui">installing open-webui</h2>
<pre><code class="lang-bash"><span class="hljs-comment"># install docker</span>
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh ./get-docker.sh
sudo usermod -aG docker <span class="hljs-variable">$USER</span>
<span class="hljs-built_in">logout</span>

<span class="hljs-comment"># pull open-webui container</span>
docker pull ghcr.io/open-webui/open-webui:main

<span class="hljs-comment"># run container on host network</span>
docker run -d -e OLLAMA_BASE_URL=http://127.0.0.1:11434 -v open-webui:/app/backend/data --network=host --name open-webui ghcr.io/open-webui/open-webui:main

<span class="hljs-comment"># visit website</span>
http://&lt;host ip&gt;:8080
</code></pre>
<ul>
<li><p>After visiting the website and creating an account, go to settings → connection, update the Ollama API connection to <code>http://127.0.0.1:11434</code> (they are in the same virtual machine)</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744036866455/3aeee03e-09fb-40a5-b20e-2323bed8976c.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>We can ask any question like ChatGPT</p>
<ul>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744037830577/3568723e-d399-495f-b1f8-1963f08d27b8.png" alt class="image--center mx-auto" /></p>
<p>  not the best hardware, seeing that it took about 6 minutes to generate a response, but it is something to try =)</p>
</li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[2024 Home lab setup]]></title><description><![CDATA[It’s almost the end of 2024, let me share my current setup for my home lab which I have been working on for the past year.
overview
Here’s a brief overview of my internal network.

Storage server
192.168.1.10 is a mini pc designated as my main storag...]]></description><link>https://blog.wongandre.com/2024-home-lab-setup</link><guid isPermaLink="true">https://blog.wongandre.com/2024-home-lab-setup</guid><category><![CDATA[Homelab]]></category><category><![CDATA[networking]]></category><category><![CDATA[proxmox]]></category><category><![CDATA[Docker]]></category><dc:creator><![CDATA[Andre Wong]]></dc:creator><pubDate>Sun, 29 Dec 2024 03:39:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735443095854/c9d3ba1e-e5c0-4cf5-ae81-23eb74123356.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It’s almost the end of 2024, let me share my current setup for my home lab which I have been working on for the past year.</p>
<h1 id="heading-overview">overview</h1>
<p>Here’s a brief overview of my internal network.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735441521457/30490da0-1658-4d9f-a24f-3b09b6e7a4d4.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-storage-server">Storage server</h2>
<p>192.168.1.10 is a mini pc designated as my main storage server, which I used to set up my network file-sharing server and Samba server. I decided to keep most of my setup here pretty simple with no virtualization and opt to run more critical and fundamental services.</p>
<p>The main services running here are my prometheus-grafana-loki monitoring stack which collects metrics and logs from other hosts for display. I also run Adguard here, which is my DNS resolver for my home network. Authentik is an identity provider that supports ODIC protocol for SSO sign-in to my internal applications. Vaultwarden serves as another password manager I use for authentication in my internal application when Authentik is not an option.</p>
<p>Here are some examples of my monitoring visualization:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735442883253/cbd3908a-b1a3-49d1-9f8a-7d7aadb294af.png" alt="node exporter metrics" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735442900253/d85778db-a77d-4e60-a02c-5e5d53627e1d.png" alt="proxmox metrics" class="image--center mx-auto" /></p>
<h2 id="heading-proxmox-server">proxmox server</h2>
<p>192.168.1.11 is another mini pc running my proxmox operating system that can create virtual machines. Not a lot is done here on this physical machine.</p>
<h2 id="heading-application-server">application server</h2>
<p>192.169.1.13 is a virtual machine running docker. Through docker-compose.yaml scripts I was able to set up my application services quickly. Some applications such as immich and hoarder rely on NFS storage from my storage server. Here I have also set up Caddy reverse proxy, which is the entry point for most of my internal applications. Incoming network request to my internal application is first routed to this reverse proxy and then to other machines through IP and port, or via docker network if on the same host.</p>
<p>Here are what some of the apps do:</p>
<ul>
<li><p>Immich: Google Photo alternative</p>
</li>
<li><p>Paperless: stores documents</p>
</li>
<li><p>Dashy: dashboard</p>
</li>
<li><p>Gitea: git repository and docker image storage</p>
</li>
<li><p>Hoarder: stores website links and web pages</p>
</li>
<li><p>Outline: notion like a note-taking app</p>
</li>
<li><p>Caddy: reverse proxy entry point for most apps</p>
</li>
</ul>
<h2 id="heading-tailscale-server">tailscale server</h2>
<p>192.169.1.12 is another virtual machine that only runs Tailscale. Tailscale provides a VPN, allowing me to connect to my home network even if I am outside. My tailscale node here is configured as an exit node.</p>
<h2 id="heading-kubernetes-cluster">kubernetes cluster</h2>
<p>192.168.1.2 is an old desktop pc running proxmox operating system for most of my testing builds. I can quickly start up and delete virtual machines easily. I also have Kubernetes clusters setup, running 1 master node and 2 worker nodes, right now mainly for experimental purposes only.</p>
<h2 id="heading-dns-connection">DNS connection</h2>
<p>To connect to my internal applications easily, I have set up a DNS Type A record on Cloudflare. So instead of using the IP address and port to connect, I can use &lt;app name&gt;.wongandre.com to resolve the IP address of my Caddy reverse proxy.</p>
<h1 id="heading-dashy-homepage">dashy homepage</h1>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735441649017/36d25f9b-847e-445b-b50b-19cda95a7b24.png" alt class="image--center mx-auto" /></p>
<p>Here is a homepage of my services, which is grouped into different categories. Monitoring for metrics and logging related apps. User management for internal app identity management. Networking for Adguard DNS, router configs, and proxmox configs. Data management for storing personal data like images, documents, or repositories.</p>
<h1 id="heading-future-plans-2025">future plans 2025</h1>
<p>some plans to improve my home lab I have in mind right now</p>
<ul>
<li><p>self-host my own large language model such as Ollama</p>
</li>
<li><p>create a production Kubernetes environment and migrate/add services to it</p>
</li>
<li><p>Infrastructure as Code</p>
<ul>
<li><p>write scripts to automate provisioning proxmox VMs and LXC through Terraform</p>
</li>
<li><p>write scripts to automate installing configurations per instance through Ansible</p>
</li>
</ul>
</li>
<li><p>Add more storage with direct attached storage (DAS)</p>
</li>
<li><p>Configure backup for my data</p>
</li>
<li><p>add more apps!</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Creating my own storage server]]></title><description><![CDATA[Background
I have been using Google Cloud to store my data such as photos, documents, and emails. I paid about $3 a month for the 100GB plan and recently it is about 80% full.

This prompted me to look for another solution: self-hosting a file server...]]></description><link>https://blog.wongandre.com/creating-my-own-storage-server</link><guid isPermaLink="true">https://blog.wongandre.com/creating-my-own-storage-server</guid><category><![CDATA[Homelab]]></category><category><![CDATA[Nfs]]></category><category><![CDATA[samba]]></category><category><![CDATA[disk]]></category><dc:creator><![CDATA[Andre Wong]]></dc:creator><pubDate>Mon, 18 Nov 2024 14:54:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1731941586648/243f04f6-e333-4d22-81b0-f7f94c5ed44a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-background">Background</h2>
<p>I have been using Google Cloud to store my data such as photos, documents, and emails. I paid about $3 a month for the 100GB plan and recently it is about 80% full.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727502786385/27a0656e-02e9-4b98-a810-5c0bd06a4b62.png" alt class="image--center mx-auto" /></p>
<p>This prompted me to look for another solution: self-hosting a file server that I can access within my network. This will provide me with much more needed space than the 100GB I have now and also will provide external storage for my Proxmox and Kubernetes projects down the road.</p>
<p>One con of a self-hosted file server is that if the data is corrupted, we might not have a guaranteed way to recover it. This means a backup is crucial if the data stored is important (this can be looked into in the future). Other factors to consider would be power and security. Availability is not an issue for me as files may not be accessed that often.</p>
<h2 id="heading-hardware">Hardware</h2>
<p>For the main server, I went with a Beelink Mini S12 Mini PC ($240). Here are some of the specifications:</p>
<ul>
<li><p>Intel Alder Lake N100 Processor</p>
<ul>
<li>it is a low-cost processor with 4 cores and uses very low energy of 6W. Assuming we run it 24 hours in a month of 30 days, we only use about <code>6/1000 × 24 × 30 = 4.32 kWh/month</code>, looking at the price of electricity right now is 32.57 cents/kWh, so theoretically we are paying about <code>$1.407</code> a month to maintain our server. Seems not too bad.</li>
</ul>
</li>
<li><p>16GB DDR4 RAM</p>
<ul>
<li>16GB RAM should be ample RAM for our server. The brand of the ram is not stated.</li>
</ul>
</li>
<li><p>500GB SSD</p>
<ul>
<li>500GB should be enough to run the server’s operating system and host a few applications.</li>
</ul>
</li>
<li><p>Wi-Fi 6 and Bluetooth 5.2</p>
<ul>
<li>I will be using an ethernet cable plugged directly into my router, so Wi-Fi is not that important. Bluetooth can be useful if I want to connect a keyboard to it. Most of the time I will communicate through SSH, so this is not really needed.</li>
</ul>
</li>
</ul>
<p>For my file storage, I got a 2TB Samsung 870 EVO 2.5-inch SSD ($240).</p>
<h2 id="heading-installation">Installation</h2>
<p>I installed an Ubuntu server of version 22.04.5 LTS. Installation instructions can be found on their website.</p>
<p>I then freeze the version of Linux and do an apt update and upgrade</p>
<pre><code class="lang-bash"><span class="hljs-comment"># prevent linux from upgrading itself when it does an upgrade</span>
sudo apt-mark hold $(uname -r)

<span class="hljs-comment"># upgrade packages</span>
sudo apt update
sudo apt list --upgradable
sudo apt upgrade
</code></pre>
<h2 id="heading-static-ip-configuration">Static IP configuration</h2>
<p>By default, the network automatically uses DHCP to get an IP address from the router. I want mine to be fixed <code>192.168.1.10</code> for easy reference. Let’s configure a static IP address for it.</p>
<p>Before changing anything in my <code>/etc/netplan/50-cloud-init.yaml</code> file, the code was:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># This file is generated from information provided by the datasource.  Changes</span>
<span class="hljs-comment"># to it will not persist across an instance reboot.  To disable cloud-init's</span>
<span class="hljs-comment"># network configuration capabilities, write a file</span>
<span class="hljs-comment"># /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:</span>
<span class="hljs-comment"># network: {config: disabled}</span>
network:
    ethernets:
        enp1s0:
            dhcp4: <span class="hljs-literal">true</span>
    version: 2
</code></pre>
<p>After adding the IP address that we want to be static:</p>
<pre><code class="lang-bash">network:
    ethernets:
        enp1s0:
            dhcp4: no
            addresses: [192.168.1.10/24]
            routes:
                - to: default
                  via: 192.168.1.254
            nameservers:
                addresses: [1.1.1.1,8.8.8.8,8.8.8.4]
    version: 2
</code></pre>
<p>dhcp4 is set to <code>no</code>, addresses are set to my static IP <code>192.168.1.10/24</code>, routes indicate the internal gateway IP address which is <code>192.168.1.254</code>. Nameservers are the DNS servers for name resolution, <code>1.1.1.1</code> for Cloudflare DNS and <code>8.8.8.8</code> for Google DNS.</p>
<p>As seen from the comments above, we need to disable the cloud-init by Ubuntu which will override this configuration at every reboot. Add to <code>/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg</code>, <code>network: {config: disabled}</code>.</p>
<p>Run <code>sudo netplan try</code> to apply the changes</p>
<h2 id="heading-bluetooth-configuration">Bluetooth configuration</h2>
<p>Since our device comes with Bluetooth, it would be great if it can pair with my wireless keyboard.</p>
<p>We need to install <code>bluez</code> package</p>
<pre><code class="lang-bash">sudo apt install bluez -y
</code></pre>
<p>Entering the command <code>bluetoothctl</code> enters to a mini program.<code>show</code> shows no default controller available, which means my Bluetooth was not being detected.</p>
<p>Looking at the driver’s message by running: <code>sudo dmesg|egrep -i 'blue|firm'</code>, there is an error <code>bluetooth hci0: Direct firmware load for intel/ibt-0040-1050.sfi failed with error -2</code> which means that the Bluetooth firmware was not being loaded. Turns out it was not included in the <code>/lib/firmware/intel/</code> directory.</p>
<p>To resolve this, we need to download it from elsewhere and do a reboot:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> /lib/firmware/intel/
sudo wget https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git/plain/intel/ibt-0040-1050.sfi
sudo wget https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git/plain/intel/ibt-0040-1050.ddc
reboot
</code></pre>
<p>Entering bluetoothctl, we can <code>scan on</code> to scan for nearby devices to pair. <code>pair &lt;address&gt;</code> to initiate the pair. Then <code>scan off</code> to turn off Bluetooth scanning again.</p>
<p>To make it automatically pair upon turning on device, we install the <code>bluez-tool</code> and run the daemon:</p>
<pre><code class="lang-bash">sudo apt install bluez-tools -y
bt-agent --capability=NoInputNoOutput -d
</code></pre>
<p><code>--capability=NoInputNoOutput</code> will prevent the agent for asking a confirmation for connection.</p>
<h2 id="heading-new-drive-configuration">New drive configuration</h2>
<p>We can take a look at our storage and initialize the SSD we have just added</p>
<pre><code class="lang-bash"><span class="hljs-comment"># looking at the ubuntu storage</span>
sudo fdisk -l /dev/sda
<span class="hljs-comment">#Disk /dev/sda: 476.94 GiB, 512110190592 bytes, 1000215216 sectors</span>
<span class="hljs-comment">#Disk model: 512GB SSD</span>
<span class="hljs-comment">#Units: sectors of 1 * 512 = 512 bytes</span>
<span class="hljs-comment">#Sector size (logical/physical): 512 bytes / 512 bytes</span>
<span class="hljs-comment">#I/O size (minimum/optimal): 512 bytes / 512 bytes</span>
<span class="hljs-comment">#Disklabel type: gpt</span>
<span class="hljs-comment">#Disk identifier: CA61C6D8-AF70-47CD-B813-C8B689C730AC</span>

<span class="hljs-comment">#Device       Start        End   Sectors   Size Type</span>
<span class="hljs-comment">#/dev/sda1     2048    2203647   2201600     1G EFI System</span>
<span class="hljs-comment">#/dev/sda2  2203648    6397951   4194304     2G Linux filesystem</span>
<span class="hljs-comment">#/dev/sda3  6397952 1000212479 993814528 473.9G Linux filesystem</span>

<span class="hljs-comment"># looking at the 2T ssd we added</span>
sudo fdisk -l /dev/sdb
<span class="hljs-comment">#Disk /dev/sdb: 1.82 TiB, 2000398934016 bytes, 3907029168 sectors</span>
<span class="hljs-comment">#Disk model: Samsung SSD 870</span>
<span class="hljs-comment">#Units: sectors of 1 * 512 = 512 bytes</span>
<span class="hljs-comment">#Sector size (logical/physical): 512 bytes / 512 bytes</span>
<span class="hljs-comment">#I/O size (minimum/optimal): 512 bytes / 512 bytes</span>

<span class="hljs-comment"># create a Linux LVM partition using gdisk</span>
sudo gdisk /dev/sdb
n                  <span class="hljs-comment"># new partition</span>
1                  <span class="hljs-comment"># partition number</span>
2048 &lt;enter&gt;       <span class="hljs-comment"># start of sector</span>
3907029134 &lt;enter&gt; <span class="hljs-comment"># end of sector</span>
8e00               <span class="hljs-comment"># code for Linux LVM</span>
<span class="hljs-comment"># result</span>
<span class="hljs-comment"># Number  Start (sector)    End (sector)  Size       Code  Name</span>
<span class="hljs-comment">#  1            2048      3907029134   1.8 TiB     8E00  Linux LVM</span>
w                  <span class="hljs-comment"># write changes</span>
<span class="hljs-comment"># Do you want to proceed? (Y/N): Y</span>
<span class="hljs-comment"># OK; writing new GUID partition table (GPT) to /dev/sdb.</span>
<span class="hljs-comment"># The operation has completed successfully.</span>
</code></pre>
<p>Once done, we can use our newly formatted partition and make it into an LVM physical volume</p>
<pre><code class="lang-bash">sudo pvcreate /dev/sdb1
<span class="hljs-comment"># Physical volume "/dev/sdb1" successfully created.</span>

<span class="hljs-comment"># view our new physical volume</span>
sudo pvdisplay /dev/sdb1
<span class="hljs-comment">#  "/dev/sdb1" is a new physical volume of "&lt;1.82 TiB"</span>
<span class="hljs-comment">#  --- NEW Physical volume ---</span>
<span class="hljs-comment">#  PV Name               /dev/sdb1</span>
<span class="hljs-comment">#  VG Name</span>
<span class="hljs-comment">#  PV Size               &lt;1.82 TiB</span>
<span class="hljs-comment">#  Allocatable           NO</span>
<span class="hljs-comment">#  PE Size               0</span>
<span class="hljs-comment">#  Total PE              0</span>
<span class="hljs-comment">#  Free PE               0</span>
<span class="hljs-comment">#  Allocated PE          0</span>
<span class="hljs-comment">#  PV UUID               ZFZ04d-JmAS-lbgt-yF5a-s56v-vXue-sO2sfD</span>
</code></pre>
<p>Next, we can add our physical volume to our newly created volume group to host a bunch of logical volumes that we will create later.</p>
<pre><code class="lang-bash">sudo vgcreate storage /dev/sdb1
<span class="hljs-comment"># Volume group "storage" successfully created</span>

sudo vgdisplay storage
<span class="hljs-comment">#  --- Volume group ---</span>
<span class="hljs-comment">#  VG Name               storage</span>
<span class="hljs-comment">#  System ID</span>
<span class="hljs-comment">#  Format                lvm2</span>
<span class="hljs-comment">#  Metadata Areas        1</span>
<span class="hljs-comment">#  Metadata Sequence No  1</span>
<span class="hljs-comment">#  VG Access             read/write</span>
<span class="hljs-comment">#  VG Status             resizable</span>
<span class="hljs-comment">#  MAX LV                0</span>
<span class="hljs-comment">#  Cur LV                0</span>
<span class="hljs-comment">#  Open LV               0</span>
<span class="hljs-comment">#  Max PV                0</span>
<span class="hljs-comment">#  Cur PV                1</span>
<span class="hljs-comment">#  Act PV                1</span>
<span class="hljs-comment">#  VG Size               &lt;1.82 TiB</span>
<span class="hljs-comment">#  PE Size               4.00 MiB</span>
<span class="hljs-comment">#  Total PE              476931</span>
<span class="hljs-comment">#  Alloc PE / Size       0 / 0</span>
<span class="hljs-comment">#  Free  PE / Size       476931 / &lt;1.82 TiB</span>
<span class="hljs-comment">#  VG UUID               P6S18y-5mK1-e1eB-c6CS-wiWx-Tkkl-ocmf5T</span>
</code></pre>
<p>Next, we can create a bunch of logical volumes, make the filesystem for each logical volume, and then mount them to our Linux</p>
<pre><code class="lang-bash">sudo lvcreate -n files -L 100G storage
<span class="hljs-comment"># Logical volume "files" created.</span>
sudo lvcreate -n photos -L 100G storage
<span class="hljs-comment"># Logical volume "photos" created.</span>

<span class="hljs-comment"># check our logical volume</span>
ls /dev/mapper/storage*
<span class="hljs-comment"># /dev/mapper/storage-files  /dev/mapper/storage-photos</span>

<span class="hljs-comment"># make filesystem on logical volume</span>
sudo mkfs -t ext4 /dev/mapper/storage-files
sudo mkfs -t ext4 /dev/mapper/storage-photos

<span class="hljs-comment"># make mount directories</span>
sudo mkdir /mnt/files
sudo mkdir /mnt/photos

<span class="hljs-comment"># mount logical volumes</span>
sudo mount /dev/mapper/storage-files /mnt/files/
sudo mount /dev/mapper/storage-photos /mnt/photos/

<span class="hljs-comment"># view result</span>
df -h
<span class="hljs-comment"># Filesystem                         Size  Used Avail Use% Mounted on</span>
<span class="hljs-comment"># /dev/mapper/ubuntu--vg-ubuntu--lv   98G  7.2G   86G   8% /</span>
<span class="hljs-comment"># /dev/sda2                          2.0G  126M  1.7G   7% /boot</span>
<span class="hljs-comment"># /dev/sda1                          1.1G  6.1M  1.1G   1% /boot/efi</span>
<span class="hljs-comment"># /dev/mapper/storage-files           98G   24K   93G   1% /mnt/files</span>
<span class="hljs-comment"># /dev/mapper/storage-photos          98G   24K   93G   1% /mnt/photos</span>
</code></pre>
<p>The good thing about logical volume is we can always add space if we slowly start running out. To resize, we can run <code>lvextend</code> and <code>resize2fs -p</code>.</p>
<p>To make sure our logical volumes are mounted automatically when we boot into our system, update the <code>fstab</code> file as so:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># /etc/fstab: static file system information.</span>
<span class="hljs-comment"># &lt;file system&gt; &lt;mount point&gt;   &lt;type&gt;  &lt;options&gt;       &lt;dump&gt;  &lt;pass&gt;</span>
<span class="hljs-comment"># other mounts ...</span>
<span class="hljs-comment"># my own mounts</span>
/dev/mapper/storage-files /mnt/files ext4 defaults 0 2
/dev/mapper/storage-photos /mnt/photos ext4 defaults 0 2
</code></pre>
<p>The first 2 columns are the file system, and the mount point paths, the 3rd column is the type of file system. The 4th column is additional options that we set to <code>defaults</code>. The 5th column is for the dump command that we will disable. The 6th column sets the order in which the check disk commands run, 0 for no checking, 1 for the root drive, and 2 for other drives.</p>
<h2 id="heading-ssh-configuration">SSH configuration</h2>
<p>To make login easier, we can do password-less authentication during ssh.</p>
<p>On our client's PC to connect, create our public and private RSA key pair via <code>ssh-keygen</code>. Follow the instructions shown by pressing enter.</p>
<p>Then copy the public key over to the server:</p>
<pre><code class="lang-bash">ssh-copy-id -i ~/.ssh/id_rsa.pub andre@192.168.1.10
</code></pre>
<p>Now we can ssh without entering the user password every time.</p>
<h2 id="heading-network-file-server-configuration">Network file server configuration</h2>
<pre><code class="lang-bash">install the required server app and edit configurations related to the nfs server

<span class="hljs-comment"># install the nfs server</span>
apt install nfs-kernel-server

<span class="hljs-comment"># edit /etc/exports</span>
<span class="hljs-comment"># assuming that we only have 1 ip client from 192.168.1.106</span>
/mnt/files 192.168.1.106(rw,sync,no_subtree_check)
/mnt/photos 192.168.1.106(rw,sync,no_subtree_check)

<span class="hljs-comment"># export the changes</span>
sudo exportfs -rav

<span class="hljs-comment"># update nfs-kernel-server settings</span>
vim /etc/default/nfs-kernel-server
<span class="hljs-comment"># change to RPCVCGSSDOPTS="-N 2 -N 3"</span>
<span class="hljs-comment"># this removes version 2 and 3 of nfs and only runs nfs version 4</span>


<span class="hljs-comment"># restart the server</span>
sudo systemctl restart nfs-kernel-server
</code></pre>
<ul>
<li><p>configure the firewall for access</p>
<pre><code class="lang-bash">  sudo ufw allow from 192.168.1.106 to any port 2049

  sudo ufw <span class="hljs-built_in">enable</span>

  <span class="hljs-comment"># check firewall status</span>
  sudo ufw status
  <span class="hljs-comment"># Status: active</span>
  <span class="hljs-comment"># To                         Action      From</span>
  <span class="hljs-comment"># --                         ------      ----</span>
  <span class="hljs-comment"># 22/tcp                     ALLOW       Anywhere</span>
  <span class="hljs-comment"># 2049                       ALLOW       192.168.1.106</span>
  <span class="hljs-comment"># 22/tcp (v6)                ALLOW       Anywhere (v6)</span>
</code></pre>
</li>
<li><p>Ensure that the directory under <code>/mnt/files</code> and <code>/mnt/photos</code> are the ownership of the start user id of <code>1000</code></p>
<pre><code class="lang-bash">  id 1000
  <span class="hljs-comment"># uid=1000(andre) gid=1000(andre) groups=1000(andre),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd)</span>

  <span class="hljs-comment"># update ownership of dir</span>
  sudo chown -R andre:andre /mnt/files
  sudo chown -R andre:andre /mnt/photos
</code></pre>
</li>
<li><p>On the client PC that we want to connect to our nfs, we can install the nfs-common package</p>
<pre><code class="lang-bash">  sudo apt install nfs-common

  <span class="hljs-comment"># try to mount nfs on client machine</span>
  sudo mount -t nfs -vvv 192.168.1.10:/mnt/files /mnt/files

  <span class="hljs-comment"># show mounts</span>
  mount -t nfs4
  <span class="hljs-comment">#192.168.1.10:/mnt/files on /mnt/files type nfs4 (rw,relatime,vers=4.2,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.1.106,local_lock=none,addr=192.168.1.10)</span>

  <span class="hljs-comment"># once mounted we can write to the nfs directory</span>
  <span class="hljs-comment"># should be able to write if client user id is also 1000</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">"hello world"</span> &gt; /mnt/files/testfile
</code></pre>
</li>
<li><p>To automatically mount the NFS every time the client machine starts up, we can update the <code>/etc/fstab</code> file</p>
<pre><code class="lang-bash">  <span class="hljs-comment"># /etc/fstab</span>
  192.168.1.10:/mnt/files /mnt/files nfs <span class="hljs-built_in">bg</span>,rsize=8192,wsize=8192 0 0
</code></pre>
<p>  Where <code>bg</code> is an option to run the mount in the background later if the mount fails. <code>rsize</code> and <code>wsize</code> sets the buffer size for reading and writing, by default it is <code>1024</code>.</p>
</li>
</ul>
<h2 id="heading-samba-file-server-configuration">Samba file server configuration</h2>
<ul>
<li><p>create a new logical volume for samba and mount directory</p>
<pre><code class="lang-bash">  <span class="hljs-comment"># new logical volume</span>
  sudo lvcreate -n sambashare -L 200G storage
  sudo mkfs -t ext4 /dev/mapper/storage-sambashare

  <span class="hljs-comment"># new mount dir</span>
  sudo mkdir /mnt/sambashare
  sudo chown -R andre:andre /mnt/sambashare

  <span class="hljs-comment"># mount or add to /etc/fstab</span>
  sudo mount /dev/mapper/storage-sambashare /mnt/sambashare
</code></pre>
</li>
<li><p>install the required server apps and edit configurations</p>
<pre><code class="lang-bash">  sudo apt install samba -y

  <span class="hljs-comment"># edit config</span>
  sudo vim /etc/samba/smb.conf
  <span class="hljs-comment"># optionally comment out printers under [printers] and [print$]</span>
  <span class="hljs-comment"># add the lines below</span>
  <span class="hljs-comment">#[sambashare]</span>
  <span class="hljs-comment">#  comment = Samba on Ubuntu</span>
  <span class="hljs-comment">#  path = /mnt/sambashare</span>
  <span class="hljs-comment">#  read only = no</span>
  <span class="hljs-comment">#  browsable = yes</span>

  <span class="hljs-comment"># start smb service</span>
  sudo systemctl start smbd.service
</code></pre>
</li>
<li><p>now we can connect via Windows by providing the IP address of our Samba server to the file explorer</p>
</li>
<li><p>To mount samba via Linux as follows:</p>
<pre><code class="lang-bash">  sudo mount -t cifs -o user=andre //192.168.1.10/sambashare /mnt/<span class="hljs-built_in">test</span>
</code></pre>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>We have set up a mini pc capable of sharing storage through NFS and Samba. We can then access these storages via other Linux system via the NFS clients or on Windows samba share client.</p>
]]></content:encoded></item><item><title><![CDATA[Using Gitea as a private image repository]]></title><description><![CDATA[Previously we have written a simple API server that interacts with MongoDB, redis, and influxDB. We also managed to run our deployments on our Kubernetes cluster with the image pulled from docker.
Now let's try to create our own private image reposit...]]></description><link>https://blog.wongandre.com/using-gitea-as-a-private-image-repository</link><guid isPermaLink="true">https://blog.wongandre.com/using-gitea-as-a-private-image-repository</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[gitea]]></category><category><![CDATA[Docker]]></category><dc:creator><![CDATA[Andre Wong]]></dc:creator><pubDate>Sat, 05 Oct 2024 15:37:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1728142572164/b1b2d4aa-734d-4355-b4f1-eb479db1f517.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Previously we have written a simple API server that interacts with MongoDB, redis, and influxDB. We also managed to run our deployments on our Kubernetes cluster with the image pulled from docker.</p>
<p>Now let's try to create our own private image repository. The Gitea application we set up before has a container registry that is compliant with the <a target="_blank" href="https://opencontainers.org/">Open Container Initiative</a>. As such, we can push and pull docker images to our Gitea.</p>
<p>Prerequisites: we need to have SSL enabled on our Gitea application, check here to find out more on how to enable it.</p>
<h2 id="heading-pushing-to-a-private-repository">Pushing to a private repository</h2>
<ul>
<li><p>My main PC is using Docker Desktop with WSL2. To push our image successfully, we must enable insecure-registries in the Docker desktop configuration.</p>
<p>  Under Settings -&gt; Docker Engine, add the following:</p>
<pre><code class="lang-json">  {
    <span class="hljs-attr">"builder"</span>: {
      <span class="hljs-attr">"gc"</span>: {
        <span class="hljs-attr">"defaultKeepStorage"</span>: <span class="hljs-string">"20GB"</span>,
        <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>
      }
    },
    <span class="hljs-attr">"experimental"</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">"insecure-registries"</span>: [
      <span class="hljs-string">"192.168.1.4:3000"</span>
    ]
  }
</code></pre>
<p>  <code>192.168.1.4:3000</code> is the URL of our Gitea application. This can be a simple domain name such as <code>gitea.local</code> if the DNS record is added into the DNS server (e.g. on PiHole).</p>
</li>
<li><p>Perform a login to save credentials, and enter the username and password used for Gitea login. Here I have an account name called <code>andre</code>.</p>
<pre><code class="lang-bash">  docker login 192.168.1.4:3000 --username andre --password *******
</code></pre>
</li>
<li><p>Next, we need to tag the docker image before pushing. According to Gitea documentation:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708794690587/3b3e1a11-b42c-4c4a-ac6a-e622f606c723.png" alt class="image--center mx-auto" /></p>
<p>  We need to first tag our image according to the above format</p>
<pre><code class="lang-bash">  <span class="hljs-comment"># tag with an image ID</span>
  docker tag 9b0f43d3c161 192.168.1.4:3000/andre/api-server:1.0.1

  <span class="hljs-comment"># tag when building image</span>
  docker build -t 192.168.1.4:3000/andre/api-server:1.0.1 .
</code></pre>
</li>
<li><p>Now we can push to Gitea as shown below</p>
<pre><code class="lang-bash">  <span class="hljs-comment"># push to gitea</span>
  docker push 192.168.1.4:3000/andre/api-server:1.0.1

  <span class="hljs-comment"># to view all the list of images you have pushed</span>
  curl -ik --user andre https://192.168.1.4:3000/v2/_catalog
  <span class="hljs-comment"># output</span>
  <span class="hljs-comment"># {"repositories":["andre/api-server"]}</span>
</code></pre>
<pre><code class="lang-bash">  <span class="hljs-comment"># to view all the tags of image</span>
  curl -ik --user andre https://192.168.1.4:3000/v2/andre/api-server/tags/list
  <span class="hljs-comment"># output</span>
  <span class="hljs-comment"># {"name":"andre/api-server","tags":["1.0.1"]}</span>
</code></pre>
</li>
<li><p><a target="_blank" href="https://specs.opencontainers.org/distribution-spec/?v=v1.0.0#content-discovery">Here is a list of APIs</a> we can use to view the images we have pushed</p>
</li>
</ul>
<h2 id="heading-pulling-from-a-private-repository">Pulling from a private repository</h2>
<p>we are following their <a target="_blank" href="https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/">guide here</a></p>
<p>we first create our <code>auth</code> value which comprises of &lt;username&gt;:&lt;password&gt; which is then baseb4 encoded. We use <code>-n</code> to prevent echo from adding extra newlines.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> -n <span class="hljs-string">"andre:12345678"</span> | base64
<span class="hljs-comment"># output</span>
<span class="hljs-comment"># YW5kcmU6MTIzNDU2Nzg=</span>
</code></pre>
<p>Next, we can create our <code>dockerconfig.json</code> to store our credentials secrets. The <code>"192.168.1.4:3000"</code> key is the IP and port where our image repository is located, we provide the credential info such as <code>username</code>, <code>password</code>, <code>email</code> and <code>auth</code>, which we had just encoded earlier into this JSON file.</p>
<pre><code class="lang-json"><span class="hljs-comment">// dockerconfig.json</span>
{
    <span class="hljs-attr">"auths"</span>: {
        <span class="hljs-attr">"192.168.1.4:3000"</span>: {
            <span class="hljs-attr">"username"</span>: <span class="hljs-string">"andre"</span>,
            <span class="hljs-attr">"password"</span>: <span class="hljs-string">"12345678"</span>,
            <span class="hljs-attr">"email"</span>: <span class="hljs-string">"andre@mail.com"</span>,
            <span class="hljs-attr">"auth"</span>: <span class="hljs-string">"YW5kcmU6MTIzNDU2Nzg="</span>
        }
    }
}
</code></pre>
<p>Now we can parse this JSON file into base64 again.</p>
<pre><code class="lang-bash">base64 dockerconfig.json
<span class="hljs-comment"># output</span>
<span class="hljs-comment"># eyJhdXRocyI6eyIxOTIuMTY4LjEuNDozMDAwIjp7InVzZXJuYW1lIjoiYW5kcmUiLCJwYXNzd29yZCI6IjEyMzQ1Njc4IiwiZW1haWwiOiJhbmRyZUBtYWlsLmNvbSIsImF1dGgiOiJZVzVrY21VNk1USXpORFUyTnpnPSJ9fX0=</span>
</code></pre>
<p>using this output value, we can then create a <code>image-pull-secret.yaml</code> file and store this value under <code>data.\.dockerconfigjson</code>. The type of secret here is <code>kubernetes.io/dockerconfigjson</code>, which is used to authenticate with a container registry when pulling a private image.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># image-pull-secret.yaml</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Secret</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span>  <span class="hljs-string">image-pull-secret</span>
<span class="hljs-attr">data:</span>
  <span class="hljs-string">.dockerconfigjson:</span>  <span class="hljs-string">eyJhdXRocyI6eyIxOTIuMTY4LjEuNDozMDAwIjp7InVzZXJuYW1lIjoiYW5kcmUiLCJwYXNzd29yZCI6IjEyMzQ1Njc4IiwiZW1haWwiOiJhbmRyZUBtYWlsLmNvbSIsImF1dGgiOiJZVzVrY21VNk1USXpORFUyTnpnPSJ9fX0=</span> <span class="hljs-comment"># dockerconfigjson is a base64 encoded string:  cat ~/.docker/config.json | base64 -w 0 </span>
<span class="hljs-attr">type:</span> <span class="hljs-string">kubernetes.io/dockerconfigjson</span>
</code></pre>
<p>Now we just need to include this secret in our <code>api-server.yaml</code> so that it knows to use this to authenticate our Gitea application</p>
<p>We can add <code>imagePullSecrets</code> it under the deployment <code>spec.template.spec</code>.</p>
<p>Change the <code>spec.template.spec.containers.image</code> to the one pointing to your private image repository as well</p>
<p><code>image: 192.168.1.4:3000/andre/api-server:1.0.1</code></p>
<pre><code class="lang-bash"><span class="hljs-comment"># api-server.yaml</span>
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server-deployment
  annotations:
    description: <span class="hljs-string">"this is my normal backend server deployment"</span>
    version: <span class="hljs-string">"1.0"</span>
spec:
  replicas: 1
  selector:
    matchLabels:
      app: api-server
  template:
    metadata:
      labels:
        app: api-server
    spec:
      containers:
        - name: api-server-container
          image: 192.168.1.4:3000/andre/api-server:1.0.1 <span class="hljs-comment"># update the link to image registry</span>
          resources: <span class="hljs-comment"># declare resources limits and request</span>
            limits:
              memory: <span class="hljs-string">"512Mi"</span>
              cpu: <span class="hljs-string">"1"</span>
            requests:
              memory: <span class="hljs-string">"256Mi"</span>
              cpu: <span class="hljs-string">"0.2"</span>
          ports: <span class="hljs-comment"># container ports exposed</span>
            - containerPort: 8080
          envFrom:
            - configMapRef:
                name: homek8-configmap
            - secretRef:
                name: homek8-secret
      imagePullSecrets:
        - name: image-pull-secret
</code></pre>
<h2 id="heading-making-deployments-to-k8s">Making deployments to k8s</h2>
<p>Now we are ready to deploy to Kubernetes.</p>
<p>First, we need to ensure that we added our Gitea SSL certificate to our Kubernetes nodes. To do that, copy the <code>giteaCA.crt</code> over to our k8 nodes then update the ca-certificate:</p>
<pre><code class="lang-bash">sudo cp giteaCA.crt /usr/<span class="hljs-built_in">local</span>/share/ca-certificates/
sudo update-ca-certificates
</code></pre>
<p>Next, we can create a namespace for deployment:</p>
<pre><code class="lang-bash">kubectl create namespace homek8-custom-registry
kubectl config set-context --current --namespace=homek8-custom-registry
</code></pre>
<p>Lastly, deploy</p>
<pre><code class="lang-bash">kubectl apply -f .
</code></pre>
<p>Now we can see the container being successfully pulled and created.</p>
]]></content:encoded></item><item><title><![CDATA[Simple Kubernetes deployment]]></title><description><![CDATA[Prerequisites:

have a running Kubernetes cluster. Check out here to create one yourself

have a copy of our backend server code and image. Check out here to create


We will deploy 1 backend-server image, 1 MongoDB image, 1 Redis image, and 1 Influx...]]></description><link>https://blog.wongandre.com/simple-kubernetes-deployment</link><guid isPermaLink="true">https://blog.wongandre.com/simple-kubernetes-deployment</guid><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Andre Wong]]></dc:creator><pubDate>Sun, 22 Sep 2024 09:19:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726996637372/3e574e67-f74a-4b3c-9bf9-999b567fd1c5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Prerequisites:</p>
<ol>
<li><p>have a running Kubernetes cluster. Check out <a target="_blank" href="https://blog.wongandre.com/installing-kubernetes-on-proxmox-vm">here</a> to create one yourself</p>
</li>
<li><p>have a copy of our backend server code and image. Check out <a target="_blank" href="https://blog.wongandre.com/create-a-simple-api-server-image-for-docker-and-kubernetes">here</a> to create</p>
</li>
</ol>
<p>We will deploy 1 backend-server image, 1 MongoDB image, 1 Redis image, and 1 InfluxDB image to our Kubernetes cluster.</p>
<h2 id="heading-writing-our-yaml-files">Writing our Yaml files</h2>
<p>Let's first set our 3 databases.</p>
<ol>
<li><p>Mongo</p>
<pre><code class="lang-yaml"> <span class="hljs-comment"># mongo.yaml</span>
 <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
 <span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
 <span class="hljs-attr">metadata:</span>
   <span class="hljs-attr">name:</span> <span class="hljs-string">mongo-deployment</span>
 <span class="hljs-attr">spec:</span>
   <span class="hljs-attr">replicas:</span> <span class="hljs-number">1</span>
   <span class="hljs-attr">selector:</span>
     <span class="hljs-attr">matchLabels:</span>
       <span class="hljs-attr">app:</span> <span class="hljs-string">mongo</span>
   <span class="hljs-attr">template:</span>
     <span class="hljs-attr">metadata:</span>
       <span class="hljs-attr">labels:</span>
         <span class="hljs-attr">app:</span> <span class="hljs-string">mongo</span>
     <span class="hljs-attr">spec:</span>
       <span class="hljs-attr">containers:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">mongo</span>
         <span class="hljs-attr">image:</span> <span class="hljs-string">mongo:4.4.18</span>
         <span class="hljs-attr">resources:</span>
           <span class="hljs-attr">limits:</span>
             <span class="hljs-attr">memory:</span> <span class="hljs-string">"128Mi"</span>
             <span class="hljs-attr">cpu:</span> <span class="hljs-string">"500m"</span>
         <span class="hljs-attr">ports:</span>
         <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">27017</span>
 <span class="hljs-string">---</span>
</code></pre>
<p> Let's go through what we had written.</p>
<ul>
<li><p><code>apiVersion</code> is specified as <code>apps/v1</code>, this is the API version of the Kubernetes object being deployed</p>
</li>
<li><p><code>kind</code> is <code>Deployment</code></p>
</li>
<li><p><code>metadata.name</code> is <code>mongo-deployment</code>. This is the name of our deployment. Metadata can contain descriptions of our deployments</p>
</li>
<li><p><code>spec</code> describes the state of our deployment.</p>
<ul>
<li><p><code>replicas</code> sets how many instances of pods we should be running, sets to <code>1</code> here</p>
</li>
<li><p><code>selector.matchLabels</code> specifies which pods will be managed by the deployment based on the labels. This value here must match the pod label that we will be creating. Here we set to match the label <code>mongo</code></p>
</li>
<li><p><code>template</code> defines the pod used by the deployment to create new pods</p>
<ul>
<li><p><code>metadata.labels</code> is used to label our pod, which we had labeled as <code>mongo</code>, this will be referenced by our deployment and service resource</p>
</li>
<li><p><code>spec</code> specify details of our containers, such as the <code>name</code>, <code>image</code>, <code>resources</code> and <code>ports</code></p>
<ul>
<li><p><code>name</code> specifies the name of our container</p>
</li>
<li><p><code>ports</code> specifies the ports that the container exposes</p>
<p>  we want to expose <code>27017</code> to other services so we define ports as such:</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">ports:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">27017</span>
</code></pre>
</li>
<li><p><code>resources</code> specifies the requests and limits for the container. we want to limit the CPU and memory of our container, as such we define our resources as such:</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">resources:</span>
    <span class="hljs-attr">limits:</span>
      <span class="hljs-attr">memory:</span> <span class="hljs-string">"128Mi"</span>
      <span class="hljs-attr">cpu:</span> <span class="hljs-string">"500m"</span>
</code></pre>
</li>
<li><p><code>image</code> specifies the docker image to use for the container</p>
<p>  We will be using the docker image from the docker hub and the image can be defined as &lt;image_name&gt;:&lt;tag&gt;, we are using version 4.4.18 here. (Mongo version 5+ might not run on Kubernetes so we are using an older version, check out <a target="_blank" href="https://github.com/docker-library/mongo/issues/485">this issue</a>)</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ol>
<p>    Next, we can define the service spec</p>
<pre><code class="lang-yaml">    <span class="hljs-comment"># mongo.yaml</span>
    <span class="hljs-comment"># deployment written earlier</span>
    <span class="hljs-string">---</span>
    <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
    <span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">name:</span>  <span class="hljs-string">mongo-service</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">selector:</span>
        <span class="hljs-attr">app:</span>  <span class="hljs-string">mongo</span>
      <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">protocol:</span> <span class="hljs-string">TCP</span>
        <span class="hljs-attr">port:</span>  <span class="hljs-number">27017</span>
        <span class="hljs-attr">targetPort:</span>  <span class="hljs-number">27017</span>
</code></pre>
<p>    Let's go through what we had written.</p>
<ul>
<li><p><code>apiVersion</code> specifies the API version for the service</p>
</li>
<li><p><code>kind</code> is a <code>Service</code>, a Service is a Kubernetes resource that allows a way for pods to be accessed.</p>
</li>
<li><p><code>metadata.name</code> specifies the name of our service as <code>mongo-service</code></p>
</li>
<li><p><code>spec</code> describes the state of our service</p>
<ul>
<li><p><code>selector</code> specifies the set of pods that follow this service configuration, mainly through the labels. We set this value to be the same label set earlier, <code>mongo</code></p>
</li>
<li><p><code>ports</code> specifies the ports exposed by the service</p>
<ul>
<li><p><code>protocol</code> is the network protocol used, we use <code>tcp</code> here</p>
</li>
<li><p><code>port</code> is the port number we want to be exposed to, <code>27017</code></p>
</li>
<li><p><code>targetPort</code> specifies the port on the pod targeted by the service</p>
</li>
<li><p>The service maintains its range of ports used (port) and this is mapped to the port used on the pod (targetPort)</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<ol start="2">
<li><p>Redis</p>
<p> This is similar to Mongo deployment, the only difference is Redis port we will use <code>6379</code></p>
<pre><code class="lang-yaml"> <span class="hljs-comment"># redis.yaml</span>
 <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
 <span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
 <span class="hljs-attr">metadata:</span>
   <span class="hljs-attr">name:</span> <span class="hljs-string">redis-deployment</span>
 <span class="hljs-attr">spec:</span>
   <span class="hljs-attr">replicas:</span> <span class="hljs-number">1</span>
   <span class="hljs-attr">selector:</span>
     <span class="hljs-attr">matchLabels:</span>
       <span class="hljs-attr">app:</span> <span class="hljs-string">redis</span>
   <span class="hljs-attr">template:</span>
     <span class="hljs-attr">metadata:</span>
       <span class="hljs-attr">labels:</span>
         <span class="hljs-attr">app:</span> <span class="hljs-string">redis</span>
     <span class="hljs-attr">spec:</span>
       <span class="hljs-attr">containers:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">redis</span>
         <span class="hljs-attr">image:</span> <span class="hljs-string">redis:alpine</span>
         <span class="hljs-attr">resources:</span>
           <span class="hljs-attr">limits:</span>
             <span class="hljs-attr">memory:</span> <span class="hljs-string">"128Mi"</span>
             <span class="hljs-attr">cpu:</span> <span class="hljs-string">"500m"</span>
         <span class="hljs-attr">ports:</span>
         <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">6379</span>
 <span class="hljs-string">---</span>
 <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
 <span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
 <span class="hljs-attr">metadata:</span>
   <span class="hljs-attr">name:</span>  <span class="hljs-string">redis-service</span>
 <span class="hljs-attr">spec:</span>
   <span class="hljs-attr">selector:</span>
     <span class="hljs-attr">app:</span>  <span class="hljs-string">redis</span>
   <span class="hljs-attr">ports:</span>
   <span class="hljs-bullet">-</span> <span class="hljs-attr">protocol:</span> <span class="hljs-string">TCP</span>
     <span class="hljs-attr">port:</span>  <span class="hljs-number">6379</span>
     <span class="hljs-attr">targetPort:</span>  <span class="hljs-number">6379</span>
</code></pre>
</li>
<li><p>InfluxDB</p>
<p> For InfluxDB, other than changing the port to <code>8086</code>, it also has a few environmental variables that need to be set for the containers. InfluxDB also exposes a web UI that we can access so we will need to expose this to the network outside our Kubernetes.</p>
<pre><code class="lang-yaml"> <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
 <span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
 <span class="hljs-attr">metadata:</span>
   <span class="hljs-attr">name:</span> <span class="hljs-string">influx-deployment</span>
 <span class="hljs-attr">spec:</span>
   <span class="hljs-attr">replicas:</span> <span class="hljs-number">1</span>
   <span class="hljs-attr">selector:</span>
     <span class="hljs-attr">matchLabels:</span>
       <span class="hljs-attr">app:</span> <span class="hljs-string">influx</span>
   <span class="hljs-attr">template:</span>
     <span class="hljs-attr">metadata:</span>
       <span class="hljs-attr">labels:</span>
         <span class="hljs-attr">app:</span> <span class="hljs-string">influx</span>
     <span class="hljs-attr">spec:</span>
       <span class="hljs-attr">containers:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">influx</span>
         <span class="hljs-attr">image:</span> <span class="hljs-string">influxdb:alpine</span>
         <span class="hljs-attr">resources:</span>
           <span class="hljs-attr">limits:</span>
             <span class="hljs-attr">memory:</span> <span class="hljs-string">"128Mi"</span>
             <span class="hljs-attr">cpu:</span> <span class="hljs-string">"500m"</span>
         <span class="hljs-attr">ports:</span>
         <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">8086</span>
         <span class="hljs-attr">env:</span>
           <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_MODE</span>
             <span class="hljs-attr">value:</span> <span class="hljs-string">"setup"</span>
           <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_USERNAME</span>
             <span class="hljs-attr">value:</span> <span class="hljs-string">"andre"</span>
           <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_PASSWORD</span>
             <span class="hljs-attr">value:</span> <span class="hljs-string">"12345678"</span>
           <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_ORG</span>
             <span class="hljs-attr">value:</span> <span class="hljs-string">"andre"</span>
           <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_BUCKET</span>
             <span class="hljs-attr">value:</span> <span class="hljs-string">"bucket1"</span>
           <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_ADMIN_TOKEN</span>
             <span class="hljs-attr">value:</span> <span class="hljs-string">"my-token"</span>
 <span class="hljs-string">---</span>
 <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
 <span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
 <span class="hljs-attr">metadata:</span>
   <span class="hljs-attr">name:</span>  <span class="hljs-string">influx-service</span>
 <span class="hljs-attr">spec:</span>
   <span class="hljs-attr">type:</span> <span class="hljs-string">NodePort</span>
   <span class="hljs-attr">selector:</span>
     <span class="hljs-attr">app:</span>  <span class="hljs-string">influx</span>
   <span class="hljs-attr">ports:</span>
   <span class="hljs-bullet">-</span> <span class="hljs-attr">protocol:</span> <span class="hljs-string">TCP</span>
     <span class="hljs-attr">port:</span>  <span class="hljs-number">8086</span>
     <span class="hljs-attr">targetPort:</span>  <span class="hljs-number">8086</span>
     <span class="hljs-attr">nodePort:</span> <span class="hljs-number">32000</span>
</code></pre>
<p> Explanations:</p>
<ul>
<li><p><code>spec.template.spec.env</code> specifies our environmental variables for the containers. We have written a few that are required by the InfluxDB image, for example, the password and the admin token. We will take this out later and put it under Secrets.</p>
</li>
<li><p><code>spec.type</code> under Service is set to <code>NodePort</code>. <code>NodePort</code> here specifies the port number allocated on every node of the cluster. This is used to access the service externally, mainly to access our web UI</p>
</li>
</ul>
</li>
<li><p>API server</p>
<p> Now we can write our deployment for api-server. We will be using port <code>8080</code> for our container and port <code>80</code> for our service. This maps port <code>80</code> of the service to port <code>8080</code> of the container.</p>
<p> We also declared the <code>ENV</code> and <code>NAMESPACE</code> environmental variables for this container to be used in our code later.</p>
<p> Our image for the api-server is using the one we had previously uploaded to the docker hub, <code>andrewongzh/api-server:1.0.0</code>. This is in the format of &lt;username&gt;/&lt;repo_name&gt;:&lt;tag_name&gt;</p>
<pre><code class="lang-yaml"> <span class="hljs-comment"># api-server.yaml</span>
 <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
 <span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
 <span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">api-server-service</span>
 <span class="hljs-attr">spec:</span>
   <span class="hljs-attr">type:</span> <span class="hljs-string">NodePort</span>
   <span class="hljs-attr">selector:</span>
     <span class="hljs-attr">app:</span> <span class="hljs-string">api-server</span>
   <span class="hljs-attr">ports:</span>
     <span class="hljs-bullet">-</span> <span class="hljs-attr">protocol:</span> <span class="hljs-string">TCP</span>
       <span class="hljs-attr">port:</span> <span class="hljs-number">80</span> 
       <span class="hljs-attr">targetPort:</span> <span class="hljs-number">8080</span> <span class="hljs-comment"># the port number which the service will forward to</span>
       <span class="hljs-attr">nodePort:</span> <span class="hljs-number">32001</span> <span class="hljs-comment"># this port is exposed externally outside of k8s</span>
 <span class="hljs-string">---</span>
 <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
 <span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
 <span class="hljs-attr">metadata:</span>
   <span class="hljs-attr">name:</span> <span class="hljs-string">api-server-deployment</span>
   <span class="hljs-attr">annotations:</span>
     <span class="hljs-attr">description:</span> <span class="hljs-string">"this is my normal backend server deployment"</span>
     <span class="hljs-attr">version:</span> <span class="hljs-string">"1.0"</span>
 <span class="hljs-attr">spec:</span>
   <span class="hljs-attr">replicas:</span> <span class="hljs-number">1</span>
   <span class="hljs-attr">selector:</span>
     <span class="hljs-attr">matchLabels:</span>
       <span class="hljs-attr">app:</span> <span class="hljs-string">api-server</span>
   <span class="hljs-attr">template:</span>
     <span class="hljs-attr">metadata:</span>
       <span class="hljs-attr">labels:</span>
         <span class="hljs-attr">app:</span> <span class="hljs-string">api-server</span>
     <span class="hljs-attr">spec:</span>
       <span class="hljs-attr">containers:</span>
         <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">api-server-container</span>
           <span class="hljs-attr">image:</span> <span class="hljs-string">andrewongzh/api-server:1.0.1</span> <span class="hljs-comment"># update the link to image registary</span>
           <span class="hljs-attr">resources:</span> <span class="hljs-comment"># declare resources limits and request</span>
             <span class="hljs-attr">limits:</span>
               <span class="hljs-attr">memory:</span> <span class="hljs-string">"512Mi"</span>
               <span class="hljs-attr">cpu:</span> <span class="hljs-string">"1"</span>
             <span class="hljs-attr">requests:</span>
               <span class="hljs-attr">memory:</span> <span class="hljs-string">"256Mi"</span>
               <span class="hljs-attr">cpu:</span> <span class="hljs-string">"0.2"</span>
           <span class="hljs-attr">ports:</span> <span class="hljs-comment"># container ports exposed</span>
             <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">8080</span>
           <span class="hljs-attr">env:</span>
             <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">TEST</span>
               <span class="hljs-attr">value:</span> <span class="hljs-string">"1-2-3"</span>
             <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">ENV</span>
               <span class="hljs-attr">value:</span> <span class="hljs-string">"k8"</span>
             <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">NAMESPACE</span>
               <span class="hljs-attr">value:</span> <span class="hljs-string">"homek8"</span>
             <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_ADMIN_TOKEN</span>
               <span class="hljs-attr">value:</span> <span class="hljs-string">"my-token"</span>
</code></pre>
</li>
</ol>
<h2 id="heading-updating-api-server-code">Updating API-server code</h2>
<p>Using the same <code>ENV</code> key we defined before, we can add an if else statement to check <code>ENV == "k8"</code> for Kubernetes deployment. If so, we can then change the URI of our client DB connection and replace it with the format of &lt;URI_scheme&gt;://&lt;service-name&gt;.&lt;namespace&gt;.svc.cluster.local:&lt;port&gt;. <code>service-name</code> is what we defined in our Service yaml earlier, which will resolve to the IP address via internal DNS resolution. <code>namespace</code> is the namespace we decided to deploy on which is <code>homek8</code>.</p>
<pre><code class="lang-go"><span class="hljs-keyword">var</span> ENV = os.Getenv(<span class="hljs-string">"ENV"</span>)
<span class="hljs-keyword">var</span> nameSpace = os.Getenv(<span class="hljs-string">"NAMESPACE"</span>)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">MongoClientInit</span><span class="hljs-params">()</span> *<span class="hljs-title">mongo</span>.<span class="hljs-title">Client</span></span> {
    <span class="hljs-keyword">defer</span> cancel()
    address := <span class="hljs-string">"mongodb://localhost:27017"</span>
    <span class="hljs-keyword">if</span> ENV == <span class="hljs-string">"dockercompose"</span> {
        address = <span class="hljs-string">"mongodb://mongo:27017"</span>
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> ENV == <span class="hljs-string">"k8"</span> {
        <span class="hljs-comment">// hard coded service name here</span>
        address = fmt.Sprintf(<span class="hljs-string">"mongodb://mongo-service.%s.svc.cluster.local:27017"</span>, nameSpace)
    }
    mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(address))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(err)
    }
    <span class="hljs-keyword">if</span> err := mongoClient.Ping(ctx, <span class="hljs-literal">nil</span>); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(err)
    }

    fmt.Println(<span class="hljs-string">"Connect to mongodb OK"</span>)
    <span class="hljs-keyword">return</span> mongoClient
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">RedisClientInit</span><span class="hljs-params">()</span> *<span class="hljs-title">redis</span>.<span class="hljs-title">Client</span></span> {
    address := <span class="hljs-string">"localhost:6379"</span>
    <span class="hljs-keyword">if</span> ENV == <span class="hljs-string">"dockercompose"</span> {
        address = <span class="hljs-string">"redis:6379"</span>
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> ENV == <span class="hljs-string">"k8"</span> {
        <span class="hljs-comment">// hard coded service name here</span>
        address = fmt.Sprintf(<span class="hljs-string">"redis-service.%s.svc.cluster.local:6379"</span>, nameSpace)
    }
    rdb := redis.NewClient(&amp;redis.Options{
        Addr:     address,
        Password: <span class="hljs-string">""</span>,
        DB:       <span class="hljs-number">0</span>,
    })

    <span class="hljs-keyword">if</span> err := rdb.Ping(redisCtx); err.Err() != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(err)
    }

    fmt.Println(<span class="hljs-string">"Connect to redis OK"</span>)
    <span class="hljs-keyword">return</span> rdb
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">InfluxClientInit</span><span class="hljs-params">()</span> *<span class="hljs-title">influxdb2</span>.<span class="hljs-title">Client</span></span> {
    address := <span class="hljs-string">"http://localhost:8086"</span>
    <span class="hljs-keyword">if</span> ENV == <span class="hljs-string">"dockercompose"</span> {
        address = <span class="hljs-string">"http://influx:8086"</span>
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> ENV == <span class="hljs-string">"k8"</span> {
        <span class="hljs-comment">// hard coded service name here</span>
        address = fmt.Sprintf(<span class="hljs-string">"http://influx-service.%s.svc.cluster.local:8086"</span>, nameSpace)
    }
    ifxdb := influxdb2.NewClient(address, influxToken)
    ok, err := ifxdb.Ping(influxCtx)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(err)
    }
    <span class="hljs-keyword">if</span> !ok {
        <span class="hljs-built_in">panic</span>(<span class="hljs-string">"Unable to connect to influxdb"</span>)
    }
    fmt.Println(<span class="hljs-string">"Connect to influx OK"</span>)
    <span class="hljs-keyword">return</span> &amp;ifxdb
}
</code></pre>
<p>Let's rebuild the image and push it to docker hub tagged as <code>1.0.1</code></p>
<pre><code class="lang-bash">docker build . -t andrewongzh/api-server:1.0.1
docker push andrewongzh/api-server:1.0.1
</code></pre>
<h2 id="heading-deploying-to-kubernetes">Deploying to Kubernetes</h2>
<p>We will ssh into our machine and use the kubectl command to execute our deployments.</p>
<p>First, we can create a namespace called <code>homek8</code> for this deployment. A namespace allows us to separate our deployments from the rest of the deployments in Kubernetes clusters. This is useful if we want to isolate resources via different users or projects.</p>
<pre><code class="lang-bash">kubectl create namespace homek8
</code></pre>
<p>Next, we can start deploying</p>
<pre><code class="lang-bash"><span class="hljs-comment"># -f referes to the yaml file we want to use</span>
kubectl apply -f influx.yaml --namespace=homek8
kubectl apply -f mongo.yaml -n homek8
kubectl apply -f redis.yaml -n homek8
kubectl apply -f api-server.yaml -n homek8
</code></pre>
<p>We can verify if our deployments are successful</p>
<pre><code class="lang-bash">kubectl get pods -n homek8
<span class="hljs-comment"># output</span>
NAME                                 READY   STATUS    RESTARTS   AGE
influx-deployment-5c875ffdbc-rmh9w   1/1     Running   0          28s
mongo-deployment-67b6bbf7fd-66thz    1/1     Running   0          10s
redis-deployment-f76b8d78c-n7r4d     1/1     Running   0          5s
</code></pre>
<p>When we create a deployment, it creates a replica set and names it as &lt;deployment_name&gt;-&lt;random-string&gt;. In the case of our influxDB deployment, the replica set name is <code>influx-deployment-5c875ffdbc</code>. The replica set then creates the pods which will add another random string to the back which is <code>rmh9w</code>.</p>
<p>We can also try to access our influxDB web UI via <code>http://192.168.1.95:32000/</code>, where the IP is any of our nodes and the port is previously defined under <code>nodePort</code> , which is <code>32000</code>.</p>
<p>To test our API server, we can make a simple curl command to it</p>
<pre><code class="lang-bash">curl http://192.168.1.95:32001
<span class="hljs-comment"># output</span>
{<span class="hljs-string">"hello"</span>:<span class="hljs-string">"world"</span>}
</code></pre>
<p>Let's change our current context to always use the home8 namespace so we don't have to keep typing it</p>
<pre><code class="lang-bash">kubectl config set-context --current --namespace=homek8
</code></pre>
<p>Let's look at our api-server's log, we can either dump out the logs or stream it</p>
<pre><code class="lang-bash"><span class="hljs-comment"># get logs via pod name</span>
<span class="hljs-comment"># -f to stream the logs</span>
kubectl logs -f api-server-deployment-77fb9b8bcc-sx6c8

<span class="hljs-comment"># get logs via deployment name</span>
kubectl logs deploy/api-server-deployment
</code></pre>
<p>Let's increase the number of replicas for our api-server to 2 then apply our yaml file again.</p>
<pre><code class="lang-bash">kubectl apply -f api-server.yaml
kubectl get pods
<span class="hljs-comment"># now we get 2 pod names</span>
api-server-deployment-77fb9b8bcc-sx6c8   1/1     Running   0          16m
api-server-deployment-77fb9b8bcc-xnbnc   1/1     Running   0          104s
</code></pre>
<p>If we want to delete our deployments, we can use kubectl delete</p>
<pre><code class="lang-bash">kubectl delete -f influx.yaml
kubectl delete -f mongo.yaml
kubectl delete -f redis.yaml
</code></pre>
<h2 id="heading-managing-environmental-variables">Managing environmental variables</h2>
<p>Let's move the environmental variables into ConfigMap and Secret.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># config-map.yaml</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ConfigMap</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">homek8-configmap</span>
<span class="hljs-attr">data:</span>
  <span class="hljs-attr">TEST:</span> <span class="hljs-string">"1-2-3"</span>
  <span class="hljs-attr">ENV:</span> <span class="hljs-string">"k8"</span>
  <span class="hljs-attr">NAMESPACE:</span> <span class="hljs-string">"homek8"</span>
  <span class="hljs-attr">DOCKER_INFLUXDB_INIT_MODE:</span> <span class="hljs-string">"setup"</span>
  <span class="hljs-attr">DOCKER_INFLUXDB_INIT_USERNAME:</span> <span class="hljs-string">"andre"</span>
  <span class="hljs-attr">DOCKER_INFLUXDB_INIT_ORG:</span> <span class="hljs-string">"andre"</span>
  <span class="hljs-attr">DOCKER_INFLUXDB_INIT_BUCKET:</span> <span class="hljs-string">"bucket1"</span>
</code></pre>
<p>We can convert our password into baseb4 encoded required in the secret.yaml via <code>echo -n "12345678" | baseb4</code></p>
<pre><code class="lang-yaml"><span class="hljs-comment"># secret.yaml</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Secret</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span>  <span class="hljs-string">homek8-secret</span>
<span class="hljs-attr">type:</span> <span class="hljs-string">Opaque</span>
<span class="hljs-attr">data:</span>
  <span class="hljs-attr">DOCKER_INFLUXDB_INIT_ADMIN_TOKEN:</span>  <span class="hljs-string">bXktdG9rZW4=</span> <span class="hljs-comment"># base64 encoded of my-token</span>
  <span class="hljs-attr">DOCKER_INFLUXDB_INIT_PASSWORD:</span> <span class="hljs-string">MTIzNDU2Nzg=</span> <span class="hljs-comment"># base64 encoded of 12345678</span>
</code></pre>
<p>In our deployment, we can reference it in 2 different ways:</p>
<p>Use <code>envFrom</code> to reference the name of our configMap or secret via <code>configMapRef</code> and <code>secretRef</code> and reference our name <code>homek8-configmap</code> and <code>homek8-secret</code></p>
<p>The second way is to use <code>valueFrom.secretKeyRef</code> or <code>valueFrom.configMapKeyRef</code> to reference the <code>name</code> and <code>key</code> of our resources under <code>env</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># influx.yaml</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">influx</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">influxdb:alpine</span>
        <span class="hljs-attr">resources:</span>
          <span class="hljs-attr">limits:</span>
            <span class="hljs-attr">memory:</span> <span class="hljs-string">"128Mi"</span>
            <span class="hljs-attr">cpu:</span> <span class="hljs-string">"500m"</span>
        <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">8086</span>
        <span class="hljs-attr">envFrom:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">configMapRef:</span>
            <span class="hljs-attr">name:</span> <span class="hljs-string">homek8-configmap</span>
        <span class="hljs-attr">env:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_PASSWORD</span>
          <span class="hljs-attr">valueFrom:</span>
            <span class="hljs-attr">secretKeyRef:</span>
              <span class="hljs-attr">name:</span>  <span class="hljs-string">homek8-secret</span>
              <span class="hljs-attr">key:</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_PASSWORD</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_ADMIN_TOKEN</span>
          <span class="hljs-attr">valueFrom:</span>
            <span class="hljs-attr">secretKeyRef:</span>
              <span class="hljs-attr">name:</span> <span class="hljs-string">homek8-secret</span>
              <span class="hljs-attr">key:</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_ADMIN_TOKEN</span>
</code></pre>
<pre><code class="lang-yaml"><span class="hljs-comment"># api-server.yaml</span>
<span class="hljs-attr">envFrom:</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">configMapRef:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">homek8-configmap</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">secretRef:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">homek8-secret</span>
</code></pre>
<h2 id="heading-manage-volumes-for-our-databases">Manage volumes for our databases</h2>
<p>We can add persistent volume for our instances so that our data will not disappear if the database instances have crashed.</p>
<p>We can create our Persistent Volume for our claims later.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># pv.yaml</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">PersistentVolume</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">mongo-pv</span>
  <span class="hljs-attr">labels:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">mongo-pv</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">capacity:</span>
    <span class="hljs-attr">storage:</span> <span class="hljs-string">1Gi</span>
  <span class="hljs-attr">volumeMode:</span> <span class="hljs-string">Filesystem</span>
  <span class="hljs-attr">accessModes:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">ReadWriteOnce</span>
  <span class="hljs-attr">persistentVolumeReclaimPolicy:</span> <span class="hljs-string">Retain</span>
  <span class="hljs-attr">hostPath:</span>
    <span class="hljs-attr">path:</span> <span class="hljs-string">/mongo</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">PersistentVolume</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">influx-pv</span>
  <span class="hljs-attr">labels:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">influx-pv</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">capacity:</span>
    <span class="hljs-attr">storage:</span> <span class="hljs-string">1Gi</span>
  <span class="hljs-attr">volumeMode:</span> <span class="hljs-string">Filesystem</span>
  <span class="hljs-attr">accessModes:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">ReadWriteOnce</span>
  <span class="hljs-attr">persistentVolumeReclaimPolicy:</span> <span class="hljs-string">Retain</span>
  <span class="hljs-attr">hostPath:</span>
    <span class="hljs-attr">path:</span> <span class="hljs-string">/influx</span>
<span class="hljs-meta">---</span>
</code></pre>
<p><code>metadata.labels</code> specifies the label on our PV, which will be referenced by the PVC later.</p>
<p><code>accessModes</code> specifies how many pods can access and how many nodes can mount this volume</p>
<p><code>resources.requests.storage</code> specifies the size of the storage we require</p>
<p><code>hostPath</code> specifies the location of where this storage mounts a folder on the node's filesystem (not recommended by k8 but an easy way for testing)</p>
<p>We can create a Persistent Volume Claim for each of our DB which is a storage request.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># pvc.yaml</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">PersistentVolumeClaim</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">mongo-pvc</span>
<span class="hljs-attr">spec:</span> 
  <span class="hljs-attr">accessModes:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">ReadWriteOnce</span>
  <span class="hljs-attr">resources:</span>
    <span class="hljs-attr">requests:</span>
      <span class="hljs-attr">storage:</span> <span class="hljs-string">1Gi</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">name:</span> <span class="hljs-string">mongo-pv</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">PersistentVolumeClaim</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">influx-pvc</span>
<span class="hljs-attr">spec:</span> 
  <span class="hljs-attr">accessModes:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">ReadWriteOnce</span>
  <span class="hljs-attr">resources:</span>
    <span class="hljs-attr">requests:</span>
      <span class="hljs-attr">storage:</span> <span class="hljs-string">1Gi</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">name:</span> <span class="hljs-string">influx-pv</span>
<span class="hljs-meta">---</span>
</code></pre>
<p><code>metadata.name</code> here can be referenced later in our deployment</p>
<p><code>accessModes</code> specifies how many pods can access and how many nodes can mount this volume</p>
<p><code>resources.requests.storage</code> specifies the size of the storage we require</p>
<p><code>selector.matchLabels</code> specifies the persistent volume to bind to</p>
<p>We can then apply this to our Kubernetes cluster: <code>kubectl apply -f pvc.yaml -f pv.yaml</code> and we can view their status as below</p>
<pre><code class="lang-bash">kubectl get pvc
<span class="hljs-comment"># output</span>
NAME         STATUS   VOLUME      CAPACITY   ACCESS MODES   STORAGECLASS   AGE
influx-pvc   Bound    influx-pv   1Gi        RWO                           6m54s
mongo-pvc    Bound    mongo-pv    1Gi        RWO                           6m54s

kubectl get pv
<span class="hljs-comment"># output</span>
NAME        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM               STORAGECLASS   REASON   AGE
influx-pv   1Gi        RWO            Retain           Bound    homek8/influx-pvc                           6m52s
mongo-pv    1Gi        RWO            Retain           Bound    homek8/mongo-pvc                            6m52s
</code></pre>
<p>Now we can update our DB deployments to include volumes:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># mongo.yaml</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">mongo-deployment</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">1</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">mongo</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">app:</span> <span class="hljs-string">mongo</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">mongo</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">mongo:4.4.18</span>
        <span class="hljs-attr">resources:</span>
          <span class="hljs-attr">limits:</span>
            <span class="hljs-attr">memory:</span> <span class="hljs-string">"128Mi"</span>
            <span class="hljs-attr">cpu:</span> <span class="hljs-string">"500m"</span>
        <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">27017</span>
        <span class="hljs-attr">volumeMounts:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span>  <span class="hljs-string">mongo-storage</span> <span class="hljs-comment"># name of the volume</span>
            <span class="hljs-attr">mountPath:</span>  <span class="hljs-string">/data/db</span>
      <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">mongo-storage</span>
        <span class="hljs-attr">persistentVolumeClaim:</span>
          <span class="hljs-attr">claimName:</span> <span class="hljs-string">mongo-pvc</span>
<span class="hljs-meta">---</span>
</code></pre>
<p>For Mongo, we are mounting it to <code>/data/db</code></p>
<p>For InfluxDB, we are mounting it to <code>/var/lib/influxdb</code></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>We learned how to create deployment yaml files and deploy them to our Kubernetes clusters. We used a few Kubernetes api-resources such as Deployment, Service, ConfigMap, Secret, PersistentVolume, and PersistentVolumeClaims.</p>
]]></content:encoded></item><item><title><![CDATA[Create a simple API server image for docker and Kubernetes]]></title><description><![CDATA[Summary
We will be creating our own backend API server that we can use to test out our Kubernetes deployment. We will also be running this on a docker-compose setup for testing as well. We will connect to Mongo Database, Redis, and Influx Database to...]]></description><link>https://blog.wongandre.com/create-a-simple-api-server-image-for-docker-and-kubernetes</link><guid isPermaLink="true">https://blog.wongandre.com/create-a-simple-api-server-image-for-docker-and-kubernetes</guid><category><![CDATA[golang]]></category><category><![CDATA[Docker]]></category><category><![CDATA[API basics ]]></category><category><![CDATA[Deploy ]]></category><dc:creator><![CDATA[Andre Wong]]></dc:creator><pubDate>Sat, 18 May 2024 02:39:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1715999596084/1cf7e7f5-0d0d-4b78-8b18-553826f96ae3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-summary">Summary</h2>
<p>We will be creating our own backend API server that we can use to test out our Kubernetes deployment. We will also be running this on a docker-compose setup for testing as well. We will connect to Mongo Database, Redis, and Influx Database to store and query our data.</p>
<p>API endpoint that we will create:</p>
<p>path "/", is just a GET method to test if we can reach our API-server, it will just return a simple JSON object <code>{"hello": "world"}</code></p>
<p>path "/note", a POST method for users to insert their notes into the database, and the GET method to retrieve their desired notes.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Some things to installed beforehand:</p>
<ol>
<li><p>Golang</p>
</li>
<li><p>Docker engine and Docker Compose</p>
</li>
</ol>
<h2 id="heading-writing-api-server-code">Writing API server code</h2>
<ol>
<li><p>initialize go mod file and install necessary dependencies</p>
<pre><code class="lang-bash"> mkdir api-server
 <span class="hljs-built_in">cd</span> api-server
 go mod init api-server

 <span class="hljs-comment"># install dependencies</span>
 go get github.com/gin-gonic/gin
 go get go.mongodb.org/mongo-driver/mongo
 go get github.com/go-redis/redis/v9
 go get github.com/influxdata/influxdb-client-go/v2
</code></pre>
</li>
<li><p>write a simple gin router to start a server in <code>main.go</code></p>
<pre><code class="lang-go"> <span class="hljs-comment">// main.go</span>

 <span class="hljs-keyword">package</span> main
 <span class="hljs-keyword">import</span> (
     ...
 )

 <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
     fmt.Println(<span class="hljs-string">"homek8"</span>)
     }()

     r := gin.Default()

     <span class="hljs-comment">// simple get function to test</span>
     r.GET(<span class="hljs-string">"/"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
         c.JSON(http.StatusOK, gin.H{
             <span class="hljs-string">"hello"</span>: <span class="hljs-string">"world"</span>,
         })
     })

     r.Run()
 }
</code></pre>
</li>
<li><p>write the clients for Mongo, Redis, and Influxdb to connect to the database.</p>
<p> Once done we can add the connection to our main function. Remember to close the connection for the clients by adding it to the defer statement.</p>
<pre><code class="lang-go"> <span class="hljs-comment">// main.go</span>

 <span class="hljs-keyword">var</span> influxToken = os.Getenv(<span class="hljs-string">"DOCKER_INFLUXDB_INIT_ADMIN_TOKEN"</span>)

 <span class="hljs-keyword">var</span> ctx, cancel = context.WithTimeout(context.Background(), <span class="hljs-number">10</span>*time.Second)
 <span class="hljs-keyword">var</span> redisCtx = context.Background()
 <span class="hljs-keyword">var</span> influxCtx = context.Background()

 <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">MongoClientInit</span><span class="hljs-params">()</span> *<span class="hljs-title">mongo</span>.<span class="hljs-title">Client</span></span> {
     <span class="hljs-keyword">defer</span> cancel()
     address := <span class="hljs-string">"mongodb://localhost:27017"</span>
     mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(address))
     <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
         <span class="hljs-built_in">panic</span>(err)
     }
     <span class="hljs-keyword">if</span> err := mongoClient.Ping(ctx, <span class="hljs-literal">nil</span>); err != <span class="hljs-literal">nil</span> {
         <span class="hljs-built_in">panic</span>(err)
     }

     fmt.Println(<span class="hljs-string">"Connect to mongodb OK"</span>)
     <span class="hljs-keyword">return</span> mongoClient
 }

 <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">RedisClientInit</span><span class="hljs-params">()</span> *<span class="hljs-title">redis</span>.<span class="hljs-title">Client</span></span> {
     address := <span class="hljs-string">"localhost:6379"</span>
     rdb := redis.NewClient(&amp;redis.Options{
         Addr:     address,
         Password: <span class="hljs-string">""</span>,
         DB:       <span class="hljs-number">0</span>,
     })

     <span class="hljs-keyword">if</span> err := rdb.Ping(redisCtx); err.Err() != <span class="hljs-literal">nil</span> {
         <span class="hljs-built_in">panic</span>(err)
     }

     fmt.Println(<span class="hljs-string">"Connect to redis OK"</span>)
     <span class="hljs-keyword">return</span> rdb
 }

 <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">InfluxClientInit</span><span class="hljs-params">()</span> *<span class="hljs-title">influxdb2</span>.<span class="hljs-title">Client</span></span> {
     address := <span class="hljs-string">"http://localhost:8086"</span>
     ifxdb := influxdb2.NewClient(address, influxToken)
     ok, err := ifxdb.Ping(influxCtx)
     <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
         <span class="hljs-built_in">panic</span>(err)
     }
     <span class="hljs-keyword">if</span> !ok {
         <span class="hljs-built_in">panic</span>(<span class="hljs-string">"Unable to connect to influxdb"</span>)
     }
     fmt.Println(<span class="hljs-string">"Connect to influx OK"</span>)
     <span class="hljs-keyword">return</span> &amp;ifxdb
 }

 <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
     fmt.Println(<span class="hljs-string">"homek8"</span>)
     fmt.Println(<span class="hljs-string">"connecting to clients ..."</span>)
     redisClient := RedisClientInit()
     mongoClient := MongoClientInit()
     influxClient := InfluxClientInit()
     fmt.Println(<span class="hljs-string">"connected"</span>)

     <span class="hljs-keyword">defer</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
         <span class="hljs-keyword">if</span> err := mongoClient.Disconnect(ctx); err != <span class="hljs-literal">nil</span> {
             <span class="hljs-built_in">panic</span>(err)
         }
         (*influxClient).Close()
     }()

     ...
 }
</code></pre>
</li>
<li><p>write our API handler for <code>/note</code></p>
<p> The POST handler <code>/note</code> will write the title and value information into our Mongo database.</p>
<p> The GET handler <code>/note</code> will first try to retrieve the data from the Redis database. If that fails, it will query the Mongo database and update the entry into the Redis database.</p>
<pre><code class="lang-go"> <span class="hljs-comment">// main.go</span>

 <span class="hljs-keyword">type</span> Note <span class="hljs-keyword">struct</span> {
     Title <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"title"`</span>
     Value <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"value"`</span>
 }

 <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
     ...initiallization...
     coll := mongoClient.Database(<span class="hljs-string">"homek8"</span>).Collection(<span class="hljs-string">"notes"</span>)

     r.GET(<span class="hljs-string">"/note"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
         name, _ := c.Params.Get(<span class="hljs-string">"name"</span>)

         val, err := redisClient.Get(redisCtx, name).Result()
         <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &amp;&amp; err != redis.Nil {
             log.Println(err)
             c.JSON(http.StatusInternalServerError, gin.H{<span class="hljs-string">"error"</span>: <span class="hljs-string">"failed to get note"</span>})
             <span class="hljs-keyword">return</span>
         }
         <span class="hljs-keyword">if</span> err == redis.Nil {
             fmt.Println(<span class="hljs-string">"found redis value:"</span>, val)
             c.JSON(http.StatusOK, Note{Title: name, Value: val})
             <span class="hljs-keyword">return</span>
         }

         <span class="hljs-keyword">var</span> note Note
         filter := bson.D{{Key: <span class="hljs-string">"name"</span>, Value: name}}
         err = coll.FindOne(ctx, filter).Decode(&amp;note)
         <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
             c.JSON(http.StatusInternalServerError, gin.H{<span class="hljs-string">"error"</span>: <span class="hljs-string">"failed to get note"</span>})
             <span class="hljs-keyword">return</span>
         }

         err = redisClient.Set(redisCtx, name, note.Value, <span class="hljs-number">0</span>).Err()
         <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
             fmt.Println(<span class="hljs-string">"failed to cache data into redis"</span>)
         }
         c.JSON(http.StatusOK, note)
     })

     r.POST(<span class="hljs-string">"/note"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
         <span class="hljs-keyword">var</span> note Note
         err := c.BindJSON(&amp;note)
         <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
             fmt.Println(<span class="hljs-string">"unmarshall: "</span>, err)
             c.JSON(http.StatusInternalServerError, gin.H{<span class="hljs-string">"error"</span>: <span class="hljs-string">"failed to add a note"</span>})
             <span class="hljs-keyword">return</span>
         }

         res, err := coll.InsertOne(ctx, note)
         <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
             fmt.Println(<span class="hljs-string">"mongo error: "</span>, err)
             c.JSON(http.StatusInternalServerError, gin.H{<span class="hljs-string">"error"</span>: <span class="hljs-string">"failed to add a note"</span>})
             <span class="hljs-keyword">return</span>
         }
         id := res.InsertedID
         c.JSON(http.StatusOK, gin.H{<span class="hljs-string">"success"</span>: id})
     })

     ...
 }
</code></pre>
</li>
<li><p>write a go routine to send data to influxdb</p>
<p> we will send random average and max values to our influxdb database. It will emit one value once every 5 seconds for 100 times</p>
<pre><code class="lang-go"> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
     writeAPI := (*influxClient).WriteAPIBlocking(<span class="hljs-string">"andre"</span>, <span class="hljs-string">"bucket1"</span>)
     <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
         count := <span class="hljs-number">100</span>
         avg := <span class="hljs-number">24.5</span>
         max := <span class="hljs-number">45.0</span>
         <span class="hljs-keyword">for</span> count &gt; <span class="hljs-number">0</span> {
             avgDeviation := rand.Intn(<span class="hljs-number">20</span>) + <span class="hljs-number">1</span>
             maxDeviation := rand.Intn(<span class="hljs-number">6</span>) + <span class="hljs-number">0</span>
             pt := influxdb2.NewPoint(<span class="hljs-string">"stat"</span>,
                 <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>{<span class="hljs-string">"unit"</span>: <span class="hljs-string">"temperature"</span>},
                 <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}{<span class="hljs-string">"avg"</span>: avg + <span class="hljs-keyword">float64</span>(avgDeviation), <span class="hljs-string">"max"</span>: max + <span class="hljs-keyword">float64</span>(maxDeviation)},
                 time.Now())
             err := writeAPI.WritePoint(influxCtx, pt)
             <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                 fmt.Println(<span class="hljs-string">"emit point error"</span>, err)
             } <span class="hljs-keyword">else</span> {
                 fmt.Println(<span class="hljs-string">"success sent to influx db"</span>)
             }
             time.Sleep(time.Second * <span class="hljs-number">5</span>)
             count -= <span class="hljs-number">1</span>
         }
     }()
 }
</code></pre>
</li>
<li><p>run <code>go build</code> to check if there are no errors</p>
</li>
</ol>
<h2 id="heading-create-a-dockerfile">Create a Dockerfile</h2>
<p>Let's create a docker file for us to containerize our application by creating a <code>Dockerfile</code></p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># image will be build using Go version 1.21</span>
<span class="hljs-keyword">FROM</span> golang:<span class="hljs-number">1.21</span>

<span class="hljs-comment"># sets the current working directory to /app</span>
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>
<span class="hljs-comment"># copy our go.mod and go.sum into our current working directory</span>
<span class="hljs-keyword">COPY</span><span class="bash"> go.mod go.sum ./</span>
<span class="hljs-comment"># install our go dependencies</span>
<span class="hljs-keyword">RUN</span><span class="bash"> go mod download</span>
<span class="hljs-comment"># copy our main.go (or all go files) to our current working directory</span>
<span class="hljs-keyword">COPY</span><span class="bash"> *.go ./</span>
<span class="hljs-comment"># build our application and name our binary file as output</span>
<span class="hljs-keyword">RUN</span><span class="bash"> go build -o output</span>

<span class="hljs-comment"># expose 8080 in our container</span>
<span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">8080</span>

<span class="hljs-comment"># execut command to launch our appliaction</span>
<span class="hljs-keyword">CMD</span><span class="bash"> [ <span class="hljs-string">"/app/output"</span> ]</span>
</code></pre>
<p>Here, we can manually build our image using this Dockerfile by running: <code>docker build .</code> .It will search for any Dockerfile in the current directory and build the image.</p>
<p>We can also tag our images when building with a <code>-t &lt;tag&gt;</code></p>
<p>To view all created images, run <code>docker images</code></p>
<h2 id="heading-create-a-docker-compose-file">Create a docker-compose file</h2>
<p>Now we can write a <code>docker-composee.yml</code> file to spin up the containers we need. We need 4 containers, our backend server we had just written, a mongo db container, a redis db container, and an influx db container. We can easily spin up multiple docker containers with a single command with docker-compose.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">backend:</span>
    <span class="hljs-attr">build:</span> <span class="hljs-string">.</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8080:8080"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ENV=dockercompose</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-token</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">mongo</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">redis</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">influx</span>
  <span class="hljs-attr">mongo:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">mongo:6.0</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"27017:27017"</span>
  <span class="hljs-attr">redis:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">redis:alpine</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"6379:6379"</span>
  <span class="hljs-attr">influx:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">influxdb:alpine</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_USERNAME=andre</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_PASSWORD=12345678</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_MODE=setup</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_ORG=andre</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_BUCKET=bucket1</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-token</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8086:8086"</span>
</code></pre>
<p>In our <code>docker-compose.yml</code> file, we list out the names of the services that we need: <code>backend</code>, <code>mongo</code>, <code>redis</code>, <code>influx</code>.</p>
<p>Under the backend, we have <code>build .</code> which will allow Docker Compose to look for the Dockerfile we had written earlier and use that to build the image for this service. For the other services, we will be using images from Docker Hub, for example, <code>mongo:6.0</code> , <code>redis:alpine</code>.</p>
<p>The ports we defined above also expose the ports of each container to our host.</p>
<p>We also added a new environment variable <code>ENV=dockercompose</code> to our backend container. This allows us to change the URI of our databases easily. We also pass in our influx admin token to the backend so it can successfully emit data to the influx database.</p>
<p><code>depends_on</code> over here will wait for Mongo, redis, and influx services to be up and running first before running the backend service.</p>
<p>Now we need to update our backend code to include this ENV variable. We can get the ENV environment variable through the code and use it to set the client address URL we will connect to. We replace localhost here to the name of our service which will then automatically resolve to the correct IP address.</p>
<p>Under the hood, docker-compose sets up its own DNS resolution that maps the service name to its IP address. This is used for communications between containers.</p>
<pre><code class="lang-go"><span class="hljs-comment">// main.go</span>

<span class="hljs-keyword">var</span> ENV = os.Getenv(<span class="hljs-string">"ENV"</span>)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">MongoClientInit</span><span class="hljs-params">()</span> *<span class="hljs-title">mongo</span>.<span class="hljs-title">Client</span></span> {
    <span class="hljs-keyword">defer</span> cancel()
    address := <span class="hljs-string">"mongodb://localhost:27017"</span>
    <span class="hljs-keyword">if</span> ENV == <span class="hljs-string">"dockercompose"</span> {
        address = <span class="hljs-string">"mongodb://mongo:27017"</span>
    }
    mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(address))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(err)
    }
    <span class="hljs-keyword">if</span> err := mongoClient.Ping(ctx, <span class="hljs-literal">nil</span>); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(err)
    }

    fmt.Println(<span class="hljs-string">"Connect to mongodb OK"</span>)
    <span class="hljs-keyword">return</span> mongoClient
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">RedisClientInit</span><span class="hljs-params">()</span> *<span class="hljs-title">redis</span>.<span class="hljs-title">Client</span></span> {
    address := <span class="hljs-string">"localhost:6379"</span>
    <span class="hljs-keyword">if</span> ENV == <span class="hljs-string">"dockercompose"</span> {
        address = <span class="hljs-string">"redis:6379"</span>
    }
    rdb := redis.NewClient(&amp;redis.Options{
        Addr:     address,
        Password: <span class="hljs-string">""</span>,
        DB:       <span class="hljs-number">0</span>,
    })

    <span class="hljs-keyword">if</span> err := rdb.Ping(redisCtx); err.Err() != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(err)
    }

    fmt.Println(<span class="hljs-string">"Connect to redis OK"</span>)
    <span class="hljs-keyword">return</span> rdb
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">InfluxClientInit</span><span class="hljs-params">()</span> *<span class="hljs-title">influxdb2</span>.<span class="hljs-title">Client</span></span> {
    address := <span class="hljs-string">"http://localhost:8086"</span>
    <span class="hljs-keyword">if</span> ENV == <span class="hljs-string">"dockercompose"</span> {
        address = <span class="hljs-string">"http://influx:8086"</span>
    }
    ifxdb := influxdb2.NewClient(address, influxToken)
    ok, err := ifxdb.Ping(influxCtx)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(err)
    }
    <span class="hljs-keyword">if</span> !ok {
        <span class="hljs-built_in">panic</span>(<span class="hljs-string">"Unable to connect to influxdb"</span>)
    }
    fmt.Println(<span class="hljs-string">"Connect to influx OK"</span>)
    <span class="hljs-keyword">return</span> &amp;ifxdb
}
</code></pre>
<h2 id="heading-verify-if-its-working">Verify if it's working</h2>
<p>Now we can run containers using docker-compose: <code>docker compose up</code>. We should be able to see 4 containers running.</p>
<p>To stop running our containers : <code>docker compose down</code></p>
<h2 id="heading-docker-image">Docker Image</h2>
<p>Now we can push our image to our image repository. Here we will be pushing to <a target="_blank" href="https://hub.docker.com/">Docker hub</a>. After creating an account on the website, we can create a new repository as shown below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1711164446907/30d0a1e7-d452-464d-8266-a1f606d869fc.png" alt class="image--center mx-auto" /></p>
<p>We can then build our image with the command: <code>docker build . -t andrewongzh/api-server:1.0.0</code></p>
<blockquote>
<p>we might need to login to docker hub first in the terminal</p>
<p>run <code>docker login -u &lt;username&gt;</code></p>
</blockquote>
<p>After that, we can push our image with the command: <code>docker push andrewongzh/api-server:1.0.0</code>. This is in the format of &lt;username&gt;/&lt;image name&gt;:&lt;image tag&gt;</p>
<blockquote>
<p>one thing to note here is to tag the image name correctly to match the format of how we are pushing to the repository</p>
</blockquote>
<p>If everything works well, we can see our image on Docker hub.</p>
<p>In conclusion, we created a simple backend server to play around with docker and docker-compose. We will use this server for more things in the future.</p>
<h2 id="heading-handle-secrets">Handle secrets</h2>
<p>Some extra we can do is to move our secrets into a file instead of writing it on the docker-compose.yml file.</p>
<p>First, we can create a folder to store our secrets and then create influxtoken.txt and passwd.txt under it.</p>
<pre><code class="lang-bash">mkdir secrets
<span class="hljs-built_in">echo</span> my-token &gt; ./secrets/influxtoken.txt
<span class="hljs-built_in">echo</span> 12345678 &gt; ./secrets/passwd.txt
</code></pre>
<p>In the docker-compose.yml file make the following changes:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">backend:</span>
    <span class="hljs-attr">build:</span> <span class="hljs-string">.</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8080:8080"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ENV=dockercompose</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=/run/secrets/influxtoken</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">mongo</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">redis</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">influx</span>
    <span class="hljs-attr">secrets:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">passwd</span>
  <span class="hljs-attr">mongo:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">mongo:6.0</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"27017:27017"</span>
  <span class="hljs-attr">redis:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">redis:alpine</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"6379:6379"</span>
  <span class="hljs-attr">influx:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">influxdb:alpine</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_USERNAME=andre</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_PASSWORD=/run/secrets/passwd</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_MODE=setup</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_ORG=andre</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_BUCKET=bucket1</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=/run/secrets/influxtoken</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8086:8086"</span>
<span class="hljs-attr">secrets:</span>
  <span class="hljs-attr">passwd:</span>
    <span class="hljs-attr">file:</span> <span class="hljs-string">./secrets/passwd.txt</span>
  <span class="hljs-attr">influxtoken:</span>
    <span class="hljs-attr">file:</span> <span class="hljs-string">./secrets/influxtoken.txt</span>
</code></pre>
<h2 id="heading-appendix">Appendix</h2>
<pre><code class="lang-go"><span class="hljs-comment">// full code of main.go</span>

<span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"log"</span>
    <span class="hljs-string">"math/rand"</span>
    <span class="hljs-string">"net/http"</span>
    <span class="hljs-string">"os"</span>
    <span class="hljs-string">"time"</span>

    <span class="hljs-string">"github.com/gin-gonic/gin"</span>
    influxdb2 <span class="hljs-string">"github.com/influxdata/influxdb-client-go/v2"</span>
    <span class="hljs-string">"github.com/redis/go-redis/v9"</span>
    <span class="hljs-string">"go.mongodb.org/mongo-driver/bson"</span>
    <span class="hljs-string">"go.mongodb.org/mongo-driver/mongo"</span>
    <span class="hljs-string">"go.mongodb.org/mongo-driver/mongo/options"</span>
)

<span class="hljs-keyword">type</span> Note <span class="hljs-keyword">struct</span> {
    Title <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"title"`</span>
    Value <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"value"`</span>
}

<span class="hljs-keyword">var</span> ENV = os.Getenv(<span class="hljs-string">"ENV"</span>)
<span class="hljs-keyword">var</span> influxToken = os.Getenv(<span class="hljs-string">"DOCKER_INFLUXDB_INIT_ADMIN_TOKEN"</span>)

<span class="hljs-keyword">var</span> ctx, cancel = context.WithTimeout(context.Background(), <span class="hljs-number">10</span>*time.Second)
<span class="hljs-keyword">var</span> redisCtx = context.Background()
<span class="hljs-keyword">var</span> influxCtx = context.Background()

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">MongoClientInit</span><span class="hljs-params">()</span> *<span class="hljs-title">mongo</span>.<span class="hljs-title">Client</span></span> {
    <span class="hljs-keyword">defer</span> cancel()
    address := <span class="hljs-string">"mongodb://localhost:27017"</span>
    <span class="hljs-keyword">if</span> ENV == <span class="hljs-string">"dockercompose"</span> {
        address = <span class="hljs-string">"mongodb://mongo:27017"</span>
    }
    mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(address))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(err)
    }
    <span class="hljs-keyword">if</span> err := mongoClient.Ping(ctx, <span class="hljs-literal">nil</span>); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(err)
    }

    fmt.Println(<span class="hljs-string">"Connect to mongodb OK"</span>)
    <span class="hljs-keyword">return</span> mongoClient
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">RedisClientInit</span><span class="hljs-params">()</span> *<span class="hljs-title">redis</span>.<span class="hljs-title">Client</span></span> {
    address := <span class="hljs-string">"localhost:6379"</span>
    <span class="hljs-keyword">if</span> ENV == <span class="hljs-string">"dockercompose"</span> {
        address = <span class="hljs-string">"redis:6379"</span>
    }
    rdb := redis.NewClient(&amp;redis.Options{
        Addr:     address,
        Password: <span class="hljs-string">""</span>,
        DB:       <span class="hljs-number">0</span>,
    })

    <span class="hljs-keyword">if</span> err := rdb.Ping(redisCtx); err.Err() != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(err)
    }

    fmt.Println(<span class="hljs-string">"Connect to redis OK"</span>)
    <span class="hljs-keyword">return</span> rdb
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">InfluxClientInit</span><span class="hljs-params">()</span> *<span class="hljs-title">influxdb2</span>.<span class="hljs-title">Client</span></span> {
    address := <span class="hljs-string">"http://localhost:8086"</span>
    <span class="hljs-keyword">if</span> ENV == <span class="hljs-string">"dockercompose"</span> {
        address = <span class="hljs-string">"http://influx:8086"</span>
    }
    ifxdb := influxdb2.NewClient(address, influxToken)
    ok, err := ifxdb.Ping(influxCtx)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(err)
    }
    <span class="hljs-keyword">if</span> !ok {
        <span class="hljs-built_in">panic</span>(<span class="hljs-string">"Unable to connect to influxdb"</span>)
    }
    fmt.Println(<span class="hljs-string">"Connect to influx OK"</span>)
    <span class="hljs-keyword">return</span> &amp;ifxdb
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    fmt.Println(<span class="hljs-string">"homek8"</span>)
    fmt.Println(<span class="hljs-string">"connecting to clients ..."</span>)
    redisClient := RedisClientInit()
    mongoClient := MongoClientInit()
    influxClient := InfluxClientInit()
    fmt.Println(<span class="hljs-string">"connected"</span>)

    <span class="hljs-keyword">defer</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">if</span> err := mongoClient.Disconnect(ctx); err != <span class="hljs-literal">nil</span> {
            <span class="hljs-built_in">panic</span>(err)
        }
        (*influxClient).Close()
    }()
    coll := mongoClient.Database(<span class="hljs-string">"homek8"</span>).Collection(<span class="hljs-string">"notes"</span>)
    writeAPI := (*influxClient).WriteAPIBlocking(<span class="hljs-string">"andre"</span>, <span class="hljs-string">"bucket1"</span>)

    <span class="hljs-comment">// we shall emit data every 5 seconds for 100 times</span>
    <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        count := <span class="hljs-number">100</span>
        avg := <span class="hljs-number">24.5</span>
        max := <span class="hljs-number">45.0</span>
        <span class="hljs-keyword">for</span> count &gt; <span class="hljs-number">0</span> {
            avgDeviation := rand.Intn(<span class="hljs-number">20</span>) + <span class="hljs-number">1</span>
            maxDeviation := rand.Intn(<span class="hljs-number">6</span>) + <span class="hljs-number">0</span>
            pt := influxdb2.NewPoint(<span class="hljs-string">"stat"</span>,
                <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>{<span class="hljs-string">"unit"</span>: <span class="hljs-string">"temperature"</span>},
                <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}{<span class="hljs-string">"avg"</span>: avg + <span class="hljs-keyword">float64</span>(avgDeviation), <span class="hljs-string">"max"</span>: max + <span class="hljs-keyword">float64</span>(maxDeviation)},
                time.Now())
            err := writeAPI.WritePoint(influxCtx, pt)
            <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                fmt.Println(<span class="hljs-string">"emit point error"</span>, err)
            } <span class="hljs-keyword">else</span> {
                fmt.Println(<span class="hljs-string">"success sent to influx db"</span>)
            }
            time.Sleep(time.Second * <span class="hljs-number">5</span>)
            count -= <span class="hljs-number">1</span>
        }
    }()

    r := gin.Default()

    <span class="hljs-comment">// simple get function to test</span>
    r.GET(<span class="hljs-string">"/"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
        c.JSON(http.StatusOK, gin.H{
            <span class="hljs-string">"hello"</span>: <span class="hljs-string">"world"</span>,
        })
    })

    r.GET(<span class="hljs-string">"/note"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
        name, _ := c.Params.Get(<span class="hljs-string">"name"</span>)

        val, err := redisClient.Get(redisCtx, name).Result()
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &amp;&amp; err != redis.Nil {
            log.Println(err)
            c.JSON(http.StatusInternalServerError, gin.H{<span class="hljs-string">"error"</span>: <span class="hljs-string">"failed to get note"</span>})
            <span class="hljs-keyword">return</span>
        }
        <span class="hljs-keyword">if</span> err == redis.Nil {
            fmt.Println(<span class="hljs-string">"found redis value:"</span>, val)
            c.JSON(http.StatusOK, Note{Title: name, Value: val})
            <span class="hljs-keyword">return</span>
        }

        <span class="hljs-keyword">var</span> note Note
        filter := bson.D{{Key: <span class="hljs-string">"name"</span>, Value: name}}
        err = coll.FindOne(ctx, filter).Decode(&amp;note)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            c.JSON(http.StatusInternalServerError, gin.H{<span class="hljs-string">"error"</span>: <span class="hljs-string">"failed to get note"</span>})
            <span class="hljs-keyword">return</span>
        }

        err = redisClient.Set(redisCtx, name, note.Value, <span class="hljs-number">0</span>).Err()
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            fmt.Println(<span class="hljs-string">"failed to cache data into redis"</span>)
        }
        c.JSON(http.StatusOK, note)
    })

    r.POST(<span class="hljs-string">"/note"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *gin.Context)</span></span> {
        <span class="hljs-keyword">var</span> note Note
        err := c.BindJSON(&amp;note)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            fmt.Println(<span class="hljs-string">"unmarshall: "</span>, err)
            c.JSON(http.StatusInternalServerError, gin.H{<span class="hljs-string">"error"</span>: <span class="hljs-string">"failed to add a note"</span>})
            <span class="hljs-keyword">return</span>
        }

        res, err := coll.InsertOne(ctx, note)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            fmt.Println(<span class="hljs-string">"mongo error: "</span>, err)
            c.JSON(http.StatusInternalServerError, gin.H{<span class="hljs-string">"error"</span>: <span class="hljs-string">"failed to add a note"</span>})
            <span class="hljs-keyword">return</span>
        }
        id := res.InsertedID
        c.JSON(http.StatusOK, gin.H{<span class="hljs-string">"success"</span>: id})
    })

    r.Run()
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Enabling SSL and HTTPS in our application]]></title><description><![CDATA[What is SSL
Secure Socket Layer is a protocol that provides secure communication over two network devices. It ensures that the data transmitted between two devices remains private. The most common example would be the client computer (us) connecting ...]]></description><link>https://blog.wongandre.com/enabling-ssl-and-https-in-our-application</link><guid isPermaLink="true">https://blog.wongandre.com/enabling-ssl-and-https-in-our-application</guid><category><![CDATA[SSL]]></category><category><![CDATA[https]]></category><category><![CDATA[nginx]]></category><category><![CDATA[networking]]></category><dc:creator><![CDATA[Andre Wong]]></dc:creator><pubDate>Sun, 12 May 2024 03:52:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1715485741291/47440124-2da1-4fa3-b4c8-6675c75c93f8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-what-is-ssl">What is SSL</h2>
<p>Secure Socket Layer is a protocol that provides secure communication over two network devices. It ensures that the data transmitted between two devices remains private. The most common example would be the client computer (us) connecting to a secure server (for example a bank website).</p>
<p>The server usually has its own private key and public signed certificate. The private key is only known by the server and no one else. Here are the steps to establish SSL communication:</p>
<ol>
<li><p>The publicly signed certificate is sent to the client for verification with the Certificate Authority (CA)</p>
</li>
<li><p>Once verified, the client sends a secret message encoded with the public certificate and sends it back to the server</p>
</li>
<li><p>The secret message can be decoded by the private key held by the server and read the contents of the message, thereby creating a new session whereby now only the client and server know this message.</p>
</li>
<li><p>Now both the client and server can use this message to encrypt and decrypt data with the session key</p>
</li>
</ol>
<h2 id="heading-setting-up-ssl">Setting up SSL</h2>
<p>We will be setting up SSL and HTTPS on our <a target="_blank" href="https://blog.wongandre.com/running-internal-applications-on-proxmox">Gitea application that we set up previously</a>. Previously our Gitea application only used HTTP for communication.</p>
<p>We will be exploring two ways to set up SSL/HTTPS:</p>
<ol>
<li><p>Let Gitea handle SSL encryption/decryption by providing the private and public keys to our application</p>
</li>
<li><p>Let our reverse proxy (Nginx) handle SSL encryption/decryption instead and communicate using HTTP between the reverse proxy and Gitea</p>
</li>
</ol>
<h3 id="heading-option-1">Option 1</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708767252447/67ef32b0-83e6-4c23-90fb-6099f9a05d7e.png" alt class="image--center mx-auto" /></p>
<p>Most of the time applications have the option to enable SSL/HTTPS by specifying the private key and public self-signed certificate. Looking at the Gitea documentation to set up SSL, we need to change a few configurations in the <code>app.ini</code> file.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708760764651/3ff90ff3-d8a6-46f7-b85d-3cf929805e28.png" alt="from gitea HTTPS documentation" /></p>
<ol>
<li><p>Let's generate a private key for the CA</p>
<pre><code class="lang-bash"> <span class="hljs-comment"># gitea GITEA_CUSTOM path</span>
 <span class="hljs-built_in">cd</span> /data/gitea/

 mkdir certs &amp;&amp; <span class="hljs-built_in">cd</span> ./certs

 <span class="hljs-comment"># generate private key</span>
 openssl genrsa -out giteaCA.key 2048
</code></pre>
</li>
<li><p>Create a self-signed certificate for the CA</p>
<p> First, create a <code>csr</code> config to set our required information. The important parts here are the subjectAlternativeName. We will need to add the IP address to the Subject Alternative Name (SAN). This is important later when we want to use Gitea as our Kubernetes image repository.</p>
<pre><code class="lang-bash"> [req]
 default_bits = 2048
 prompt = no
 default_md = sha256
 distinguished_name = dn
 req_extensions = req_ext
 default_days = 900

 [dn]
 <span class="hljs-comment"># The Common Name should be the fully qualified domain name (FQDN) of the server.</span>
 <span class="hljs-comment"># Replace "example.com" with your domain name.</span>
 CN = 192.168.1.4

 [req_ext]
 <span class="hljs-comment"># Subject Alternative Name (SAN) extension</span>
 subjectAltName = @alt_names

 [alt_names]
 <span class="hljs-comment"># Add additional subject alternative names (SANs) here.</span>
 IP = 192.168.1.4
</code></pre>
<p> After that we can use this csr to generate our certificate</p>
</li>
<li><pre><code class="lang-bash">  openssl req -new -x509 -key giteaCA.key -out giteaCA.crt -config csr -extensions req_ext
</code></pre>
<p> <code>`-x509` </code> : output should be an X.509 certificate instead of a certificate signing request</p>
<p> <code>-days</code> : how long this certificate is valid for</p>
<p> <code>-key</code> : the private key file used for generating the public certificate</p>
<p> <code>-out</code> : specifies the name of the newly created public certificate</p>
<p> <code>-config</code> : specifies the location of our configuration file</p>
<p> <code>-externsions</code> : specifies that we are using extensions by passing in the var <code>req_ext</code> in our config file</p>
</li>
<li><p>Now we have giteaCA.key and giteaCA.crt</p>
</li>
<li><p>Looking at the Gitea <code>app.ini</code> file here. Our <code>ROOT_URL</code> here should be using HTTPS, <code>PROTOCOL</code> set to HTTPS and also we need to provide the path to our <code>CERT_FILE</code> and <code>KEY_FILE</code> as shown below</p>
<pre><code class="lang-plaintext"> [server]
 ...
 HTTP_PORT = 3000
 ROOT_URL = https://192.168.1.4:3000/
 PROTOCOL = https
 CERT_FILE = ./certs/giteaCA.crt
 KEY_FILE = ./certs/giteaCA.key
</code></pre>
</li>
<li><p>After editing the app.ini file we can restart our Gitea application</p>
</li>
<li><p>Verify that we can access the application via <code>https://192.168.1.4:3000</code> , however, it will give us a warning when we are accessing via a browser</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708751289337/5f875510-e07d-445f-8b28-f9b6513c0f27.png" alt="invalid certificate" /></p>
<p> This is because the client PC that we are accessing our application failed to validate the public certificate that the server has sent us. To solve this issue, we need to add our <code>giteaCA.crt</code> file to the trusted root certificates. This can be done in different ways on different machines (windows, Mac, Linux, etc...) but won't be covered here for simplicity.</p>
</li>
</ol>
<p>Option 1 is a simple way to set out SSL and HTTPS on the application itself.</p>
<h3 id="heading-option-2">Option 2</h3>
<p>We will add SSL and HTTPS to our Nginx reverse proxy instead and keep the Gitea application communication using HTTP. Here are a few reasons why we do this:</p>
<ol>
<li><p>Sometimes enabling SSL on every application can be a hassle. If we have X number of applications needing to enable SSL, then we probably need to generate X private and public keys for each application. This can be reduced to 1 if we only use SSL up to the reverse proxy.</p>
</li>
<li><p>We might not want our application to handle SSL as it needs to use extra resources (ie CPU and memory) to handle SSL encryption and decryption.</p>
</li>
</ol>
<p>As such, we sometimes let our network gateway, which is our reverse proxy also be the point where SSL is decrypted, and for the internal network communication is often done through http instead.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708767262802/10132c26-e2f1-4d55-b9b6-42c60c5f9eb5.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p>Let's create our certificate and key again, one-liner to generate it:</p>
<pre><code class="lang-bash"> openssl req -new -x509 -days 365 -newkey rsa:2048 -nodes -keyout ca.key -out ca.crt
</code></pre>
</li>
<li><p>Let's update our <code>nginx.conf</code> to include this certificate and key:</p>
<pre><code class="lang-nginx"> <span class="hljs-section">server</span> {
     <span class="hljs-attribute">listen</span> <span class="hljs-number">443</span> ssl;
     <span class="hljs-attribute">server_name</span> <span class="hljs-number">192.168.1.4</span>;
     <span class="hljs-attribute">ssl_certificate</span> /certs/ca.crt;
     <span class="hljs-attribute">ssl_certificate_key</span> /certs/ca.key;

     <span class="hljs-attribute">location</span> /gitea/ {

         <span class="hljs-attribute">rewrite</span><span class="hljs-regexp"> ^</span> <span class="hljs-variable">$request_uri</span>;
         <span class="hljs-attribute">rewrite</span><span class="hljs-regexp"> ^/gitea(/.*)</span> <span class="hljs-variable">$1</span> <span class="hljs-literal">break</span>;

         <span class="hljs-attribute">proxy_pass</span> http://192.168.1.4:3000<span class="hljs-variable">$uri</span>;
         <span class="hljs-attribute">proxy_set_header</span> Host <span class="hljs-variable">$host</span>;
         <span class="hljs-attribute">proxy_set_header</span> X-Real-IP <span class="hljs-variable">$remote_addr</span>;
         <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-For <span class="hljs-variable">$proxy_add_x_forwarded_for</span>;
         <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-Proto <span class="hljs-variable">$scheme</span>;
     }
 }
</code></pre>
<p> SSL uses port 443, which we will configure our server to listen to. <code>ssl_certifcate</code> and <code>ssl_certificate_key</code> we will provide a path from our side. We want our client to access Gitea using HTTPS using the domain: <code>https://192.168.1.4/gitea</code>, so we added a <code>/gitea/</code> location so that it can match our prefix with the URI.</p>
<p> <code>rewrite ^/gitea(/.*) $1 break;</code> over here will then rewrite our URI from <code>gitea/($1)</code> to <code>($1)</code>, thereby removing the extra <code>gitea/</code> prefix before calling our proxy_pass directive.</p>
<p> For example, a client sending a request <code>https://192.168.1.4/gitea/user/login</code> will be modified to <code>http://192.168.1.4/user/login</code> in Nginx before forwarding this request. The URI being rewritten here is <code>/gitea/user/login</code> to <code>/user/login</code>.</p>
</li>
<li><p>If we are running our Nginx on a docker container, remember to enable port 443 under the ports section</p>
<pre><code class="lang-yaml"> <span class="hljs-attr">version:</span> <span class="hljs-string">"1.0"</span>
 <span class="hljs-attr">services:</span>
   <span class="hljs-attr">nginx:</span>
     <span class="hljs-attr">image:</span> <span class="hljs-string">nginx:stable</span>
     <span class="hljs-attr">container_name:</span> <span class="hljs-string">nginx</span>
     <span class="hljs-attr">volumes:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">/home/internal/nginx/nginx.conf:/etc/nginx/conf.d/nginx.conf</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">/home/internal/nginx/access.log:/var/log/nginx/access.log</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">/home/internal/nginx/error.log:/var/log/nginx/error.log</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">/home/internal/certs/:/certs</span>
     <span class="hljs-attr">ports:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">"80:80"</span>
       <span class="hljs-comment"># for SSL use only</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">"443:443"</span>
</code></pre>
</li>
<li><p>Update the Gitea <code>app.ini</code> file to use the correct domain. This should be <code>192.168.1.4/gitea</code></p>
<pre><code class="lang-plaintext"> [server]
 ...
 DOMAIN = 192.168.1.4/gitea
 SSH_DOMAIN = 192.168.1.4/gitea
 ROOT_URL = http://192.168.1.4:3000/gitea
</code></pre>
</li>
</ol>
<p>Start our Nginx application and try to access our Gitea application through HTTPS again and it should work as well.</p>
]]></content:encoded></item><item><title><![CDATA[Installing kubernetes on proxmox VM]]></title><description><![CDATA[Using Proxmox, we can create virtual machines that we will be using to set up our Kubernetes cluster.
What is Kubernetes?
According to their official website, Kubernetes (K8s) is an open-source platform for automating deployment, scaling, and managin...]]></description><link>https://blog.wongandre.com/installing-kubernetes-on-proxmox-vm</link><guid isPermaLink="true">https://blog.wongandre.com/installing-kubernetes-on-proxmox-vm</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[proxmox]]></category><category><![CDATA[Homelab]]></category><category><![CDATA[networking]]></category><dc:creator><![CDATA[Andre Wong]]></dc:creator><pubDate>Sun, 11 Feb 2024 17:38:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1707673056592/646719a3-6c0b-4d7c-9cac-110735605e77.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Using Proxmox, we can create virtual machines that we will be using to set up our Kubernetes cluster.</p>
<h3 id="heading-what-is-kubernetes">What is Kubernetes?</h3>
<p>According to their <a target="_blank" href="https://kubernetes.io/">official website</a>, Kubernetes (K8s) is an open-source platform for automating deployment, scaling, and managing containerized applications. Some of K8's features are Automated rollout and rollbacks, availability, resource management, service discovery, and load balancing.</p>
<p>A Kubernetes cluster minimally has two nodes, one being the master and the other being the worker nodes. The master node is also known as the control plane which manages the state of the cluster through etcd, an API server for users to make API calls to query and modify the cluster, schedules newly created pods to worker nodes to run on, and also have various controller manager that watch and handle their specific tasks.</p>
<h3 id="heading-setting-up-kubernetes">Setting up Kubernetes</h3>
<p>We will be using kubeadm to set up our Kubernetes cluster consisting of one master and one worker node. This is a much simpler way to bootstrap our Linux virtual machine with Kubernetes. We will be using Ubuntu-22.04.3-live-server-amd64.iso which can be downloaded from <a target="_blank" href="https://ubuntu.com/download/server">here</a>.</p>
<p>Let's create our first virtual machine and make this our master node. When creating our virtual machine, ensure we have minimally 2 virtual CPU cores and 2GB of RAM. The rest of the settings can be left as default. We will name this node <code>k8-master</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707671423243/7a091fad-5d6a-454b-a229-df074b4c016f.png" alt class="image--center mx-auto" /></p>
<p>Now start the virtual machine and then proceed with the Ubuntu server installation. We can just go for the defaults.</p>
<ol>
<li><p>remove the swap from the server, edit the <code>/etc/fstab</code> and comment out the swap, and then reboot the server. We can verify if the swap is disabled by running <code>swapon --show</code></p>
</li>
<li><p>enable IP tables bridged traffic. we need to enable overlay and br_netfilter, as well as to enable bridged traffic to pass through the firewall rules for packet filtering</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">echo</span> -e <span class="hljs-string">"overlay\nbr_netfilter"</span> | sudo tee /etc/modules-load.d/k8s.conf

 sudo modprobe overlay
 sudo modprobe br_netfilter

 <span class="hljs-built_in">echo</span> -e <span class="hljs-string">"net.bridge.bridge-nf-call-iptables  = 1
 net.bridge.bridge-nf-call-ip6tables = 1
 net.ipv4.ip_forward                 = 1"</span> | sudo tee /etc/sysctl.d/k8s.conf

 <span class="hljs-comment"># refresh system configurations</span>
 sudo sysctl --system
</code></pre>
</li>
<li><p>Now we can install our container runtime. Here we have some options to choose from, mainly CRI-O, containerd, or Docker Engine. In our setup, we will be using CRI-O. The documentation of how to install can be found <a target="_blank" href="https://github.com/cri-o/cri-o/blob/main/install.md#install-packaged-versions-of-cri-o">here</a>.</p>
<p> Set up environmental variables and then run the following as root</p>
<ol>
<li><pre><code class="lang-bash">     OS=<span class="hljs-string">"xUbuntu_22.04"</span>
     VERSION=<span class="hljs-string">"1.28"</span>

     sudo -s
     <span class="hljs-built_in">echo</span> <span class="hljs-string">"deb [signed-by=/usr/share/keyrings/libcontainers-archive-keyring.gpg] https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/<span class="hljs-variable">$OS</span>/ /"</span> &gt; /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
     <span class="hljs-built_in">echo</span> <span class="hljs-string">"deb [signed-by=/usr/share/keyrings/libcontainers-crio-archive-keyring.gpg] https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/<span class="hljs-variable">$VERSION</span>/<span class="hljs-variable">$OS</span>/ /"</span> &gt; /etc/apt/sources.list.d/devel:kubic:libcontainers:stable:cri-o:<span class="hljs-variable">$VERSION</span>.list

     mkdir -p /usr/share/keyrings
     curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/<span class="hljs-variable">$OS</span>/Release.key | gpg --dearmor -o /usr/share/keyrings/libcontainers-archive-keyring.gpg
     curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/<span class="hljs-variable">$VERSION</span>/<span class="hljs-variable">$OS</span>/Release.key | gpg --dearmor -o /usr/share/keyrings/libcontainers-crio-archive-keyring.gpg

     apt-get update
     apt-get install cri-o cri-o-runc -y

     <span class="hljs-comment"># optional</span>
     apt-get install install cri-tools -y

     <span class="hljs-comment"># enable cri-o</span>
     systemctl daemon-reload
     systemctl <span class="hljs-built_in">enable</span> crio --now

     <span class="hljs-comment"># verify cri-o running</span>
     systemctl status crio
</code></pre>
</li>
</ol>
</li>
<li><p>Install kubeadm, kublet, and kubectl</p>
<p> We are installing Kubernetes V1.28, documentations can be referred to <a target="_blank" href="https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/">here</a></p>
<p> Run the following commands in root:</p>
<pre><code class="lang-bash"> apt-get update
 apt-get install -y apt-transport-https ca-certificates curl gpg

 <span class="hljs-comment"># version number here can be disregarded, they all use the same signing key</span>
 curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg

 <span class="hljs-comment"># add packages for kubernetes 1.28</span>
 <span class="hljs-built_in">echo</span> <span class="hljs-string">'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.28/deb/ /'</span> | sudo tee /etc/apt/sources.list.d/kubernetes.list

 apt-get update
 apt-get install -y kubelet kubeadm kubectl

 <span class="hljs-comment"># pin the package version, do not allow them to auto update</span>
 apt-mark hold kubelet kubeadm kubectl
</code></pre>
</li>
</ol>
<p>Now we can do a clone here to make a copy of our virtual machine so we can replicate it later for our worker nodes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707671518463/f86bb3ee-2c45-4369-97ec-18d86b12737d.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-bootstrap-kubeadm">Bootstrap kubeadm</h3>
<ol>
<li><p>set the following environment variables, ensure that the POD_CIDR does not conflict with the host network's CIDR</p>
<p> Pod CIDR addresses are the range of network addresses that Kubernetes assigns to pods for communication with each other</p>
<pre><code class="lang-bash"> IPADDR=<span class="hljs-string">"192.168.1.95"</span>
 <span class="hljs-comment"># nodename will be k8-master</span>
 NODENAME=$(hostname -s)
 POD_CIDR=<span class="hljs-string">"10.244.0.0/16"</span>

 kubeadm init --apiserver-advertise-address=<span class="hljs-variable">$IPADDR</span>  --apiserver-cert-extra-sans=<span class="hljs-variable">$IPADDR</span>  --pod-network-cidr=<span class="hljs-variable">$POD_CIDR</span> --node-name <span class="hljs-variable">$NODENAME</span>
</code></pre>
</li>
<li><p>once done we can see the output as follows:</p>
<pre><code class="lang-plaintext"> Your Kubernetes control-plane has initialized successfully!

 To start using your cluster, you need to run the following as a regular user:

   mkdir -p $HOME/.kube
   sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
   sudo chown $(id -u):$(id -g) $HOME/.kube/config

 Alternatively, if you are the root user, you can run:

   export KUBECONFIG=/etc/kubernetes/admin.conf

 You should now deploy a pod network to the cluster.
 Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
   https://kubernetes.io/docs/concepts/cluster-administration/addons/

 Then you can join any number of worker nodes by running the following on each as root:

 kubeadm join 192.168.1.95:6443 --token rt9hbl.rm42uzvezo1h76ms \
         --discovery-token-ca-cert-hash sha256:11bfcff27dff2930b0f6a236d10105d721239334aa65c738e4a411b7e779ea38
</code></pre>
<p> as you can see we have a few things to do after installation:</p>
<ul>
<li><p>if running as a regular user, run the following:</p>
<pre><code class="lang-bash">  mkdir -p <span class="hljs-variable">$HOME</span>/.kube
  sudo cp -i /etc/kubernetes/adm2in.conf <span class="hljs-variable">$HOME</span>/.kube/config
  sudo chown $(id -u):$(id -g) <span class="hljs-variable">$HOME</span>/.kube/config
</code></pre>
<p>  this allows us to run kubectl commands from the host with this config file. We can copy this config into our personal PC if we do not want to SSH into the control plane to apply changes.</p>
</li>
<li><p>on the other virtual machine that we have cloned, we can join the master node as follows:</p>
<pre><code class="lang-bash">  <span class="hljs-comment"># update worker hostname to k8-worker-1</span>
  sudo hostnamectl set-hostname k8-worker-1
  sudo reboot

  <span class="hljs-comment"># join network</span>
  kubeadm join 192.168.1.95:6443 --token rt9hbl.rm42uzvezo1h76ms \
          --discovery-token-ca-cert-hash sha256:11bfcff27dff2930b0f6a236d10105d721239334aa65c738e4a411b7e779ea38
</code></pre>
<p>  Note that you should use your token as shown in your terminal instead of the token shown here. this is an example of what it may look like.</p>
</li>
<li><p>This is what it should look like after joining successfully</p>
<pre><code class="lang-bash">  This node has joined the cluster:
  * Certificate signing request was sent to apiserver and a response was received.
  * The Kubelet was informed of the new secure connection details.

  Run <span class="hljs-string">'kubectl get nodes'</span> on the control-plane to see this node join the cluster.
</code></pre>
</li>
</ul>
</li>
<li><p>Here are some kubectl commands to verify the cluster</p>
<pre><code class="lang-bash"> kubectl get pod -n kube-system
 kubectl get nodes
 kubctl cluster-info
 kubectl get --raw=<span class="hljs-string">'/readyz?verbose'</span>
</code></pre>
</li>
</ol>
<h3 id="heading-installing-calico-network-plugin">Installing Calico Network Plugin</h3>
<p>The kubeadm installation also mentions deploying a pod network to the cluster. We will be using Calico to manage our pods' networking and policy.</p>
<p>documentation on Calico can be found <a target="_blank" href="https://docs.tigera.io/calico/latest/getting-started/kubernetes/quickstart">here</a></p>
<pre><code class="lang-bash"><span class="hljs-comment"># install Tigera Calico operator and CRDs</span>
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/tigera-operator.yaml
<span class="hljs-comment"># install Calico and CRDs</span>
curl https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/custom-resources.yaml &gt; calico.yaml
<span class="hljs-comment"># update ipPools.cidr: 192.168.0.0/16 to your defined $POD_CIDR above</span>
kubectl create -f calico.yaml

<span class="hljs-comment"># ensure the pods are running</span>
watch kubectl get pods -n calico-system
kubectl get nodes -o wide
</code></pre>
<p>The output of <code>kubectl get pods -A</code> should be as shown below</p>
<pre><code class="lang-bash">Every 2.0s: kubectl get pods -A                                                      k8-master: Sun Feb 11 07:06:16 2024

NAMESPACE          NAME                                      READY   STATUS    RESTARTS   AGE
calico-apiserver   calico-apiserver-567f687f88-dkhkr         1/1     Running   0          2m8s
calico-apiserver   calico-apiserver-567f687f88-sftwl         1/1     Running   0          2m8s
calico-system      calico-kube-controllers-6c5c88c78-j9n57   1/1     Running   0          3m39s
calico-system      calico-node-49pjm                         1/1     Running   0          3m39s
calico-system      calico-node-xdkgv                         1/1     Running   0          3m39s
calico-system      calico-typha-6656cb9b9b-dsgnc             1/1     Running   0          3m39s
calico-system      csi-node-driver-4xlkj                     2/2     Running   0          3m39s
calico-system      csi-node-driver-c5s9d                     2/2     Running   0          3m39s
kube-system        coredns-5dd5756b68-bg8jz                  1/1     Running   0          56m
kube-system        coredns-5dd5756b68-ghrtb                  1/1     Running   0          56m
kube-system        etcd-k8-master                            1/1     Running   0          56m
kube-system        kube-apiserver-k8-master                  1/1     Running   0          57m
kube-system        kube-controller-manager-k8-master         1/1     Running   0          56m
kube-system        kube-proxy-jhdds                          1/1     Running   0          56m
kube-system        kube-proxy-sjmrl                          1/1     Running   0          36m
kube-system        kube-scheduler-k8-master                  1/1     Running   0          56m
tigera-operator    tigera-operator-55585899bf-ktqsx          1/1     Running   0          3m50s
</code></pre>
<h3 id="heading-simple-deployment">Simple Deployment</h3>
<p>Now that our Kubernetes cluster is set up, let's run a simple Nginx deployment to verify our cluster is working</p>
<p>First, create a <code>nginx-deployment.yaml</code> file:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-deployment</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">nginx</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">1</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">app:</span> <span class="hljs-string">nginx</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">nginx</span>
          <span class="hljs-attr">image:</span> <span class="hljs-string">nginx:latest</span>
          <span class="hljs-attr">ports:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">80</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-service</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">nginx</span>
  <span class="hljs-attr">type:</span> <span class="hljs-string">NodePort</span>
  <span class="hljs-attr">ports:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">port:</span> <span class="hljs-number">80</span>
      <span class="hljs-attr">targetPort:</span> <span class="hljs-number">80</span>
      <span class="hljs-attr">nodePort:</span> <span class="hljs-number">31000</span>
</code></pre>
<p>This will create one Nginx deployment and also its corresponding service, which we will expose externally via nodePort on port 31000</p>
<p>Run <code>kubectl apply -f nginx-deployment.yaml</code> to apply our changes. If everything goes well, we should be able to see our default Nginx page at <code>http://&lt;node IP&gt;:31000</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707671169797/92e8260b-6de1-48b2-b813-40100f4879c0.png" alt class="image--center mx-auto" /></p>
<p>Resources referenced:</p>
<ul>
<li><p><a target="_blank" href="https://kubernetes.io/">https://kubernetes.io/</a></p>
</li>
<li><p><a target="_blank" href="https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/">https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/</a></p>
</li>
<li><p><a target="_blank" href="https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/">https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/cri-o/cri-o/blob/main/install.md#install-packaged-versions-of-cri-o">https://github.com/cri-o/cri-o/blob/main/install.md#install-packaged-versions-of-cri-o</a></p>
</li>
<li><p><a target="_blank" href="https://docs.tigera.io/calico/latest/getting-started/kubernetes/quickstart">https://docs.tigera.io/calico/latest/getting-started/kubernetes/quickstart</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Homelab reverse proxy with NGINX]]></title><description><![CDATA[background
Now we have a bunch of docker containers set up in our internal Linux container. They can be accessed through the IP address of the Linux container, 192.168.1.4 and the respective port number. However, this seems a bit troublesome whenever...]]></description><link>https://blog.wongandre.com/homelab-reverse-proxy-with-nginx</link><guid isPermaLink="true">https://blog.wongandre.com/homelab-reverse-proxy-with-nginx</guid><category><![CDATA[nginx]]></category><category><![CDATA[proxmox]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Reverse Proxy]]></category><dc:creator><![CDATA[Andre Wong]]></dc:creator><pubDate>Mon, 25 Dec 2023 07:51:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1703490616508/3f2feafa-7bf4-4b3d-81f5-d93d04b3521a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-background">background</h3>
<p>Now we have a bunch of docker containers set up in our internal Linux container. They can be accessed through the IP address of the Linux container, <code>192.168.1.4</code> and the respective port number. However, this seems a bit troublesome whenever we need to type it in our web browser URL. Instead, what we will do is create a nginx docker container to handle and redirect all our oncoming network coming in. This nginx will act as a reverse proxy to our backend applications.</p>
<h3 id="heading-what-is-a-reverse-proxy">what is a reverse proxy?</h3>
<p>A reverse proxy is a device or application that can handle requests from clients and redirect the request to other backend services that are managed by us. Our reverse proxy acts as the middleman between the clients and the backend servers. From the point of the client, we are only accessing from a single IP address which is the reverse proxy. The client will not be able to directly access our backend servers' IP address and ports, which improves our security.</p>
<p>Some other notable functions of a reverse proxy include:</p>
<ol>
<li><p>load balancing</p>
<p> The reverse proxy can distribute the request between the list of upstream servers provided. This ensures that servers will not be overloaded with requests, which will help our system's stability and latency.</p>
<p> Some reverse proxies can also provide health checks that periodically check the uptime of the upstream server. If the reverse proxy discovers that one of the servers is down, then it can help to redirect the requests to other available servers.</p>
</li>
<li><p>caching</p>
<p> Request can be cached in the reverse proxy for static content to reduce unnecessary queries to the backend for the same content. This helps to reduce the load on our backend servers.</p>
</li>
<li><p>SSL encryption</p>
<p> SSL can be decrypted at the reverse proxy to read the unencrypted data. This data can then be passed to our backend servers without the use of SSL. That way our backend servers will not need to do the decryption themselves, which can free up valuable resources</p>
</li>
</ol>
<h3 id="heading-lets-set-up-our-own-reverse-proxy">let's set up our own reverse proxy</h3>
<p><a target="_blank" href="https://blog.wongandre.com/running-internal-applications-on-proxmox">Check out the applications we deployed previously here.</a></p>
<p>The reverse proxy that we will be using is <a target="_blank" href="https://www.nginx.com/">Nginx</a>. Instead of accessing each of our applications deployed previously via their IP address and port number, we will use easy to remember domain names to access our application</p>
<ol>
<li><p>Firstly let us configure our DNS server to add the entry in our local DNS record to resolve the domain name to the IP address.</p>
<p> we will resolve <code>dashy.local, gitea.local, paperless.local, seafile.local</code> to our host machine IP address at <code>192.168.1.4</code>.</p>
<p> I am doing this in my own Pi-hole server under Local DNS records. This configuration can also be done on the router which manages the DNS records.</p>
<p> The Pi-hole example is shown below:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703488997902/f62275ab-4891-4d08-9ac4-692a6a8c3edf.png" alt class="image--center mx-auto" /></p>
<p> Be sure to add both www.XXX.local and XXX.local domains in the records.</p>
</li>
<li><p>Now we can set up a Nginx reverse proxy in our docker environment that listens to all incoming connections to the host machine.</p>
<p> Write a docker-compose.yml file with the contents as shown below:</p>
<pre><code class="lang-yaml"> <span class="hljs-attr">version:</span> <span class="hljs-string">"1.0"</span>
 <span class="hljs-attr">services:</span>
   <span class="hljs-attr">nginx:</span>
     <span class="hljs-attr">image:</span> <span class="hljs-string">nginx:stable</span>
     <span class="hljs-attr">container_name:</span> <span class="hljs-string">nginx</span>
     <span class="hljs-attr">volumes:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">/home/internal/nginx/nginx.conf:/etc/nginx/conf.d/nginx.conf</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">/home/internal/nginx/access.log:/var/log/nginx/access.log</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">/home/internal/nginx/error.log:/var/log/nginx/error.log</span>
     <span class="hljs-attr">ports:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">"80:80"</span>
</code></pre>
<p> Here are some explanations of this ymal file:</p>
<p> image:</p>
<p> We defined a nginx service that uses the <code>nginx:stable</code> image to run our application.</p>
<p> container_name</p>
<p> <code>container_name</code> here helps to give our container an easy name to refer to.</p>
<p> volumes:</p>
<p> <code>volumes</code> here we have <code>nginx.conf</code> with our reverse proxy configuration, we will be writing later. We also have the <code>access.log</code> and <code>error.log</code> linked from our local storage to the container's default log location for nginx to refer to log entries more easily.</p>
<p> ports:</p>
<p> Ports here help to expose the ports in our container to our LXC container. <code>80:80</code> , the right-hand side refers to the port in the container and nginx, while the left-hand side refers to the port on the host machine.</p>
</li>
<li><p>Writing our nginx configurations</p>
<pre><code class="lang-nginx"> <span class="hljs-section">server</span> {
     <span class="hljs-attribute">listen</span> <span class="hljs-number">80</span>;
     <span class="hljs-attribute">server_name</span> www.gitea.local gitea.local;

     <span class="hljs-attribute">location</span> / {
         <span class="hljs-attribute">proxy_pass</span> http://192.168.1.4:3000;

         <span class="hljs-attribute">proxy_set_header</span> Host <span class="hljs-variable">$host</span>;
         <span class="hljs-attribute">proxy_set_header</span> X-Real-IP <span class="hljs-variable">$remote_addr</span>;
         <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-For <span class="hljs-variable">$proxy_add_x_forwarded_for</span>;
         <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-Proto <span class="hljs-variable">$scheme</span>;
     }
 }

 <span class="hljs-section">server</span> {
     <span class="hljs-attribute">listen</span> <span class="hljs-number">80</span>;
     <span class="hljs-attribute">server_name</span> www.paperless.local paperless.local;

     <span class="hljs-attribute">location</span> / {
         <span class="hljs-attribute">proxy_pass</span> http://192.168.1.4:8000;

         <span class="hljs-attribute">proxy_set_header</span> Host <span class="hljs-variable">$host</span>;
         <span class="hljs-attribute">proxy_set_header</span> X-Real-IP <span class="hljs-variable">$remote_addr</span>;
         <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-For <span class="hljs-variable">$proxy_add_x_forwarded_for</span>;
         <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-Proto <span class="hljs-variable">$scheme</span>;
     }
 }

 <span class="hljs-section">server</span> {
     <span class="hljs-attribute">listen</span> <span class="hljs-number">80</span>;
     <span class="hljs-attribute">server_name</span> www.dashy.local dashy.local;

     <span class="hljs-attribute">location</span> / {
         <span class="hljs-attribute">proxy_pass</span> http://192.168.1.4:4000;

         <span class="hljs-attribute">proxy_set_header</span> Host <span class="hljs-variable">$host</span>;
         <span class="hljs-attribute">proxy_set_header</span> X-Real-IP <span class="hljs-variable">$remote_addr</span>;
         <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-For <span class="hljs-variable">$proxy_add_x_forwarded_for</span>;
         <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-Proto <span class="hljs-variable">$scheme</span>;
     }
 }

 <span class="hljs-section">server</span> {
     <span class="hljs-attribute">listen</span> <span class="hljs-number">80</span>;
     <span class="hljs-attribute">server_name</span> www.seafile.local seafile.local;

     <span class="hljs-attribute">location</span> / {
         <span class="hljs-attribute">proxy_pass</span> http://192.168.1.4:3001;

         <span class="hljs-attribute">proxy_set_header</span> Host <span class="hljs-variable">$host</span>;
         <span class="hljs-attribute">proxy_set_header</span> X-Real-IP <span class="hljs-variable">$remote_addr</span>;
         <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-For <span class="hljs-variable">$proxy_add_x_forwarded_for</span>;
         <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-Proto <span class="hljs-variable">$scheme</span>;
     }
 }
</code></pre>
<p> Some explanation:</p>
<p> <code>listen</code> keyword here denotes that nginx is listening to port 80 in the container.</p>
<p> <code>server_name</code> here refers to the domain that nginx is handling, which is important for our domain-based routing.</p>
<p> <code>location /</code> block here denotes how the root URL is being handled.</p>
<p> <code>proxy_pass</code> here means nginx will forward the incoming request to the specified URL instead, which in this case is our application IP address and port number.</p>
<p> <code>proxy_set_header</code> here change the headers of our incoming request so that the application knows where did this request originally came from. For example, <code>X-Forwarded-For</code> header here is the chain of IP addresses starting from the client IP address all the way to the last proxy IP address before reaching the application. There can be several levels of proxies before it eventually reaches our application.</p>
</li>
<li><p>start our nginx server by running <code>docker compose up -d</code></p>
</li>
<li><p>try navigating to our applications via <code>dashy.local</code>, <code>seafile.local</code>, <code>gitea.local</code> or <code>paperless.local</code> and verify everything is running all right.</p>
</li>
</ol>
<p>We have now implemented our very own reverse proxy 🎉</p>
<p>Here is what it looks like before:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703489393599/6bc0af67-06f5-4172-84a5-9dc09b18cfb7.png" alt class="image--center mx-auto" /></p>
<p>Here is our new updated network flow:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703489419987/3045207c-e411-4065-8937-811c1d4531d5.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-whats-next">what's next?</h3>
<p>In the future, we will be adding SSL security to our nginx so that we can connect via HTTPS instead of HTTP.</p>
]]></content:encoded></item><item><title><![CDATA[Installing and connecting to a GUI in Proxmox LXC]]></title><description><![CDATA[Heres a short guide to installing a desktop environment in our Linux container and remotely connecting to it using x2go client.

in Proxmox, create a container

update our dependencies
 apt update and upgrade


install a desktop environment, we are g...]]></description><link>https://blog.wongandre.com/installing-and-connecting-to-a-gui-in-proxmox-lxc</link><guid isPermaLink="true">https://blog.wongandre.com/installing-and-connecting-to-a-gui-in-proxmox-lxc</guid><category><![CDATA[proxmox]]></category><category><![CDATA[Ubuntu]]></category><category><![CDATA[xfce4]]></category><dc:creator><![CDATA[Andre Wong]]></dc:creator><pubDate>Mon, 18 Dec 2023 15:56:45 GMT</pubDate><content:encoded><![CDATA[<p>Heres a short guide to installing a desktop environment in our Linux container and remotely connecting to it using x2go client.</p>
<ol>
<li><p>in Proxmox, create a container</p>
</li>
<li><p>update our dependencies</p>
<pre><code class="lang-bash"> apt update and upgrade
</code></pre>
</li>
<li><p>install a desktop environment, we are going to install xfce4</p>
<pre><code class="lang-bash">  apt install xfce4 -y
</code></pre>
</li>
<li><p>choose either gdm3 or lightDM as your preferred display manager</p>
</li>
<li><p>install x2goserver and x2goserver-xsession so we can use the client to connect to it later</p>
<pre><code class="lang-bash"> apt install x2goserver x2goserver-xsession -y
</code></pre>
</li>
<li><p>create a new user to be added in later. This is because ssh doesn't allow connection via root user</p>
<pre><code class="lang-bash"> adduser user1
</code></pre>
</li>
<li><p>install the x2go client on another PC. download link can be found here: <a target="_blank" href="https://wiki.x2go.org/doku.php">https://wiki.x2go.org/doku.php</a></p>
</li>
<li><p>connect to the remote Linux container. Enter login, which is the username we just created and also the host, which is the IP address of where our Linux container is. Session type select XFCE</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702914771922/ad3e74ba-2ba8-40c1-87d9-f256bdb897e5.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Now we can remotely connect to our containers using a GUI</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702914900311/d6f05f9d-1cb2-4e73-a2f2-6b26adfd356a.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Running internal applications on proxmox]]></title><description><![CDATA[things to do after creating a new container
sudo apt update && apt upgrade
apt install curl

let's install docker now using the quick install script
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh ./get-docker.sh

now let's create our non-...]]></description><link>https://blog.wongandre.com/running-internal-applications-on-proxmox</link><guid isPermaLink="true">https://blog.wongandre.com/running-internal-applications-on-proxmox</guid><category><![CDATA[proxmox]]></category><category><![CDATA[Homelab]]></category><category><![CDATA[server]]></category><dc:creator><![CDATA[Andre Wong]]></dc:creator><pubDate>Sat, 09 Dec 2023 05:55:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1702101011908/02a80ba8-de7a-489b-ba0d-f6b6fdf6598d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-things-to-do-after-creating-a-new-container">things to do after creating a new container</h2>
<pre><code class="lang-bash">sudo apt update &amp;&amp; apt upgrade
apt install curl
</code></pre>
<p>let's install docker now using the quick install script</p>
<pre><code class="lang-bash">curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh ./get-docker.sh
</code></pre>
<p>now let's create our non-privileged user and call it internal</p>
<p>we wouldn't want to run docker with a root user for security issues. If a user in the docker container managed to break out of it, it may gain root access privileges on the host.</p>
<p>we also need to add this user to the docker group to have access to the docker commands</p>
<pre><code class="lang-bash">sudo adduser internal
sudo usermod -aG docker internal
</code></pre>
<h2 id="heading-now-we-can-set-up-a-few-applications-that-we-can-use">now we can set up a few applications that we can use</h2>
<p>For easier access to application files, we will be using docker bind mounts instead of named volume mounts. We will also want the data folders to be located in each of the respective application folders.</p>
<ul>
<li><p>set up gitea</p>
<p>  Gitea is an application that allows us to host our git repositories</p>
<p>  create a <code>gitea</code> directory and add a <code>docker-compose.yml</code> file. we will be using postgresql for this app.</p>
<pre><code class="lang-bash">  version: <span class="hljs-string">"3"</span>

  networks:
    gitea:
      external: <span class="hljs-literal">false</span>

  services:
    server:
      image: gitea/gitea:1.21.0
      container_name: gitea
      environment:
        - USER_UID=1000
        - USER_GID=1000
        - GITEA__database__DB_TYPE=postgres
        - GITEA__database__HOST=db:5432
        - GITEA__database__NAME=gitea
        - GITEA__database__USER=gitea
        - GITEA__database__PASSWD=gitea
      restart: always
      networks:
        - gitea
      volumes:
        - /home/internal/gitea/data:/data
        - /etc/timezone:/etc/timezone:ro
        - /etc/localtime:/etc/localtime:ro
      ports:
        - <span class="hljs-string">"3000:3000"</span>
        - <span class="hljs-string">"222:22"</span>
      depends_on:
        - db

    db:
      image: postgres:14
      restart: always
      environment:
        - POSTGRES_USER=gitea
        - POSTGRES_PASSWORD=gitea
        - POSTGRES_DB=gitea
      networks:
        - gitea
      volumes:
        - /home/internal/gitea/postgres:/var/lib/postgresql/data
</code></pre>
<p>  start the containers with <code>docker compose up -d</code> and verify if the link is working at http://&lt;ip&gt;:3000</p>
</li>
<li><p>set up paperless-ngx</p>
<p>  Paperless-ngx is a document management system that can archive and index important documents so we do not have extra paper or receipts lying around</p>
<p>  we can run an easy install script that will auto create our folders and configurations</p>
<p>  since we are using volume bind mounts, be sure to set the consume, media and db folders to use absolute paths</p>
<pre><code class="lang-bash">  bash -c <span class="hljs-string">"<span class="hljs-subst">$(curl -L https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)</span>"</span>
</code></pre>
<p>  verify if the link is working at http://&lt;ip&gt;:8000</p>
</li>
<li><p>set up dashy</p>
<p>  Dashy is a homepage organizer to keep tracks of all the links that we will have when we create more applications down the road</p>
<p>  create a <code>dashy</code> directory and add a <code>docker-compose.yml</code> file.</p>
<pre><code class="lang-bash">  ---
  version: <span class="hljs-string">"3.8"</span>
  services:
      dashy:
        image: lissy93/dashy
        container_name: Dashy
        <span class="hljs-comment"># Pass in your config file below, by specifying the path on your host machine</span>
        volumes:
          - /home/internal/dashy/data/my-config.yml:/app/public/conf.yml
        ports:
          - 4000:80
        <span class="hljs-comment"># Set any environmental variables</span>
        environment:
          - NODE_ENV=production
        <span class="hljs-comment"># Specify your user ID and group ID. You can find this by running `id -u` and `id -g`</span>
          - UID=1000
          - GID=1000
        <span class="hljs-comment"># Specify restart policy</span>
        restart: unless-stopped
        <span class="hljs-comment"># Configure healthchecks</span>
        healthcheck:
          <span class="hljs-built_in">test</span>: [<span class="hljs-string">'CMD'</span>, <span class="hljs-string">'node'</span>, <span class="hljs-string">'/app/services/healthcheck'</span>]
          interval: 1m30s
          timeout: 10s
          retries: 3
          start_period: 40s
</code></pre>
<p>  start the containers with <code>docker compose up -d</code> and verify if the link is working at http://&lt;ip&gt;:4000</p>
</li>
<li><p>set up seafile</p>
<p>  Seafile is a file hosting system that can keep track of important files and data. Files can be sync across multiple devices as well.</p>
<p>  create a <code>seafile</code> directory and add a <code>docker-compose.yml</code> file.</p>
<pre><code class="lang-bash">  services:
    db:
      image: mariadb:10.11
      container_name: seafile-mysql
      environment:
        - MYSQL_ROOT_PASSWORD=db_dev  <span class="hljs-comment"># Requested, set the root's password of MySQL service.</span>
        - MYSQL_LOG_CONSOLE=<span class="hljs-literal">true</span>
      volumes:
        - /home/internal/seafile/mysql/db:/var/lib/mysql  <span class="hljs-comment"># Requested, specifies the path to MySQL data persistent store.</span>
      networks:
        - seafile-net

    memcached:
      image: memcached:1.6.18
      container_name: seafile-memcached
      entrypoint: memcached -m 256
      networks:
        - seafile-net

    seafile:
      image: seafileltd/seafile-mc:11.0.0
      container_name: seafile
      ports:
        - <span class="hljs-string">"80:80"</span>
  <span class="hljs-comment">#     - "443:443"  # If https is enabled, cancel the comment.</span>
      volumes:
        - /home/internal/seafile/data:/shared   <span class="hljs-comment"># Requested, specifies the path to Seafile data persistent store.</span>
      environment:
        - DB_HOST=db
        - DB_ROOT_PASSWD=db_dev  <span class="hljs-comment"># Requested, the value should be root's password of MySQL service.</span>
        - TIME_ZONE=Asia/Singapore  <span class="hljs-comment"># Optional, default is UTC. Should be uncomment and set to your local time zone.</span>
        - SEAFILE_ADMIN_EMAIL=me@example.com <span class="hljs-comment"># Specifies Seafile admin user, default is 'me@example.com'.</span>
        - SEAFILE_ADMIN_PASSWORD=PASSWORD     <span class="hljs-comment"># Specifies Seafile admin password, default is 'asecret'.</span>
        - SEAFILE_SERVER_LETSENCRYPT=<span class="hljs-literal">false</span>   <span class="hljs-comment"># Whether to use https or not.</span>
  <span class="hljs-comment">#      - SEAFILE_SERVER_HOSTNAME=docs.seafile.com # Specifies your host name if https is enabled.</span>
      depends_on:
        - db
        - memcached
      networks:
        - seafile-net

  networks:
    seafile-net:
</code></pre>
<p>  start the containers with <code>docker compose up -d</code> and verify if the link is working at http://&lt;ip&gt;:80</p>
</li>
</ul>
<h2 id="heading-lets-look-at-our-homelab-overview-again">Let's look at our homelab overview again</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702100990941/e934fe51-adc0-473f-8878-ccd6912ad615.png" alt class="image--center mx-auto" /></p>
<p>I used a static IP address for my internal Linux container and set it to <code>192.168.1.4</code> for easy reference when I want to enter into my web app. This can be changed inside <code>/etc/network/interfaces</code> for Ubuntu.</p>
<p>Manually typing <code>http://192.168.1.4:4000</code> can be difficult to remember. In the future, we can add a reverse proxy to handle all the connections to our internal docker containers.</p>
<h3 id="heading-links-to-the-application-documentation">links to the application documentation</h3>
<ul>
<li><p><a target="_blank" href="https://docs.gitea.com/">https://docs.gitea.com/</a></p>
</li>
<li><p><a target="_blank" href="https://docs.paperless-ngx.com/setup/">https://docs.paperless-ngx.com/setup/</a></p>
</li>
<li><p><a target="_blank" href="https://dashy.to/docs/quick-start/">https://dashy.to/docs/quick-start/</a></p>
</li>
<li><p><a target="_blank" href="https://manual.seafile.com/docker/deploy_seafile_with_docker/">https://manual.seafile.com/docker/deploy_seafile_with_docker/</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Installing proxmox on a old server desktop]]></title><description><![CDATA[Background
Recently I got my hands on an old server desktop. I plan to repurpose it into a home lab to host some internal applications to be used at home and also host some of my side projects which can be externally accessed by the public. I have de...]]></description><link>https://blog.wongandre.com/installing-proxmox-on-a-old-server-desktop</link><guid isPermaLink="true">https://blog.wongandre.com/installing-proxmox-on-a-old-server-desktop</guid><category><![CDATA[proxmox]]></category><category><![CDATA[Homelab]]></category><category><![CDATA[server]]></category><dc:creator><![CDATA[Andre Wong]]></dc:creator><pubDate>Sun, 18 Jun 2023 03:24:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1687058641388/22f41a2b-707c-4f68-892e-36a09243546e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-background">Background</h2>
<p>Recently I got my hands on an old server desktop. I plan to repurpose it into a home lab to host some internal applications to be used at home and also host some of my side projects which can be externally accessed by the public. I have decided to try installing Proxmox on the desktop.</p>
<p>Proxmox is an open-source virtualization management platform that can provide both <strong>Kernel-based Virtual machines (KVM)</strong> and also <strong>container-based virtualization with Linux Containers (LXC)</strong>. It also provides a nifty web-based interface for the user to interact with Proxmox.</p>
<h2 id="heading-hardware-specs">Hardware specs</h2>
<p>Here is some hardware information about my old server desktop:</p>
<ul>
<li><p>Precision Tower 5810</p>
</li>
<li><p>Intel Xeon E5-1607 v3</p>
</li>
<li><p>Nvidia Quadro K2200</p>
</li>
<li><p>8GB of ram</p>
</li>
<li><p>256GB Samsung SSD</p>
</li>
<li><p>500GB Hard disk drive</p>
</li>
</ul>
<p>It's a pretty old desktop, released in 2014 and I managed to get all its components for free. Next, we can try to wipe everything in the disk drives and install Proxmox.</p>
<h2 id="heading-installing-proxmox">Installing Proxmox</h2>
<ol>
<li><p>The Proxmox VE (Virtual Environment) can be downloaded for free from the <a target="_blank" href="https://www.proxmox.com/en/downloads">official Proxmox website</a>. The version I am using is <code>proxmox-ve_7.3-1.iso</code>.</p>
</li>
<li><p>Create a bootable installation media from a USB drive. Here I used <a target="_blank" href="https://rufus.ie/en/">Rufus</a> to write the Proxmox ISO image.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684490385847/9c089a45-cf68-4ca8-a845-d63619251bf8.jpeg" alt="Rufus" class="image--center mx-auto" /></p>
<p> For Rufus, remember to write in DD Image or else it would not work.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684490377411/bf1808bf-12ca-402d-86fa-b26c420d93a5.jpeg" alt="select DD image mode" class="image--center mx-auto" /></p>
</li>
<li><p>Insert the USB drive into the desktop and boot from the USB drive.</p>
<ul>
<li><p>remember to disable secure boot on the BIOS of certain Desktop</p>
</li>
<li><p>you might need to enter the BIOS to change the boot order so that the USB drive can be booted first</p>
</li>
</ul>
</li>
<li><p>Follow the installation guide shown on the desktop</p>
<ol>
<li><p>Select <strong>install proxmox ve</strong></p>
</li>
<li><p>Accept End User License Agreement</p>
</li>
<li><p>Select which disk to be used for installing Proxmox</p>
</li>
<li><p>Select password, language and keyboard layout</p>
</li>
<li><p>Configure the management network settings. Here my router is on <strong>192.168.1.254</strong>, which I will be using as my gateway and DNS server. I set the IP address of my Proxmox server to <strong>192.168.1.2</strong> for easy reference. In the router web interface, I also assigned a static IP address of <strong>192.168.1.2</strong> to the Mac address of my desktop.</p>
<ul>
<li><p>Hostname: host1.pve.local</p>
</li>
<li><p>IP address: 192.168.1.2</p>
</li>
<li><p>Gateway: 192.168.1.254</p>
</li>
<li><p>DNS server: 192.168.1.254</p>
</li>
</ul>
</li>
<li><p>After installation is successful, you will be prompted to reboot.</p>
</li>
</ol>
</li>
</ol>
<h2 id="heading-accessing-proxmox-web-interface">Accessing Proxmox Web Interface</h2>
<p>Once fully installed, we can go to the Proxmox web interface denoted by the IP address of the Proxmox server and run on port 8006. In my case, it would be at <code>https://192.168.1.2:8006</code>. The interface is shown below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684491150799/4ec0200a-ffb2-4233-bfb4-d89705c388c9.jpeg" alt="proxmox interface" class="image--center mx-auto" /></p>
<p>Currently, it only has one node, which represents a physical server that is currently running. We could install another Promox on another desktop to have another node, but one should be enough for a small home lab.</p>
<h2 id="heading-creating-linux-containers">Creating Linux Containers</h2>
<p>Now, we are ready to create our Linux Containers.</p>
<p>I want to use Linux Containers (LXC) over Kernel-based Virtual machines (KVM) because it provides lightweight virtualization and efficient resource utilization. KVM runs its own kernel as compared with LXC which uses the host operating system's kernel and shares them with other containers. This means that there is less overhead and fewer resources utilized.</p>
<p>The downside of LXC is that since our host operating system uses Linux, LXC can only run Linux-based operating systems such as Ubuntu or ArchLinux.</p>
<p>Let's get some LXC templates first. Under <strong>host1 &gt; local &gt; CT Templates &gt; Templates</strong>, choose <strong>ubuntu-22.04-standard</strong> and then press download.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684492175150/ae8e4580-1c30-4010-b06a-65c641b149a2.jpeg" alt="download template" class="image--center mx-auto" /></p>
<p>then click on <strong>Create CT</strong> on the top right-hand side of the interface. Set our Node as host1, our physical server and the hostname of our LXC as internal. I intend for this LXC to contain all my docker applications that are used internally in my home and not exposed to the public.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684492293889/32ea0c1e-3904-4f20-9db1-f21795ed3835.png" alt="create LXC 1" class="image--center mx-auto" /></p>
<p>Next, select the template we had just downloaded earlier.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684492423512/80d2212a-7fbd-4b30-b4f6-d30e0f1e6f11.jpeg" alt="create LXC template" class="image--center mx-auto" /></p>
<p>Set the amount of disk space we require.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684492548617/b6a35f21-0c91-491d-9cd9-d75e87ff44da.jpeg" alt="create LXC storage" class="image--center mx-auto" /></p>
<p>Followed by the size of the ram we require.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684511037673/abdef481-4e1a-4956-bc2c-5abf7080aba3.jpeg" alt="create LXC RAM" class="image--center mx-auto" /></p>
<p>Lastly, we configure the IPv4 and IPv6 to use DHCP, to dynamically allocate IP addresses for our new LXC.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684511228883/415330f9-36cf-486e-a33c-6319ae6d99e1.jpeg" alt="create LXC network" class="image--center mx-auto" /></p>
<p>Confirm our selection and then reboot. Once our LXC has started up, we can view it through the console tab as shown below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684511318744/b7dd09f1-0719-4db9-87cf-41641fc4e9d0.jpeg" alt="console output" class="image--center mx-auto" /></p>
<p>I will set up 2 more containers, one that is externally accessible for public use, named <strong>external</strong>, and the other for development use, named <strong>dev</strong>.</p>
<h2 id="heading-architecture">Architecture</h2>
<p>Here is an overview of what I have created.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684664366756/c9b270d3-b55c-4a7f-a90a-50b0876376db.jpeg" alt="architecture" class="image--center mx-auto" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I have installed Proxmox on an old desktop and launched three Linux containers. In the future, I plan to install applications that I can use in my own home network and also to host some of my projects to the public.</p>
]]></content:encoded></item><item><title><![CDATA[Authentication with email and code verification]]></title><description><![CDATA[When using passwords to authenticate our applications, we often want an extra layer of security to prevent malicious attackers from spamming our registration. As such we can try to implement an email and code verification in our app.
In the app that ...]]></description><link>https://blog.wongandre.com/authentication-with-email-and-code-verification</link><guid isPermaLink="true">https://blog.wongandre.com/authentication-with-email-and-code-verification</guid><category><![CDATA[golang]]></category><category><![CDATA[gin-gonic]]></category><category><![CDATA[authentication]]></category><category><![CDATA[MongoDB]]></category><category><![CDATA[Next.js]]></category><dc:creator><![CDATA[Andre Wong]]></dc:creator><pubDate>Wed, 17 May 2023 07:18:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1684296252105/aa544c58-65ef-451c-9e17-0e6a427bff37.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When using passwords to authenticate our applications, we often want an extra layer of security to prevent malicious attackers from spamming our registration. As such we can try to implement an email and code verification in our app.</p>
<p>In the app that we are creating today, we will be using NextJs for our frontend, Go for our backend and saving our user data into MongoDB.</p>
<p>The full code can be found on my <a target="_blank" href="https://github.com/AndreWongZH/auth_boilerplate">GitHub repository</a>.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before diving into the implementation, make sure to have the following prerequisites:</p>
<ol>
<li><p>Go installed on your machine</p>
</li>
<li><p>Node.js and NPM installed on your machine</p>
</li>
<li><p>MongoDB operations in Go (can read the <a target="_blank" href="https://www.mongodb.com/docs/drivers/go/current/quick-start/">documentation here</a>)</p>
</li>
<li><p>A MongoDB database set up and accessible (I am using MongoDB Atlas which is exposed using a MONGODB_URI)</p>
</li>
<li><p>An email service provider (here I am using Gmail to send my emails)</p>
</li>
</ol>
<h2 id="heading-architecture">Architecture</h2>
<p>Here is a general overview of the components we will be building and the connection between the different services. We will only be sending our crafted emails to our mail service server using SMTP protocol, which will then forward this email to the intended recipient.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684295358613/ec6c1667-7599-4c27-ae38-81bf6eb46627.png" alt="software architecture" class="image--center mx-auto" /></p>
<h2 id="heading-approach">Approach</h2>
<p>After a user registers for an account, we want to store their details in our database and then send them an email with a verification link and code.</p>
<p>For our approach to verification with a link, we want to generate a link such that when clicked, will make a GET request to our server and once verified, we will redirect them back to our frontend homepage for them to log in.</p>
<p>For our approach to verification with code, we want to generate a random 4-digit code that is sent to their email. The user can then key this code in the frontend after they registered and if the code is correct, will allow the user into the dashboard.</p>
<h2 id="heading-configure-smtp-with-mail-service">Configure SMTP with mail service</h2>
<p>The email service provider I am using is Gmail and they have <a target="_blank" href="https://support.google.com/mail/answer/7126229">a guide</a> on how to enable SMTP on the Gmail account. You can use other options like Sendgrid, Mailgun or Outlook.</p>
<p>For Gmail, we need to enable the <strong>IMAP Access</strong> option on our Gmail settings page.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684305134286/b87c56b1-0c1e-4777-b37c-7ae5ad5a55c7.png" alt class="image--center mx-auto" /></p>
<p>We will also need to generate an <strong>App Password</strong> as we do not want to use the same password we use for our Google account to authenticate our SMTP access. For that, we can go to Google account settings, under <strong>Security</strong> and then <strong>2 steps verification</strong>, add your new app password.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684305250285/440d3ccf-c08d-4467-873e-926fb6c77dca.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-setting-up-backend-service">Setting up backend service</h2>
<ol>
<li><p>Make backend folder</p>
<pre><code class="lang-bash"> mkdir backend
 <span class="hljs-built_in">cd</span> backend
</code></pre>
</li>
<li><p>Set up a new Go module and install necessary dependencies</p>
<pre><code class="lang-bash"> go mod init auth-boilerplate

 go get github.com/gin-gonic/gin
 go get github.com/gin-contrib/cors

 go get go.mongodb.org/mongo-driver/mongo
 go get github.com/joho/godotenv

 go get golang.org/x/crypto/bcrypt
</code></pre>
</li>
<li><p>Set up the API server using <code>gin</code> and include <code>cors</code> settings so that we do not run into any "Blocked by CORS policy" errors.</p>
<pre><code class="lang-go"> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
     r := gin.Default()

     r.Use(cors.New(cors.Config{
         AllowOrigins:  []<span class="hljs-keyword">string</span>{fmt.Sprintf(<span class="hljs-string">"http://%s"</span>, origin)},
         AllowMethods:  []<span class="hljs-keyword">string</span>{<span class="hljs-string">"POST"</span>, <span class="hljs-string">"GET"</span>, <span class="hljs-string">"PUT"</span>, <span class="hljs-string">"OPTIONS"</span>, <span class="hljs-string">"DELETE"</span>},
         AllowHeaders:  []<span class="hljs-keyword">string</span>{<span class="hljs-string">"Origin"</span>, <span class="hljs-string">"Content-Type"</span>, <span class="hljs-string">"Set-Cookie"</span>},
         ExposeHeaders: []<span class="hljs-keyword">string</span>{<span class="hljs-string">"Content-Length"</span>, <span class="hljs-string">"Content-Type"</span>, <span class="hljs-string">"Set-Cookie"</span>, <span class="hljs-string">"Access-Control-Allow-Credentials"</span>, <span class="hljs-string">"Access-Control-Expose-Headers"</span>, <span class="hljs-string">"Access-Control-Allow-Origin"</span>, <span class="hljs-string">"set-cookie"</span>},

         AllowCredentials: <span class="hljs-literal">true</span>,
         AllowWebSockets:  <span class="hljs-literal">true</span>,
         MaxAge:           <span class="hljs-number">12</span> * time.Hour,
     }))

     r.POST(<span class="hljs-string">"/verify"</span>, verifyCode)
     r.GET(<span class="hljs-string">"/verify/link"</span>, verifyLink)
     r.POST(<span class="hljs-string">"/register"</span>, register)
     r.POST(<span class="hljs-string">"/login"</span>, login)

     r.Run(<span class="hljs-string">":3001"</span>)
 }
</code></pre>
</li>
<li><p>Establish a connection with the Mongo database using <code>mongo.Connect()</code> and if successful, we can use this Mongo client to query and insert our collections. We will use <code>godotenv.Load(".env")</code> to load our <code>MONGODB_URI</code> value from a <code>.env</code> file in our project folder.</p>
<pre><code class="lang-go"> <span class="hljs-keyword">var</span> client *mongo.Client

 <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">initDatabase</span><span class="hljs-params">()</span> <span class="hljs-title">error</span></span> {
     uri := os.Getenv(<span class="hljs-string">"MONGODB_URI"</span>)
     fmt.Println(uri)
     <span class="hljs-keyword">if</span> uri == <span class="hljs-string">""</span> {
         log.Println(<span class="hljs-string">"URI not found in environmental variables"</span>)
         <span class="hljs-keyword">return</span> errors.New(<span class="hljs-string">"URI not found"</span>)
     }

     <span class="hljs-keyword">var</span> err error
     client, err = mongo.Connect(context.TODO(), options.Client().ApplyURI(uri))
     <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
         log.Println(<span class="hljs-string">"Error connecting to database"</span>)
         <span class="hljs-keyword">return</span> errors.New(<span class="hljs-string">"cannot connect to db"</span>)
     }

     <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
 }

 <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
     err := godotenv.Load(<span class="hljs-string">".env"</span>)
     <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
         log.Println(<span class="hljs-string">"failed to load .env file"</span>)
         <span class="hljs-keyword">return</span>
     }
     err = initDatabase()
     <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
         <span class="hljs-keyword">return</span>
     }
     <span class="hljs-comment">///...</span>
 }
</code></pre>
</li>
<li><p>Declare user struct to handle user inputs from JSON and BSON. BSON, which stands for binary JSON, is used by MongoDB to store and transmit data. When <code>omitempty</code> is used in the ID field, it means that the ID field will not be added into MongoDB if the value of ID in our User struct is empty, MongoDB will auto-generate the <code>_id</code> field for us.</p>
<p> We will be generating the <code>VerifyHash</code> and <code>VerifyCode</code> on Registration and compare them when they try to verify.</p>
<p> <code>ValidHash</code> and <code>ValidCode</code> will be storing the deadline when the hash or code is considered invalid and will need to be regenerated.</p>
<pre><code class="lang-go"> <span class="hljs-keyword">type</span> User <span class="hljs-keyword">struct</span> {
     ID          <span class="hljs-keyword">string</span>    <span class="hljs-string">`bson:"_id,omitempty"`</span>
     Name        <span class="hljs-keyword">string</span>    <span class="hljs-string">`json:"name" bson:"name"`</span>
     Email       <span class="hljs-keyword">string</span>    <span class="hljs-string">`json:"email" bson:"email"`</span>
     Password    <span class="hljs-keyword">string</span>    <span class="hljs-string">`json:"password" bson:"passwordHash"`</span>
     VerifyHash  <span class="hljs-keyword">string</span>    <span class="hljs-string">`bson:"verifyhash"`</span>
     VerifyCode  <span class="hljs-keyword">string</span>    <span class="hljs-string">`bson:"verifycode"`</span>
     IsVerified  <span class="hljs-keyword">bool</span>      <span class="hljs-string">`json:"isVerified" bson:"isVerified"`</span>
     DateCreated time.Time <span class="hljs-string">`bson:"dateCreated"`</span>
     ValidHash   time.Time <span class="hljs-string">`bson:"validHash"`</span>
     ValidCode   time.Time <span class="hljs-string">`bson:"validCode"`</span>
 }
</code></pre>
</li>
<li><p>Let's add our register function whenever the user registers on the frontend.</p>
<p> <code>ctx.BindJSON</code> is used to bind the JSON data from the request body to the <code>newUser</code> struct.</p>
<p> <code>bcrypt.GenerateFromPassword</code> function is used to generate a secure hash from the user's password. We then replace the current password provided by the user.</p>
<p> We will then generate the verification link and code and add them to the struct, together with their expiration.</p>
<p> We will insert our <code>newUser</code> struct into the database using the <code>InsertOne</code> method and add it to our <code>auth</code> database and <code>users</code> collection.</p>
<p> Lastly, we will send the user their verification email with the link and code attached.</p>
<pre><code class="lang-go"> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">register</span><span class="hljs-params">(ctx *gin.Context)</span></span> {
     <span class="hljs-keyword">var</span> newUser User

     err := ctx.BindJSON(&amp;newUser)
     <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
         log.Println(<span class="hljs-string">"Error binding JSON"</span>)
         ctx.AbortWithStatusJSON(http.StatusInternalServerError, <span class="hljs-literal">nil</span>)
         <span class="hljs-keyword">return</span>
     }

     <span class="hljs-comment">// ensure email does not exist in database</span>
     <span class="hljs-comment">// ensure email and password is valid</span>
     <span class="hljs-comment">// left as an exercise</span>

     <span class="hljs-comment">// generate hash from user password</span>
     hash, err := bcrypt.GenerateFromPassword([]<span class="hljs-keyword">byte</span>(newUser.Password), <span class="hljs-number">6</span>)
     <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
         log.Println(<span class="hljs-string">"error hashing password"</span>)
     }
     newUser.Password = <span class="hljs-keyword">string</span>(hash)

     <span class="hljs-comment">// generate verification link</span>
     newUser.VerifyHash = generateVerifyHash()
     <span class="hljs-comment">// generate verification code</span>
     newUser.VerifyCode = generateVerifyCode()

     <span class="hljs-comment">// add deadline for hash and code expiry</span>
     <span class="hljs-comment">// in the example here, we use 15mins for hash and 3 mins for code</span>
     now := time.Now()
     newUser.DateCreated = now
     newUser.ValidHash = now.Add(<span class="hljs-number">15</span> * time.Minute)
     newUser.ValidCode = now.Add(<span class="hljs-number">3</span> * time.Minute)

     <span class="hljs-comment">// add user to database</span>
     coll := client.Database(<span class="hljs-string">"auth"</span>).Collection(<span class="hljs-string">"users"</span>)
     log.Println(<span class="hljs-string">"add: "</span>, newUser)
     result, err := coll.InsertOne(context.TODO(), newUser)
     <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
         log.Println(<span class="hljs-string">"failed to add new user:"</span>, err)
         ctx.JSON(http.StatusOK, gin.H{
             <span class="hljs-string">"success"</span>: <span class="hljs-literal">false</span>,
         })
         <span class="hljs-keyword">return</span>
     }

     fmt.Println(result)

     <span class="hljs-comment">// send the verification email</span>
     sendVerificationEmail(newUser)

     ctx.JSON(http.StatusOK, gin.H{
         <span class="hljs-string">"success"</span>: <span class="hljs-literal">true</span>,
     })
 }
</code></pre>
</li>
<li><p>Create our login function.</p>
<p> We will use the user's email to query the database and fetch their document.</p>
<p> Once found, use <code>bcrypt.CompareHashAndPassword</code> to compare the hash in the database and the user's password in the request body. If successful, then proceed to return a JSON response based on whether the user is verified. If not successful, then return a JSON response with an HTTP code unauthorized.</p>
<pre><code class="lang-go"> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">login</span><span class="hljs-params">(ctx *gin.Context)</span></span> {
     <span class="hljs-keyword">var</span> user User

     err := ctx.BindJSON(&amp;user)
     <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
         log.Println(<span class="hljs-string">"Error binding JSON"</span>)
         ctx.AbortWithStatusJSON(http.StatusInternalServerError, <span class="hljs-literal">nil</span>)
         <span class="hljs-keyword">return</span>
     }

     <span class="hljs-keyword">var</span> dbUser User
     coll := client.Database(<span class="hljs-string">"auth"</span>).Collection(<span class="hljs-string">"users"</span>)
     filter := bson.D{{<span class="hljs-string">"email"</span>, user.Email}}
     err = coll.FindOne(context.TODO(), filter).Decode(&amp;dbUser)
     <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
         <span class="hljs-keyword">if</span> err == mongo.ErrNoDocuments {
             log.Println(fmt.Sprintf(<span class="hljs-string">"searching for users with email: %s yield no result found"</span>, dbUser.Email))
             ctx.JSON(http.StatusUnauthorized, gin.H{
                 <span class="hljs-string">"success"</span>: <span class="hljs-literal">false</span>,
                 <span class="hljs-string">"error"</span>:   <span class="hljs-string">"Either email or password is incorrect"</span>,
             })
             <span class="hljs-keyword">return</span>
         }
         log.Println(<span class="hljs-string">"failed to search for user"</span>)
     }

     err = bcrypt.CompareHashAndPassword([]<span class="hljs-keyword">byte</span>(dbUser.Password), []<span class="hljs-keyword">byte</span>(user.Password))
     <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
         log.Println(<span class="hljs-string">"password is different"</span>)
         ctx.JSON(http.StatusUnauthorized, gin.H{
             <span class="hljs-string">"success"</span>: <span class="hljs-literal">false</span>,
             <span class="hljs-string">"error"</span>:   <span class="hljs-string">"Either email or password is incorrect"</span>,
         })
         <span class="hljs-keyword">return</span>
     }

     <span class="hljs-keyword">if</span> !user.IsVerified {
         ctx.JSON(http.StatusOK, gin.H{
             <span class="hljs-string">"success"</span>:    <span class="hljs-literal">false</span>,
             <span class="hljs-string">"isVerified"</span>: <span class="hljs-literal">false</span>,
             <span class="hljs-string">"error"</span>:      <span class="hljs-string">"user not verified"</span>,
         })
     }

     ctx.JSON(http.StatusOK, gin.H{
         <span class="hljs-string">"success"</span>:    <span class="hljs-literal">true</span>,
         <span class="hljs-string">"isVerified"</span>: <span class="hljs-literal">false</span>,
         <span class="hljs-string">"error"</span>:      <span class="hljs-string">""</span>,
     })
 }
</code></pre>
</li>
<li><p>Let's create our handler function for <code>verifyCode</code> and <code>verifyLink</code>.</p>
<p> We will read the link's email, hash and code from the request's query params. Compare the hash or code with the one retrieved from the database. If successful, we can then update the <code>isVerified</code> field in the Mongo database.</p>
<p> Here we use the <code>coll.UpdateOne</code> to update our document based on the <code>_id</code> of the document.</p>
<pre><code class="lang-go"> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">verifyCode</span><span class="hljs-params">(ctx *gin.Context)</span></span> {
     <span class="hljs-comment">// read from query params</span>
     email := ctx.Query(<span class="hljs-string">"email"</span>)
     hash := ctx.Query(<span class="hljs-string">"hash"</span>)
     code := ctx.Query(<span class="hljs-string">"code"</span>)

     <span class="hljs-keyword">var</span> user User

     coll := client.Database(<span class="hljs-string">"auth"</span>).Collection(<span class="hljs-string">"users"</span>)
     filter := bson.D{{<span class="hljs-string">"email"</span>, email}}
     err := coll.FindOne(context.TODO(), filter).Decode(&amp;user)
     <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
         <span class="hljs-keyword">if</span> err == mongo.ErrNoDocuments {
             log.Println(fmt.Sprintf(<span class="hljs-string">"searching for users with email: %s yield no result found"</span>, email))
         }
         log.Println(<span class="hljs-string">"failed to search for user"</span>)
     }

     <span class="hljs-comment">// set user to verified</span>
     <span class="hljs-keyword">if</span> user.VerifyCode == code &amp;&amp; user.ValidCode.After(time.Now()) {
         id, _ := primitive.ObjectIDFromHex(user.ID)
         filter = bson.D{{<span class="hljs-string">"_id"</span>, id}}
         update := bson.D{{<span class="hljs-string">"$set"</span>, bson.D{{<span class="hljs-string">"isVerified"</span>, <span class="hljs-literal">true</span>}}}}
         result, err := coll.UpdateOne(context.TODO(), filter, update)
         <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
             log.Println(<span class="hljs-string">"failed to update:"</span>, err)
         }
         log.Println(result)
         ctx.JSON(http.StatusOK, gin.H{
             <span class="hljs-string">"success"</span>: <span class="hljs-literal">true</span>,
         })
         <span class="hljs-keyword">return</span>
     }

     ctx.JSON(http.StatusOK, gin.H{
         <span class="hljs-string">"success"</span>: <span class="hljs-literal">false</span>,
         <span class="hljs-string">"error"</span>:   <span class="hljs-string">"incorrect code"</span>,
     })
 }
</code></pre>
</li>
<li><p>Let's implement the helper functions we used earlier.</p>
<p> <code>generateVerifyCode</code> will randomly generate an Int between 0 to 9999 and then pad the value such that it is 4 characters long</p>
<p> <code>generateVerifyHash</code> will randomly generate random bytes in our array and then change this value by encoding it into a string via base64 encoding.</p>
<pre><code class="lang-go"> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">generateVerifyCode</span><span class="hljs-params">()</span> <span class="hljs-title">string</span></span> {
     rand.Seed(time.Now().UnixNano())
     randomNumber := rand.Intn(<span class="hljs-number">10000</span>)
     paddedNumber := fmt.Sprintf(<span class="hljs-string">"%04d"</span>, randomNumber)

     <span class="hljs-keyword">return</span> paddedNumber
 }

 <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">generateVerifyHash</span><span class="hljs-params">()</span> <span class="hljs-title">string</span></span> {
     charLen := <span class="hljs-number">50</span>
     randomBytes := <span class="hljs-built_in">make</span>([]<span class="hljs-keyword">byte</span>, charLen)

     _, err := rand.Read(randomBytes)
     <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
         fmt.Println(<span class="hljs-string">"failed to generate random bytes"</span>)
     }

     randomString := base64.URLEncoding.EncodeToString(randomBytes)
     <span class="hljs-keyword">return</span> randomString[:charLen]
 }
</code></pre>
</li>
<li><p>Set up our connection with the SMTP host. We then use the <code>net/smtp</code> package from Go to construct and send our email.</p>
<pre><code class="lang-go"><span class="hljs-keyword">const</span> (
    <span class="hljs-comment">// configs to connect to gmail smtp server</span>
    smtpHost = <span class="hljs-string">"smtp.gmail.com"</span>
    smtpPort = <span class="hljs-number">587</span>
)

appkey = os.Getenv(<span class="hljs-string">"APPKEY"</span>)
hostEmail = os.Getenv(<span class="hljs-string">"HOSTEMAIL"</span>)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getVerifyLink</span><span class="hljs-params">(email <span class="hljs-keyword">string</span>, verifyHash <span class="hljs-keyword">string</span>, verifyCode <span class="hljs-keyword">string</span>)</span> <span class="hljs-title">string</span></span> {
    <span class="hljs-keyword">return</span> fmt.Sprintf(<span class="hljs-string">"http://localhost:3001/verify/link?email=%s&amp;hash=%s&amp;code=%s"</span>, email, verifyHash, verifyCode)
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">sendVerificationEmail</span><span class="hljs-params">(user User)</span></span> {
    verifyLink := getVerifyLink(user.Email, user.VerifyHash, user.VerifyCode)
    to := []<span class="hljs-keyword">string</span>{user.Email}
    subject := <span class="hljs-string">"Email verification"</span>
    body := fmt.Sprintf(<span class="hljs-string">"Hello, \r\n\r\nThis email contains the verification link and verification code, please the link below to verify your account.\r\n\r\nVerification link: %s\r\n\r\nAlternatively, u can also key in the verification code to verify.\r\n\r\nVerification code: %s"</span>, verifyLink, user.VerifyCode)

    message := []<span class="hljs-keyword">byte</span>(<span class="hljs-string">"To: "</span> + user.Email + <span class="hljs-string">"\r\n"</span> +
        <span class="hljs-string">"From: "</span> + hostEmail + <span class="hljs-string">"\r\n"</span> +
        <span class="hljs-string">"Subject: "</span> + subject + <span class="hljs-string">"\r\n"</span> +
        <span class="hljs-string">"\r\n"</span> +
        body + <span class="hljs-string">"\r\n"</span>)

    auth := smtp.PlainAuth(<span class="hljs-string">""</span>, hostEmail, appkey, smtpHost)

    err := smtp.SendMail(<span class="hljs-string">"smtp.gmail.com:587"</span>, auth, hostEmail, to, message)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Println(<span class="hljs-string">"Failed to send email"</span>)
        log.Println(err)
        <span class="hljs-keyword">return</span>
    }
    log.Println(<span class="hljs-string">"Email sent successfully"</span>)
}
</code></pre>
</li>
</ol>
<h2 id="heading-setting-up-frontend">Setting up frontend</h2>
<ol>
<li><p>Initialize a new Next.js project using the command below and name the folder <strong>frontend</strong></p>
<pre><code class="lang-bash"> npx create-next-app@latest
</code></pre>
</li>
<li><p>In the root frontend, create a folder <strong>components</strong> that holds our styled-components for Fields, Buttons and ButtonText.</p>
<pre><code class="lang-typescript"> <span class="hljs-comment">// component.tsx</span>

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Field = <span class="hljs-function">(<span class="hljs-params">{ title, placeholder = <span class="hljs-string">''</span>, <span class="hljs-keyword">type</span> = <span class="hljs-string">'text'</span>, value, onChange }:
     { title: <span class="hljs-built_in">string</span>, placeholder?: <span class="hljs-built_in">string</span>, <span class="hljs-keyword">type</span>?: <span class="hljs-built_in">string</span>, value: <span class="hljs-built_in">string</span>, onChange: <span class="hljs-built_in">Function</span> }</span>) =&gt;</span> {
     <span class="hljs-keyword">return</span> (
         &lt;div&gt;
             &lt;h3&gt;{title}&lt;/h3&gt;
             &lt;input className=<span class="hljs-string">"py-2 px-4 leading-tight text-gray-700 appearance-none border-4 border-gray-200 rounded focus:outline-none focus:bg-white focus:border-teal-500"</span>
                 placeholder={placeholder} <span class="hljs-keyword">type</span>={<span class="hljs-keyword">type</span>} value={value} onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> onChange(e.target.value)} /&gt;
         &lt;/div&gt;
     )
 }

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Button = <span class="hljs-function">(<span class="hljs-params">{ text, onClick }: { text: <span class="hljs-built_in">string</span>, onClick?: <span class="hljs-built_in">Function</span> }</span>) =&gt;</span> {
     <span class="hljs-keyword">return</span> (
        &lt;button onClick={<span class="hljs-function">() =&gt;</span> { onClick?.() }} 
             className=<span class="hljs-string">"px-7 py-3 mt-10 bg-teal-500 hover:bg-teal-700 border rounded-lg font-bold"</span>&gt;{text}&lt;/button&gt;
     )
 }

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> ButtonText = <span class="hljs-function">(<span class="hljs-params">{ text, onClick }: { text: <span class="hljs-built_in">string</span>, onClick: <span class="hljs-built_in">Function</span> }</span>) =&gt;</span> {
     <span class="hljs-keyword">return</span> (
         &lt;button onClick={<span class="hljs-function">() =&gt;</span> { onClick() }} 
             className=<span class="hljs-string">"px-7 py-3 mt-10 hover:bg-gray-600 rounded-lg font-bold"</span>&gt;{text}&lt;/button&gt;
     )
 }
</code></pre>
</li>
<li><p>Under <strong>/app/page.tsx</strong>, replace the boiler code with our login, register and Verify components. They provide the fields for inputting our user's login or register info and calling the backend API on submit.</p>
<pre><code class="lang-typescript"> <span class="hljs-string">'use client'</span>

 <span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>
 <span class="hljs-keyword">import</span> { useRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/navigation"</span>
 <span class="hljs-keyword">import</span> { Button, ButtonText, Field } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/components/component"</span>

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
     <span class="hljs-keyword">const</span> [isLogin, setLogin] = useState(<span class="hljs-literal">true</span>)
     <span class="hljs-keyword">const</span> [showVerify, setVerify] = useState(<span class="hljs-literal">false</span>)
     <span class="hljs-keyword">const</span> [email, setEmail] = useState(<span class="hljs-string">""</span>)

     <span class="hljs-keyword">return</span> (
         &lt;main className=<span class="hljs-string">"min-h-screen flex flex-col items-center justify-center"</span>&gt;
             &lt;h1 className=<span class="hljs-string">"text-4xl font-bold mb-10"</span>&gt;Authentication demo page&lt;/h1&gt;
             {
                 showVerify ? &lt;Verify email={email}/&gt; : isLogin 
                     ? &lt;Login setLogin={setLogin} setVerify={setVerify}/&gt;
                     : &lt;Register setLogin={setLogin} setVerify={setVerify} email={email} setEmail={setEmail}/&gt;
             }
         &lt;/main&gt;
     )
 }

 <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Register</span>(<span class="hljs-params">{ setLogin, setVerify, email, setEmail }:
     { setLogin: <span class="hljs-built_in">Function</span>, setVerify: <span class="hljs-built_in">Function</span>, email: <span class="hljs-built_in">string</span>, setEmail: <span class="hljs-built_in">Function</span> }</span>) </span>{
     <span class="hljs-keyword">const</span> [name, setName] = useState(<span class="hljs-string">""</span>)
     <span class="hljs-keyword">const</span> [password, setPassword] = useState(<span class="hljs-string">""</span>)
     <span class="hljs-keyword">const</span> [repassword, setRepassword] = useState(<span class="hljs-string">""</span>)
     <span class="hljs-keyword">const</span> [error, setError] = useState(<span class="hljs-string">""</span>)

     <span class="hljs-keyword">const</span> register = <span class="hljs-function">() =&gt;</span> {
         <span class="hljs-keyword">if</span> (password !== repassword) {
             <span class="hljs-keyword">return</span>
         }
         <span class="hljs-keyword">const</span> user = {
             name,
             email,
             password
         }
         fetch(<span class="hljs-string">"http://localhost:3001/register"</span>, {
             method: <span class="hljs-string">"POST"</span>,
             headers: {
                 <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
             },
             body: <span class="hljs-built_in">JSON</span>.stringify(user)
         })
             .then(<span class="hljs-function">(<span class="hljs-params">resp</span>) =&gt;</span> resp.json())
             .then(<span class="hljs-function">(<span class="hljs-params">{ success, error }</span>) =&gt;</span> {
                 <span class="hljs-keyword">if</span> (success) {
                     setVerify(<span class="hljs-literal">true</span>);
                 } <span class="hljs-keyword">else</span> {
                     <span class="hljs-comment">// handle incorrect login </span>
                     setError(error)
                 }
             })
     }

     <span class="hljs-keyword">return</span> (
         &lt;&gt;
             &lt;h1 className=<span class="hljs-string">"text-4xl font-bold mb-10"</span>&gt;Register&lt;/h1&gt;
             &lt;div className=<span class="hljs-string">"flex flex-col gap-5"</span>&gt;
                 &lt;Field title={<span class="hljs-string">'Name'</span>} value={name} onChange={setName} /&gt;
                 &lt;Field value={email} onChange={setEmail}
                     title={<span class="hljs-string">'Email'</span>} placeholder=<span class="hljs-string">"example@mail.com"</span> <span class="hljs-keyword">type</span>=<span class="hljs-string">"email"</span> /&gt;
                 &lt;Field value={password} onChange={setPassword}
                     title={<span class="hljs-string">'Password'</span>} <span class="hljs-keyword">type</span>=<span class="hljs-string">"password"</span> /&gt;
                 &lt;Field value={repassword} onChange={setRepassword}
                     title={<span class="hljs-string">'Retype Password'</span>} <span class="hljs-keyword">type</span>=<span class="hljs-string">"password"</span> /&gt;
             &lt;/div&gt;
             &lt;Button text={<span class="hljs-string">"Register"</span>} onClick={register} /&gt;
             &lt;ButtonText text={<span class="hljs-string">"Login instead"</span>} onClick={<span class="hljs-function">() =&gt;</span> setLogin(<span class="hljs-literal">true</span>)} /&gt;
         &lt;/&gt;
     )
 }

 <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Login</span>(<span class="hljs-params">{ setLogin, setVerify }: { setLogin: <span class="hljs-built_in">Function</span> , setVerify: <span class="hljs-built_in">Function</span> }</span>) </span>{
     <span class="hljs-keyword">const</span> router = useRouter()

     <span class="hljs-keyword">const</span> [email, setEmail] = useState(<span class="hljs-string">""</span>)
     <span class="hljs-keyword">const</span> [password, setPassword] = useState(<span class="hljs-string">""</span>)
     <span class="hljs-keyword">const</span> [error, setError] = useState(<span class="hljs-string">""</span>)

     <span class="hljs-keyword">const</span> login = <span class="hljs-function">() =&gt;</span> {
         <span class="hljs-keyword">const</span> user = {
             email,
             password
         }

         fetch(<span class="hljs-string">"http://localhost:3001/login"</span>, {
             method: <span class="hljs-string">"POST"</span>,
             headers: {
                 <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
             },
             body: <span class="hljs-built_in">JSON</span>.stringify(user)
         })
             .then(<span class="hljs-function">(<span class="hljs-params">resp</span>) =&gt;</span> resp.json())
             .then(<span class="hljs-function">(<span class="hljs-params">{ success, isVerified, error }</span>) =&gt;</span> {
                 <span class="hljs-keyword">if</span> (success) {
                     <span class="hljs-keyword">if</span> (isVerified) {
                         router.push(<span class="hljs-string">"/dashboard"</span>)
                     }
                     setVerify(<span class="hljs-literal">true</span>);
                 } <span class="hljs-keyword">else</span> {
                     <span class="hljs-comment">// handle incorrect login </span>
                     setError(error)
                 }
             })
     }

     <span class="hljs-keyword">return</span> (
         &lt;&gt;
             &lt;h1 className=<span class="hljs-string">"text-4xl font-bold mb-10"</span>&gt;Login&lt;/h1&gt;
             &lt;div className=<span class="hljs-string">"flex flex-col gap-5"</span>&gt;
                 &lt;Field value={email} onChange={setEmail}
                     title={<span class="hljs-string">'Email'</span>} placeholder=<span class="hljs-string">"example@mail.com"</span> <span class="hljs-keyword">type</span>=<span class="hljs-string">"email"</span> /&gt;
                 &lt;Field value={password} onChange={setPassword}
                     title={<span class="hljs-string">'Password'</span>} <span class="hljs-keyword">type</span>=<span class="hljs-string">"password"</span> /&gt;
             &lt;/div&gt;
             {error === <span class="hljs-string">""</span> ? &lt;&gt;&lt;<span class="hljs-regexp">/&gt; : &lt;p className="my-2 text-red-800"&gt;{error}&lt;/</span>p&gt;}
             &lt;Button text={<span class="hljs-string">"Login"</span>} onClick={<span class="hljs-function">() =&gt;</span> { login() }} /&gt;
             &lt;ButtonText text={<span class="hljs-string">"Register instead"</span>} onClick={<span class="hljs-function">() =&gt;</span> setLogin(<span class="hljs-literal">false</span>)} /&gt;
         &lt;/&gt;
     )
 }

 <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Verify</span>(<span class="hljs-params">{ email }: {email: <span class="hljs-built_in">string</span>}</span>) </span>{

     <span class="hljs-keyword">const</span> router = useRouter()
     <span class="hljs-keyword">const</span> [code, setCode] = useState(<span class="hljs-string">""</span>)
     <span class="hljs-keyword">const</span> [error, setError] = useState(<span class="hljs-string">""</span>)

     <span class="hljs-keyword">const</span> verify = <span class="hljs-function">() =&gt;</span> {
         fetch(<span class="hljs-string">`http://localhost:3001/verify?email=<span class="hljs-subst">${email}</span>&amp;code=<span class="hljs-subst">${code}</span>`</span>, {
             method: <span class="hljs-string">"POST"</span>,
             headers: {
                 <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
             },
         })
             .then(<span class="hljs-function">(<span class="hljs-params">resp</span>) =&gt;</span> resp.json())
             .then(<span class="hljs-function">(<span class="hljs-params">{ success, error }</span>) =&gt;</span> {
                 <span class="hljs-keyword">if</span> (success) {
                     router.push(<span class="hljs-string">"/dashboard"</span>)
                 } <span class="hljs-keyword">else</span> {
                     <span class="hljs-comment">// handle incorrect login </span>
                     setError(error)
                 }
             })
     }

     <span class="hljs-keyword">return</span> (
         &lt;div className=<span class="hljs-string">"flex flex-col gap-5 items-center justify-center"</span>&gt;
             &lt;h1 className=<span class="hljs-string">"uppercase text-xl"</span> &gt;Verify your email address&lt;/h1&gt;
             &lt;div className=<span class="hljs-string">"w-1/2 text-center"</span>&gt;
             &lt;p className=<span class="hljs-string">""</span>&gt;A verification code and link has been sent to *****@****mail.com&lt;/p&gt;
             &lt;/div&gt;
             &lt;p&gt;Please check your inbox or junk folder and proceed to verify <span class="hljs-keyword">with</span> us. The code will expire <span class="hljs-keyword">in</span> <span class="hljs-number">2</span>mins&lt;/p&gt;
             &lt;Field title=<span class="hljs-string">"Code"</span> value={code} onChange={setCode} /&gt;
             &lt;Button text={<span class="hljs-string">"Verify"</span>} onClick={<span class="hljs-function">() =&gt;</span> {verify()}} /&gt;
         &lt;/div&gt;
     )
 }
</code></pre>
</li>
<li><p>Lastly, we create the dashboard route when the user successfully logins and also the verify route when the user clicks on the verify link which will prompt them to return to the homepage to log in again.</p>
<pre><code class="lang-typescript"> <span class="hljs-comment">// in /app/dashboard/page.tsx</span>
 <span class="hljs-string">"use client"</span>
 <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Page</span>(<span class="hljs-params"></span>) </span>{
     <span class="hljs-keyword">return</span> (
         &lt;div className=<span class="hljs-string">"min-h-screen flex flex-col items-center justify-center"</span>&gt;
             &lt;h1 className=<span class="hljs-string">"text-3xl mb-3"</span>&gt;This is a dashboard viewable when logged <span class="hljs-keyword">in</span>&lt;/h1&gt;
             &lt;h2&gt;Return to root page using button below&lt;/h2&gt;
             &lt;Link href=<span class="hljs-string">"/"</span>&gt;
                 &lt;Button text=<span class="hljs-string">"Return"</span>/&gt;
             &lt;/Link&gt;
         &lt;/div&gt;
     )
 }
</code></pre>
<pre><code class="lang-typescript"> <span class="hljs-comment">// in /app/verify/page.tsx</span>
 <span class="hljs-string">"use client"</span>
 <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Page</span>(<span class="hljs-params"></span>) </span>{
     <span class="hljs-keyword">return</span> (
         &lt;div className=<span class="hljs-string">"min-h-screen flex flex-col items-center justify-center"</span>&gt;
             &lt;h1 className=<span class="hljs-string">"text-3xl mb-3"</span>&gt;You are now verified!&lt;/h1&gt;
             &lt;h2&gt;Click below to <span class="hljs-keyword">return</span> back to the homepage and login&lt;/h2&gt;
             &lt;Link href=<span class="hljs-string">"/"</span>&gt;
                 &lt;Button text=<span class="hljs-string">"Return"</span>/&gt;
             &lt;/Link&gt;
         &lt;/div&gt;
     )
 }
</code></pre>
</li>
</ol>
<h2 id="heading-testing-our-app">Testing our app</h2>
<p>To start our Go server, run</p>
<pre><code class="lang-bash">go run .
</code></pre>
<blockquote>
<p>ensure that you have the <code>.env</code> file in the root of the backend folder. Be sure not to commit this into the git repository as the keys are private and should be kept hidden.</p>
<pre><code class="lang-bash">// .env file
MONGODB_URI=<span class="hljs-string">"mongodb+srv://&lt;USER&gt;:&lt;PASSWORD&gt;@cluster23.honxmor.mongodb.net/?retryWrites=true&amp;w=majority"</span>
HOSTEMAIL=<span class="hljs-string">"XXXXX@gmail.com"</span>
APPKEY=<span class="hljs-string">"ABCDEFGH"</span>
</code></pre>
</blockquote>
<p>To start our Next.js frontend, run:</p>
<pre><code class="lang-bash">yarn dev
</code></pre>
<p>Our backend will be running in <code>localhost:3001</code> while our frontend will be running in <code>localhost:3000</code>.</p>
<p>If we try to register:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684307369470/78d34fdb-190f-4f68-8c53-b991e433efdf.png" alt class="image--center mx-auto" /></p>
<p>We will have a user entry created in MongoDB Atlas</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684307429435/4ce305ad-538c-4e9e-89a0-00309e604098.png" alt class="image--center mx-auto" /></p>
<p>and also received my verification email</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684307479174/5eb6e272-e458-4b97-8363-7d6aa796560e.png" alt class="image--center mx-auto" /></p>
<p>which we can proceed to click the link or fill up the verification code on our page</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684307536382/b2aaa2b8-a27c-48a3-a266-ce27bdf4f8c4.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-summary">Summary</h2>
<p>We have created an application that can send verification emails for users to verify.</p>
<p>In the future, I will try to optimize our backend code so that the frontend side can receive the response from the server faster.</p>
]]></content:encoded></item><item><title><![CDATA[IOT Home application Part 3]]></title><description><![CDATA[In our post today, we will be adding the sqlite3 database to our application and creating a few more API endpoints for us to add and remove rooms and devices.

SQLite3 should come installed with most Linux distributions but if it is not installed, yo...]]></description><link>https://blog.wongandre.com/iot-home-application-part-3</link><guid isPermaLink="true">https://blog.wongandre.com/iot-home-application-part-3</guid><category><![CDATA[golang]]></category><category><![CDATA[gin-gonic]]></category><category><![CDATA[SQLite]]></category><dc:creator><![CDATA[Andre Wong]]></dc:creator><pubDate>Sat, 06 May 2023 14:46:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1683384139889/c2aa91b4-caff-482d-9bc4-e48dfb221454.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In our post today, we will be adding the sqlite3 database to our application and creating a few more API endpoints for us to add and remove rooms and devices.</p>
<blockquote>
<p>SQLite3 should come installed with most Linux distributions but if it is not installed, you can run <code>sudo apt install sqlite3</code></p>
</blockquote>
<p>Below is the updated file structure we are going to modify today:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1683384262487/48b141f3-fad1-4e11-a22e-209e2ef7c61a.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-api-endpoints">API endpoints</h2>
<p>The first step is to create a room for the home. Rooms can be any part of the room in the house, for example, bedroom, living room or kitchen. Once our rooms are created, we can start associating the rooms with the devices we want to connect.</p>
<p>Our devices can then be tied to the rooms we created and be listed on our front end.</p>
<p>The table below shows the different API routes we will be creating and their description.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Endpoint</td><td>HTTP method</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td>/create-room</td><td>POST</td><td>creates a room</td></tr>
<tr>
<td>/rooms</td><td>GET</td><td>show all available rooms</td></tr>
<tr>
<td>/delete-rooms/:roomname</td><td>DELETE</td><td>deletes a room named by <code>:roomname</code></td></tr>
<tr>
<td></td><td></td><td></td></tr>
<tr>
<td>/:roomname/add-device</td><td>POST</td><td>add a device to the room</td></tr>
<tr>
<td>/:roomname/devices</td><td>GET</td><td>show all devices in the room</td></tr>
<tr>
<td>/:roomsname/:ip</td><td>GET</td><td>show device information associated to its <code>:ip</code></td></tr>
<tr>
<td></td><td></td><td></td></tr>
<tr>
<td>/:roomname/:ip/delete-device</td><td>DELETE</td><td>delete a device from the room</td></tr>
<tr>
<td>/:roomname/:ip/:toggle</td><td>POST</td><td>switches the device on and off</td></tr>
</tbody>
</table>
</div><p>Some of our routes have parameters in their paths as denoted by the <code>:</code> in front of the names. The handler will match the routes based on the parameters and map them into values. So a route such as <code>/bedroom/devices</code> will match with the <code>/:roomname/devices</code> and <code>roomname == 'bedroom'</code>.</p>
<p>We can then translate this into code and add them to our <code>router.go</code> file</p>
<pre><code class="lang-go"><span class="hljs-comment">// router.go</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">InitRouter</span><span class="hljs-params">()</span> *<span class="hljs-title">gin</span>.<span class="hljs-title">Engine</span></span> {
    r := gin.Default()

    r.GET(<span class="hljs-string">"/"</span>, getServerStatus)


    r.POST(<span class="hljs-string">"/create-room"</span>, createRoom)
    r.GET(<span class="hljs-string">"/rooms"</span>, getRooms)
    r.DELETE(<span class="hljs-string">"/delete-room/:roomname"</span>, deleteRoom)

    r.POST(<span class="hljs-string">"/:roomname/add-device"</span>, addDevice)
    r.GET(<span class="hljs-string">"/:roomname/devices"</span>, showDevices)

    r.GET(<span class="hljs-string">"/:roomname/:ip"</span>, getDeviceInfo)
    r.DELETE(<span class="hljs-string">"/:roomname/:ip/delete-device"</span>, deleteDevice)
    r.POST(<span class="hljs-string">"/:roomname/:ip/:toggle"</span>, toggleDevice)

    <span class="hljs-keyword">return</span> r
}
</code></pre>
<h2 id="heading-creating-golang-struct">creating golang struct</h2>
<p>To facilitate the loading of data from our routes as well as our database, we will create golang structs that encapsulate our fields of data together. Under the models folder, we can add the following structs for our room and devices.</p>
<pre><code class="lang-go"><span class="hljs-comment">// models.go</span>

<span class="hljs-keyword">package</span> models

<span class="hljs-keyword">type</span> RoomInfo <span class="hljs-keyword">struct</span> {
    Name  <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"name"`</span>
    Count <span class="hljs-keyword">int</span>    <span class="hljs-string">`json:"count"`</span>
}

<span class="hljs-keyword">type</span> DeviceType <span class="hljs-keyword">string</span>

<span class="hljs-keyword">const</span> (
    Wled   DeviceType = <span class="hljs-string">"wled"</span>
    Switch DeviceType = <span class="hljs-string">"switch"</span>
)

<span class="hljs-keyword">type</span> RegisteredDevice <span class="hljs-keyword">struct</span> {
    Hostname <span class="hljs-keyword">string</span>     <span class="hljs-string">`json:"hostname"`</span>
    Ipaddr   <span class="hljs-keyword">string</span>     <span class="hljs-string">`json:"ipaddr"`</span>
    Name     <span class="hljs-keyword">string</span>     <span class="hljs-string">`json:"name"`</span>
    Type     DeviceType <span class="hljs-string">`json:"type"`</span>
}

<span class="hljs-keyword">type</span> DeviceStatus <span class="hljs-keyword">struct</span> {
    Connected <span class="hljs-keyword">bool</span> <span class="hljs-string">`json:"connected"`</span>
    On_state  <span class="hljs-keyword">bool</span> <span class="hljs-string">`json:"on_state"`</span>
}
</code></pre>
<blockquote>
<p>All the fields here have a corresponding JSON tag, which specifies how the field should be encoded in JSON.</p>
<p>Type names and Fields are capitalized to indicate that the identifier is public</p>
</blockquote>
<p>For <code>RoomInfo</code>, we have a <code>Name</code> field that is unique and the <code>Count</code> field, which we will use to keep track of the number of devices in the room.</p>
<p>To keep track of the device, we have the <code>RegisteredDevice</code> type that has fields about the information of the device such as the name, IP address and the device type. <code>DeviceType</code> is defined as a string type that refers to our constant values <code>wled</code> and <code>switch</code>, which represents the different device types we support.</p>
<p>Our <code>DeviceStatus</code> helps to maintain information about the state of the device. Currently, we have <code>Connected</code> field to check if the device is operational and <code>On_state</code> field to check if the switch is on or off.</p>
<h2 id="heading-setting-up-our-database">setting up our database</h2>
<p>We will be using the go-sqlite3 package to help us execute SQL queries to our SQLite3 database.</p>
<pre><code class="lang-bash">go get github.com/mattn/go-sqlite3
</code></pre>
<p>First, let's create our <code>InitDatabase</code> function that runs every time we start our backend server.</p>
<pre><code class="lang-go"><span class="hljs-comment">// database.go</span>
<span class="hljs-keyword">package</span> database

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"database/sql"</span>
    _ <span class="hljs-string">"github.com/mattn/go-sqlite3"</span>
)

<span class="hljs-keyword">const</span> databaseFilePath = <span class="hljs-string">"iothome.db"</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">InitDatabase</span><span class="hljs-params">()</span> *<span class="hljs-title">sql</span>.<span class="hljs-title">DB</span></span> {
    db, err := sql.Open(<span class="hljs-string">"sqlite3"</span>, databaseFilePath)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Println(<span class="hljs-string">"Error opening database"</span>)
    }

    <span class="hljs-keyword">if</span> _, err := db.Exec(<span class="hljs-string">"PRAGMA foreign_keys = 1"</span>); err != <span class="hljs-literal">nil</span> {
        log.Println(<span class="hljs-string">"Error setting command"</span>)
    }

    <span class="hljs-keyword">return</span> db
}
</code></pre>
<p>Using <code>sql.Open</code>, we can try to open our SQL database using the name that we have given. If no such name exists, then our database driver will initialize a new database file for us.</p>
<p>We then execute the SQL command <code>PRAGMA foreign_keys = 1</code> to enable foreign key constraints in our database. By default, foreign key constraints are disabled. Foreign key constraints are used to enforce referential integrity, which we will be utilizing in our tables later on.</p>
<h2 id="heading-sql-create-tables">SQL create tables</h2>
<p>Next, we create our database schema for containing our data. We have three different tables, <code>rooms</code>, <code>devInfo</code> and <code>devStatus</code> each with its own headers. In the <code>devInfo</code> table, we have our <code>room_id</code> column that references the <code>room_id</code> column in the <code>rooms</code> table. This means that each device info entry has a room_id column which can be referenced to the <code>rooms</code> table. This is similar to the <code>device_id</code> in <code>devInfo</code> and <code>devStatus</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682782694486/4c7d3c25-27a1-4c86-aad7-ffaf23caa1ce.png" alt class="image--center mx-auto" /></p>
<p>Let's translate this into SQL commands to create our table, as shown below:</p>
<pre><code class="lang-go"><span class="hljs-keyword">const</span> (
    createRooms <span class="hljs-keyword">string</span> = <span class="hljs-string">`
        CREATE TABLE IF NOT EXISTS rooms (
            room_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL UNIQUE
        )
    `</span>

    createDevInfo <span class="hljs-keyword">string</span> = <span class="hljs-string">`
        CREATE TABLE IF NOT EXISTS deviceInfo (
            room_id INTEGER NOT NULL,
            device_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            ipaddr TEXT NOT NULL,
            type TEXT NOT NULL,
            hostname TEXT,
            macaddress TEXT,
            FOREIGN KEY (room_id)
                REFERENCES rooms (room_id)
                    ON DELETE CASCADE
        )
    `</span>

    createDevStatus <span class="hljs-keyword">string</span> = <span class="hljs-string">`
        CREATE TABLE IF NOT EXISTS deviceStatus (
            device_status_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
            device_id INTEGER NOT NULL,
            connected INTEGER NOT NULL,
            on_state INTEGER NOT NULL,
            FOREIGN KEY (device_id)
                REFERENCES deviceInfo (device_id)
                    ON DELETE CASCADE
        )
    `</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">InitDatabase</span><span class="hljs-params">()</span> *<span class="hljs-title">sql</span>.<span class="hljs-title">DB</span></span> {
    <span class="hljs-comment">//  ... </span>
    <span class="hljs-keyword">if</span> _, err := db.Exec(createRooms); err != <span class="hljs-literal">nil</span> {
        log.Println(<span class="hljs-string">"failed to create new rooms table"</span>)
    }

    <span class="hljs-keyword">if</span> _, err := db.Exec(createDevInfo); err != <span class="hljs-literal">nil</span> {
        log.Println(<span class="hljs-string">"failed to create new devinfo table"</span>)
    }

    <span class="hljs-keyword">if</span> _, err := db.Exec(createDevStatus); err != <span class="hljs-literal">nil</span> {
        log.Println(<span class="hljs-string">"failed to create new devstatus table"</span>)
    }
    <span class="hljs-comment">//  ...</span>
}
</code></pre>
<p>We use <code>CREATE TABLE IF NOT EXISTS</code> to initialize our database as it will create the table if the table does not currently exist.</p>
<p><code>NOT NULL</code> is used here to ensure that the important columns we are storing have a value to them and is not empty.</p>
<p><code>AUTOINCREMENT</code> keyword is used to create a unique identifier for each of our inserted rows in the table. It automatically generates an integer starting from 1.</p>
<p><code>ON DELETE CASCASE</code> keywords indicate that when a row in <code>deviceInfo</code> is deleted, all the rows in the <code>deviceStatus</code> table that references the deleted row via the <code>device_id</code> foreign key should also be deleted, which makes sense if we want to delete a particular device from the table.</p>
<p><code>UNIQUE</code> keyword is used here to ensure that our name column will not have any two rows with the same values.</p>
<h2 id="heading-initializing-database">initializing database</h2>
<p>let's create a struct <code>DatabaseManager</code> that holds a pointer to the database that we initialized in the previous section. In our <code>database.go</code> file, we will declare <code>Dbman</code> that holds a pointer to the <code>DatabaseManager</code>. This is so that other files can reference <code>Dbman</code> to execute the SQL commands.</p>
<pre><code class="lang-go"><span class="hljs-comment">// database.go</span>

<span class="hljs-keyword">type</span> DatabaseManager <span class="hljs-keyword">struct</span> {
    Db *sql.DB
}

<span class="hljs-keyword">var</span> Dbman *DatabaseManager

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">InitializeGlobals</span><span class="hljs-params">(db *sql.DB)</span></span> {
    Dbman = &amp;DatabaseManager{
        Db: db,
    }
}
</code></pre>
<h2 id="heading-sql-statements">SQL statements</h2>
<p>In <code>database.go</code>, we also declare our SQL commands for inserting, selecting, deleting and updating our entries in the database. The <code>?</code> here denotes the data that will be replaced when we execute these commands below in our server.</p>
<pre><code class="lang-go"><span class="hljs-comment">// database.go</span>

<span class="hljs-keyword">const</span> (
    insertNewRoom <span class="hljs-keyword">string</span> = <span class="hljs-string">`INSERT INTO rooms (name) VALUES (?)`</span>
    getRooms <span class="hljs-keyword">string</span> = <span class="hljs-string">`SELECT rooms.name, count(deviceInfo.room_id) FROM rooms LEFT JOIN deviceInfo ON rooms.room_id = deviceInfo.room_id GROUP BY rooms.name`</span>
    getRoomId     <span class="hljs-keyword">string</span> = <span class="hljs-string">`SELECT room_id FROM rooms WHERE name=?`</span>

    insertNewDevInfo <span class="hljs-keyword">string</span> = <span class="hljs-string">`INSERT INTO deviceInfo (room_id, name, ipaddr, type) VALUES (?, ? ,? ,?)`</span>
    insertNewDevStatus <span class="hljs-keyword">string</span> = <span class="hljs-string">`INSERT INTO deviceStatus (device_id, connected, on_state) VALUES (?, ? ,?)`</span>

    getDeviceInfoByRoom <span class="hljs-keyword">string</span> = <span class="hljs-string">`SELECT dev.name, dev.ipaddr, dev.type, dev.connected, dev.on_state FROM rooms JOIN (SELECT * FROM deviceInfo JOIN deviceStatus WHERE deviceInfo.device_id = deviceStatus.device_id) as dev WHERE rooms.room_id = dev.room_id and rooms.name=?`</span>

    getDeviceInfo <span class="hljs-keyword">string</span> = <span class="hljs-string">`SELECT dev.device_id, dev.name, dev.type, dev.connected, dev.on_state FROM rooms JOIN (SELECT * FROM deviceInfo JOIN deviceStatus WHERE deviceInfo.device_id = deviceStatus.device_id) as dev WHERE rooms.room_id = dev.room_id and rooms.name=? and dev.ipaddr=?`</span>

    updateDeviceStatus <span class="hljs-keyword">string</span> = <span class="hljs-string">`UPDATE deviceStatus SET connected=?, on_state=? WHERE device_id=?`</span>

    deleteRoom <span class="hljs-keyword">string</span> = <span class="hljs-string">`DELETE FROM rooms WHERE name=?`</span>
    deleteDevice <span class="hljs-keyword">string</span> = <span class="hljs-string">`DELETE FROM deviceInfo WHERE ipaddr IN (
        SELECT ipaddr FROM rooms JOIN deviceInfo ON rooms.room_id=deviceInfo.room_id WHERE rooms.name=? and deviceInfo.ipaddr=?
        )
    `</span>
)
</code></pre>
<p>After that, we create the corresponding go functions to execute the SQL commands.</p>
<p>For example, create a <code>AddRoom</code> function below, we have a receiver parameter denoted by the <code>(s *DatabaseManager)</code> which allows one to associate a particular function, AddRoom, with a specific type, in this case, the DatabaseManager.</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *DatabaseManager)</span> <span class="hljs-title">AddRoom</span><span class="hljs-params">(room models.RoomInfo)</span> <span class="hljs-title">error</span></span> {
    _, err := s.Db.Exec(insertNewRoom, room.Name)

    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Println(<span class="hljs-string">"insertNewRoom query failed"</span>)
        <span class="hljs-keyword">return</span> err
    }

    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}
</code></pre>
<p>We call <code>s.Db.Exec</code> to execute our <code>insertNewRoom</code> command and also the name of our room. We can then check if the insertion is successful by checking if the <code>err</code> is nil.</p>
<p>To query our database, we can call <code>.Query</code>, which will return to us all the rows that satisfy our conditions. We need to call <code>rows.Close()</code> after reading so that we prevent memory leaks. We can read through all the rows by looping through <code>rows.Next()</code> and then call <code>rows.Scan(&lt;column names queries&gt;)</code>. Below is an example for querying all the rooms we have.</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *DatabaseManager)</span> <span class="hljs-title">GetRooms</span><span class="hljs-params">()</span> <span class="hljs-params">([]models.RoomInfo, error)</span></span> {
    rows, err := s.Db.Query(getRooms)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Println(<span class="hljs-string">"getRooms query failed"</span>)
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
    }
    <span class="hljs-keyword">defer</span> rows.Close()

    rooms := []models.RoomInfo{}
    <span class="hljs-keyword">for</span> rows.Next() {
        ri := models.RoomInfo{}
        err = rows.Scan(&amp;ri.Name, &amp;ri.Count)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            log.Println(<span class="hljs-string">"failed to scan db rows"</span>)
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
        }
        rooms = <span class="hljs-built_in">append</span>(rooms, ri)
    }

    <span class="hljs-keyword">return</span> rooms, <span class="hljs-literal">nil</span>
}
</code></pre>
<h2 id="heading-testing-our-application">testing our application</h2>
<p>To test the code that we had just written, let's write a function to populate our database:</p>
<pre><code class="lang-go"><span class="hljs-comment">// database.go</span>

<span class="hljs-comment">// for testing purposes</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">PopulateDatabase</span><span class="hljs-params">()</span></span> {

    Dbman.AddRoom(models.RoomInfo{Name: <span class="hljs-string">"livingroom"</span>})
    Dbman.AddRoom(models.RoomInfo{Name: <span class="hljs-string">"bedroom1"</span>})
    Dbman.AddDevice(
        models.RegisteredDevice{Name: <span class="hljs-string">"andre"</span>, Type: <span class="hljs-string">"wled"</span>, Ipaddr: <span class="hljs-string">"192.168.1.1"</span>, Hostname: <span class="hljs-string">"123"</span>},
        models.DeviceStatus{Connected: <span class="hljs-literal">false</span>, On_state: <span class="hljs-literal">false</span>},
        <span class="hljs-string">"bedroom1"</span>,
    )
    Dbman.AddDevice(
        models.RegisteredDevice{Name: <span class="hljs-string">"betty"</span>, Type: <span class="hljs-string">"switch"</span>, Ipaddr: <span class="hljs-string">"192.168.1.2"</span>, Hostname: <span class="hljs-string">"123"</span>},
        models.DeviceStatus{Connected: <span class="hljs-literal">false</span>, On_state: <span class="hljs-literal">false</span>},
        <span class="hljs-string">"bedroom1"</span>,
    )
    Dbman.AddDevice(
        models.RegisteredDevice{Name: <span class="hljs-string">"cathy"</span>, Type: <span class="hljs-string">"wled"</span>, Ipaddr: <span class="hljs-string">"192.168.1.3"</span>, Hostname: <span class="hljs-string">"123"</span>},
        models.DeviceStatus{Connected: <span class="hljs-literal">false</span>, On_state: <span class="hljs-literal">false</span>},
        <span class="hljs-string">"bedroom1"</span>,
    )
    Dbman.AddDevice(
        models.RegisteredDevice{Name: <span class="hljs-string">"derick"</span>, Type: <span class="hljs-string">"switch"</span>, Ipaddr: <span class="hljs-string">"192.168.1.4"</span>, Hostname: <span class="hljs-string">"123"</span>},
        models.DeviceStatus{Connected: <span class="hljs-literal">false</span>, On_state: <span class="hljs-literal">false</span>},
        <span class="hljs-string">"livingroom"</span>,
    )
    Dbman.AddDevice(
        models.RegisteredDevice{Name: <span class="hljs-string">"eagle"</span>, Type: <span class="hljs-string">"wled"</span>, Ipaddr: <span class="hljs-string">"192.168.1.5"</span>, Hostname: <span class="hljs-string">"123"</span>},
        models.DeviceStatus{Connected: <span class="hljs-literal">false</span>, On_state: <span class="hljs-literal">false</span>},
        <span class="hljs-string">"livingroom"</span>,
    )
}
</code></pre>
<p>In our main, we can then initialize our database and call <code>PopulateDatabase</code>.</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    db := database.InitDatabase()
    database.InitializeGlobals(db)

    database.PopulateDatabase()

    <span class="hljs-comment">//...</span>
}
</code></pre>
<p>After running our <code>main.go</code> once, we can look into our database and check if it is correctly populated.</p>
<pre><code class="lang-bash">$ sqlite3 iothome.db
sqlite3&gt; .table
// deviceInfo deviceStatus rooms

sqlite3&gt; select * from rooms;
// 1|livingroom
// 2|bedroom1

sqlite3&gt; select * from deviceInfo;
// room_id|device_id|name|ipaddr|<span class="hljs-built_in">type</span>|hostname|macaddress
// 2|1|andre|192.168.1.1|wled||
// 2|2|betty|192.168.1.2|switch||
// 2|3|cathy|192.168.1.3|wled||
// 1|4|derick|192.168.1.4|switch||
// 1|5|eagle|192.168.1.5|wled||

sqlite3&gt; select * from deviceStatus;
// device_status_id|device_id|connected|on_state
// 1|1|0|0
// 2|2|0|0
// 3|3|0|0
// 4|4|0|0
// 5|5|0|0

sqlite3&gt; .<span class="hljs-built_in">exit</span>
</code></pre>
<blockquote>
<p>In SQLite3, the boolean is represented by integers. A 0 means false and 1 means true.</p>
</blockquote>
<p>In the next part, we will use our DatabaseManager in our Gin routes and merge them.</p>
<hr />
<p>Below is the full implementation of all our database receiver functions for the <code>DatabaseManager</code> type.</p>
<pre><code class="lang-go"><span class="hljs-comment">// database.go</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *DatabaseManager)</span> <span class="hljs-title">AddRoom</span><span class="hljs-params">(room models.RoomInfo)</span> <span class="hljs-title">error</span></span> {
    _, err := s.Db.Exec(insertNewRoom, room.Name)

    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Println(<span class="hljs-string">"insertNewRoom query failed"</span>)
        <span class="hljs-keyword">return</span> err
    }

    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *DatabaseManager)</span> <span class="hljs-title">DelRoom</span><span class="hljs-params">(roomname <span class="hljs-keyword">string</span>)</span> <span class="hljs-title">error</span></span> {
    _, err := s.Db.Exec(deleteRoom, roomname)

    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Println(<span class="hljs-string">"deleteRoom query failed"</span>)
        <span class="hljs-keyword">return</span> err
    }

    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *DatabaseManager)</span> <span class="hljs-title">DelDevice</span><span class="hljs-params">(roomname <span class="hljs-keyword">string</span>, ipAddr <span class="hljs-keyword">string</span>)</span> <span class="hljs-title">error</span></span> {
    _, err := s.Db.Exec(deleteDevice, roomname, ipAddr)

    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Println(<span class="hljs-string">"deleteDevice query failed"</span>)
        <span class="hljs-keyword">return</span> err
    }

    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *DatabaseManager)</span> <span class="hljs-title">AddDevice</span><span class="hljs-params">(dev models.RegisteredDevice, devStatus models.DeviceStatus, roomName <span class="hljs-keyword">string</span>)</span> <span class="hljs-title">error</span></span> {
    row := s.Db.QueryRow(getRoomId, roomName)
    <span class="hljs-keyword">var</span> roomId <span class="hljs-keyword">int</span>
    err := row.Scan(&amp;roomId)
    <span class="hljs-keyword">if</span> err == sql.ErrNoRows {
        <span class="hljs-keyword">return</span> err
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Println(<span class="hljs-string">"getroomId query failed"</span>)
        <span class="hljs-keyword">return</span> err
    }

    res, err := s.Db.Exec(insertNewDevInfo, roomId, dev.Name, dev.Ipaddr, dev.Type)

    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Println(<span class="hljs-string">"inserting entry into deviceInfo table failed"</span>)
        <span class="hljs-keyword">return</span> err
    }

    <span class="hljs-keyword">var</span> devId <span class="hljs-keyword">int64</span>
    devId, _ = res.LastInsertId()

    _, err = s.Db.Exec(insertNewDevStatus, devId, devStatus.Connected, devStatus.On_state)

    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Println(<span class="hljs-string">"inserting entry into deviceStatus table"</span>)
        <span class="hljs-keyword">return</span> err
    }

    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *DatabaseManager)</span> <span class="hljs-title">UpdateDevStatus</span><span class="hljs-params">(roomName <span class="hljs-keyword">string</span>, ipAddr <span class="hljs-keyword">string</span>, devStatus models.DeviceStatus)</span> <span class="hljs-title">error</span></span> {
    device_id, _, _, err := Dbman.GetDevice(roomName, ipAddr)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }

    _, err = s.Db.Exec(updateDeviceStatus, devStatus.Connected, devStatus.On_state, device_id)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }

    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *DatabaseManager)</span> <span class="hljs-title">GetDevice</span><span class="hljs-params">(roomName <span class="hljs-keyword">string</span>, ipAddr <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(<span class="hljs-keyword">int</span>, models.RegisteredDevice, models.DeviceStatus, error)</span></span> {
    row := s.Db.QueryRow(getDeviceInfo, roomName, ipAddr)

    devInfo := models.RegisteredDevice{}
    devStatus := models.DeviceStatus{}
    <span class="hljs-keyword">var</span> device_id <span class="hljs-keyword">int</span>
    err := row.Scan(&amp;device_id, &amp;devInfo.Name, &amp;devInfo.Type, &amp;devStatus.Connected, &amp;devStatus.On_state)
    <span class="hljs-keyword">if</span> err == sql.ErrNoRows {
        <span class="hljs-keyword">return</span> device_id, devInfo, devStatus, err
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Println(<span class="hljs-string">"failed to scan db rows"</span>)
        <span class="hljs-keyword">return</span> device_id, devInfo, devStatus, err
    }

    devInfo.Ipaddr = ipAddr

    <span class="hljs-keyword">return</span> device_id, devInfo, devStatus, <span class="hljs-literal">nil</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *DatabaseManager)</span> <span class="hljs-title">GetDevices</span><span class="hljs-params">(roomName <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">([]models.RegisteredDevice, <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]models.DeviceStatus, error)</span></span> {
    rows, err := s.Db.Query(getDeviceInfoByRoom, roomName)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Println(<span class="hljs-string">"getDeviceInfoByRoom query failed"</span>) 
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, <span class="hljs-literal">nil</span>, err
    }
    <span class="hljs-keyword">defer</span> rows.Close()

    devList := []models.RegisteredDevice{}
    devStatus := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]models.DeviceStatus)
    <span class="hljs-keyword">for</span> rows.Next() {
        rd := models.RegisteredDevice{}
        ds := models.DeviceStatus{}
        err = rows.Scan(&amp;rd.Name, &amp;rd.Ipaddr, &amp;rd.Type, &amp;ds.Connected, &amp;ds.On_state)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            log.Println(<span class="hljs-string">"failed to scan db rows"</span>)
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, <span class="hljs-literal">nil</span>, err
        }

        devList = <span class="hljs-built_in">append</span>(devList, rd)
        devStatus[rd.Ipaddr] = ds
    }

    <span class="hljs-keyword">return</span> devList, devStatus, <span class="hljs-literal">nil</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *DatabaseManager)</span> <span class="hljs-title">GetRooms</span><span class="hljs-params">()</span> <span class="hljs-params">([]models.RoomInfo, error)</span></span> {
    rows, err := s.Db.Query(getRooms)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Println(<span class="hljs-string">"getRooms query failed"</span>)
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
    }
    <span class="hljs-keyword">defer</span> rows.Close()

    rooms := []models.RoomInfo{}
    <span class="hljs-keyword">for</span> rows.Next() {

        ri := models.RoomInfo{}
        err = rows.Scan(&amp;ri.Name, &amp;ri.Count)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            log.Println(<span class="hljs-string">"failed to scan db rows"</span>)
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
        }

        rooms = <span class="hljs-built_in">append</span>(rooms, ri)
    }

    <span class="hljs-keyword">return</span> rooms, <span class="hljs-literal">nil</span>
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[IOT Home application Part 2]]></title><description><![CDATA[setting up Go and initial files
Golang can be installed through the official website. I will be using Go version 1.19.
Once Go is installed, we can set up our backend project structure.
Create our backend folder and initialize our backend workspace w...]]></description><link>https://blog.wongandre.com/iot-home-application-part-2</link><guid isPermaLink="true">https://blog.wongandre.com/iot-home-application-part-2</guid><category><![CDATA[golang]]></category><category><![CDATA[gin-gonic]]></category><dc:creator><![CDATA[Andre Wong]]></dc:creator><pubDate>Sat, 22 Apr 2023 04:01:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1681546643170/2d74c79d-3e60-46c8-a0cb-72ea67d0bd48.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-setting-up-go-and-initial-files">setting up Go and initial files</h2>
<p>Golang can be installed through the <a target="_blank" href="https://go.dev/doc/install">official website</a>. I will be using Go version 1.19.</p>
<p>Once Go is installed, we can set up our backend project structure.</p>
<p>Create our backend folder and initialize our backend workspace with <code>go mod init</code>. This helps to track our Go dependencies.</p>
<pre><code class="lang-bash">mkdir backend
<span class="hljs-built_in">cd</span> backend

<span class="hljs-comment"># module path can be anything but best is to be the same path as repo</span>
go mod init &lt;module path&gt;
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681529172504/373d0fc9-fb16-47ea-8d04-ba2e3fa90a54.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-creating-the-gin-engine">creating the gin engine</h2>
<p>Gin is a web framework that we can create API routes for our front end to interface with. To start, we can first import the module into our Go project.</p>
<pre><code class="lang-bash">go get -u github.com/gin-gonic/gin
</code></pre>
<p>Then inside our <code>router.go</code> file, create a <code>InitRouter</code> function to initialize our router instance as <code>r</code>.</p>
<p>The router object <code>r</code> is then used to define various HTTP routes for the API. It takes two arguments, the first argument is the HTTP route in string and the second argument is the handler function that will perform a specific action to process the HTTP request. For example, <code>r.GET("/", getServerStatus)</code> maps a GET request to the root URL path to a handler function called <code>getServerStatus</code>.</p>
<p>HTTP methods used here include GET, POST, and DELETE. GET requests are commonly used to retrieve information from the server and are considered safe, as multiple calls to the server do not change its state. Therefore, HTTP GET requests are considered idempotent. In contrast, HTTP POST requests are used to create or update data on the server, potentially altering the server's state and making them non-idempotent. We should keep in mind this when implementing our handler function to ensure this holds and to use each HTTP method appropriately depending on the nature of the request.</p>
<p>The summary of the code in the <code>router.go</code> file is shown below:</p>
<pre><code class="lang-go"><span class="hljs-comment">// router.go</span>

<span class="hljs-keyword">package</span> routes

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"github.com/gin-gonic/gin"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">InitRouter</span><span class="hljs-params">()</span> *<span class="hljs-title">gin</span>.<span class="hljs-title">Engine</span></span> {
    r := gin.Default()

    r.GET(<span class="hljs-string">"/"</span>, getServerStatus)

    r.POST(<span class="hljs-string">"/create-room"</span>, createRoom)
    r.GET(<span class="hljs-string">"/rooms"</span>, getRooms)

    <span class="hljs-keyword">return</span> r
}
</code></pre>
<p>Overall, this code sets up a web server that listens for HTTP requests and routes them to the appropriate handler functions to process the requests and return responses.</p>
<p>In our <code>main.go</code> file, we can then call our <code>InitRouter</code> function and then start the server by calling <code>r.run()</code>. Using a function with names that start with an uppercase letter will cause it to be visible to other packages in your workspace. A function starting with a lowercase letter can only be used within its own package and cannot be called from another package.</p>
<pre><code class="lang-go"><span class="hljs-comment">// main.go</span>

<span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-comment">// path to my routes folder</span>
    <span class="hljs-string">"github.com/AndreWongZH/iothome/routes"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    r := routes.InitRouter()

    r.Run(<span class="hljs-string">":3001"</span>)
}
</code></pre>
<h2 id="heading-creating-handler-functions">creating handler functions</h2>
<p>Let's first implement the HTTP <code>/</code> route's handler function so that we can query if our server is online. All the handler functions take <code>*gin.Content</code> as a parameter. Calling <code>ctx.JSON</code> here will send a JSON response back to the client. If our backend server is online, we will receive a JSON response of <code>{"status": "ok"}</code> back.</p>
<pre><code class="lang-go"><span class="hljs-comment">// routes.go</span>
<span class="hljs-keyword">package</span> routes

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getServerStatus</span><span class="hljs-params">(ctx *gin.Context)</span></span> {
    ctx.JSON(http.StatusOK, gin.H{
        <span class="hljs-string">"status"</span>: <span class="hljs-string">"ok"</span>,
    })
}
</code></pre>
<p>We will then create two other handlers, <code>createRoom</code> and <code>getRooms</code> that will update our internal list <code>rooms</code> holding all the room names. For our POST route to <code>/create-room</code>, we want to pass the name of our room as a JSON payload in our HTTP request. We will then create a <code>RoomJSON</code> struct which can hold a collection of fields. Our <code>RoomJSON</code> currently has a field called <code>Name</code> that is of type string. The struct tag is denoted by the two backticks and within that contains <code>json:"name"</code>. This helps to control the encoding of JSON and will use the lowercase version of <code>name</code> rather than the field <code>Name</code>. We can then call <code>ctx.BindJSON()</code>, which will bind the JSON payload to our RoomJSON struct. If our operation is successful, we can then append the room name to our list of room names.</p>
<p>Lastly, create our <code>getRooms</code> function that will return our list of rooms as JSON data and can see the rooms that we have added previously.</p>
<pre><code class="lang-go"><span class="hljs-comment">// routes.go</span>
<span class="hljs-comment">// package routes</span>

<span class="hljs-comment">// hold our list of room names</span>
<span class="hljs-keyword">var</span> rooms []<span class="hljs-keyword">string</span>;

<span class="hljs-comment">// struct to hold JSON body data</span>
<span class="hljs-keyword">type</span> RoomJSON <span class="hljs-keyword">struct</span> {
    Name <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"name"`</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">createRoom</span><span class="hljs-params">(ctx *gin.Context)</span></span> {
    <span class="hljs-keyword">var</span> roomJSON RoomJSON
    err := ctx.BindJSON(&amp;roomJSON)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
            <span class="hljs-string">"error"</span>:   <span class="hljs-string">"failed to parse JSON data"</span>,
        })
    }

    rooms = <span class="hljs-built_in">append</span>(rooms, roomJSON.Name)

    ctx.JSON(http.StatusOK, gin.H{
        <span class="hljs-string">"status"</span>: <span class="hljs-string">"room name successfully added"</span>,
    })
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getRooms</span><span class="hljs-params">(ctx *gin.Context)</span></span> {
    ctx.JSON(http.StatusOK, gin.H{
        <span class="hljs-string">"rooms"</span>: rooms,
    })
}
</code></pre>
<h2 id="heading-running-and-testing-our-server">running and testing our server</h2>
<p>we can run our backend server by running</p>
<pre><code class="lang-bash">go run main.go
</code></pre>
<p>and the sever will be setup as shown below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681544998726/885266d0-d85d-4ef2-ac77-479cd0557ef7.png" alt class="image--center mx-auto" /></p>
<p>To test if our API endpoints are working, we can make an HTTP request to the server either on the command line or an API platform such as <a target="_blank" href="https://www.postman.com/">Postman</a>.</p>
<p>Using command line</p>
<pre><code class="lang-bash"><span class="hljs-comment"># test if server is online</span>
$ curl http://localhost:3001
<span class="hljs-comment"># will return {"status": "ok"}</span>

<span class="hljs-comment"># add a room name</span>
$ curl -X POST -d <span class="hljs-string">'{"name": "room1"}'</span> http://localhost:3001/create-room
<span class="hljs-comment"># will return {"status":"room name successfully added"}</span>

<span class="hljs-comment"># view all rooms</span>
$ curl http://localhost:3001/rooms
<span class="hljs-comment"># will return {"rooms":["room1"]}</span>
</code></pre>
<p>Using Postman</p>
<p>Check if the server is online</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681545556777/68a947aa-6817-478e-982e-80679c0c0d3e.png" alt="check if server is online" class="image--center mx-auto" /></p>
<p>Adding a new room name</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681545608820/330f9621-9b48-4dfd-a398-c4a1ecae339b.png" alt class="image--center mx-auto" /></p>
<p>View all room names</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681545568895/3e12feff-6e6a-4bbd-b704-f87dd497fa69.png" alt class="image--center mx-auto" /></p>
<p>Seems like all our endpoints are working fine.</p>
<p>However, the room names that we just stored are only persistent only when our server is running. If our server throws an error unexpectedly and exits, we will lose all our data. To make our storage persistent, we will introduce a database into our project.</p>
<p>In part 3, we will look at how to integrate the SQLite3 database into our application to make our data storage persistent. We will also add more handler functions to handle more request types.</p>
]]></content:encoded></item><item><title><![CDATA[IOT Home application Part 1]]></title><description><![CDATA[Background
IOT Home is an application I created to help manage my smart Home. The application allows me to control lights and switches in my house, providing me with a convenient and intuitive way to manage my home automation.
My inspiration for IOT ...]]></description><link>https://blog.wongandre.com/iot-home-application-part-1</link><guid isPermaLink="true">https://blog.wongandre.com/iot-home-application-part-1</guid><category><![CDATA[projects]]></category><dc:creator><![CDATA[Andre Wong]]></dc:creator><pubDate>Sat, 15 Apr 2023 03:07:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1681538908010/ea04975b-52ff-435e-a354-777dac6cb1aa.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-background">Background</h2>
<p>IOT Home is an application I created to help manage my smart Home. The application allows me to control lights and switches in my house, providing me with a convenient and intuitive way to manage my home automation.</p>
<p>My inspiration for IOT Home came from Home Assistant, an open-source home automation software that can connect with many smart devices. I wanted to create a similar application that I could use daily, with a focus on controlling lights and switches. I also wanted to explore a variety of technologies and integrate them into my project.</p>
<h2 id="heading-application-overview">Application Overview</h2>
<p>IOT Home is a web application that provides a dashboard to control lights and switches in my home. The application's current focus is on controlling lights and switches, but it can be extended to control other smart home devices in the future.</p>
<p>Here's a preview of the application we will be building:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681527283898/303eb706-1846-4a9e-a4dd-cb77ff9e458f.jpeg" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681527335736/e7c89150-e5b8-4110-95ff-7b11453ef7c7.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-app-features">App features</h2>
<p>Below is a list of features to build for my first minimal version of the app.</p>
<p>Authentication</p>
<ul>
<li><p>Users can log in to the dashboard</p>
</li>
<li><p>Users can logout of the account</p>
</li>
<li><p>Users can signup for an account</p>
</li>
</ul>
<p>Rooms</p>
<ul>
<li><p>Users can create a room</p>
</li>
<li><p>Users can view all the rooms created</p>
</li>
<li><p>Users can delete rooms</p>
</li>
</ul>
<p>Devices</p>
<ul>
<li><p>Users can add a device to a room</p>
</li>
<li><p>Users can view all devices in a room</p>
</li>
<li><p>Users can see the status of the device</p>
<ul>
<li><p>check if the device is currently connected</p>
</li>
<li><p>check if the device is currently on</p>
</li>
</ul>
</li>
<li><p>Users can access the settings page of a device</p>
</li>
<li><p>Users can switch a device on or off</p>
</li>
<li><p>Users can delete a device</p>
</li>
<li><p>Device type can support</p>
<ul>
<li><p><a target="_blank" href="https://kno.wled.ge/">WLED</a></p>
</li>
<li><p><a target="_blank" href="https://tasmota.github.io/docs/">Tasmota</a></p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-application-architecture">Application Architecture</h2>
<p>Below is a simple architecture diagram that shows the interaction between the different components of the application that I will be working on. The initial communication protocol between the device and the server will be through REST API, similar to the communication between the server and the front end. Our data will be persistent through the use of a database in which we can insert and modify the data through SQL queries.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681023783524/5dc80fc9-bdd1-4619-aa87-2a0508c48d38.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-tech-stack">Tech stack</h2>
<p>Some of the main technologies I will be using are listed below:</p>
<ul>
<li><p>Next.js</p>
</li>
<li><p>Golang</p>
</li>
<li><p>Sqlite3</p>
</li>
</ul>
<p><strong>Next.js</strong></p>
<p>Next.js is a React-based web framework that I will be using to write my frontend part of the application. I like Next.js as they simplify development for my project. It uses a built-in router that automatically generates routes based on the file system structure of the project. Next.js also uses TypeScript which helps in static type checking in JavaScript code.</p>
<p><strong>Golang</strong></p>
<p>I plan to use Golang as my backend server programming language due to its simplicity, efficiency and concurrency support. It is a popular language for building backend services.</p>
<p><strong>SQLite3</strong></p>
<p>SQLite3 is a lightweight relational database management system that is used in small-scale applications. I believe that utilizing SQLite3 will provide me with an opportunity to enhance my knowledge about relational databases and their optimization techniques.</p>
<p>I don't think this is a perfect guide to creating an application but more of a documentation of my thought process and difficulties when developing an application from scratch.</p>
<p>Check out part 2 on creating a backend server with Golang and Gin</p>
]]></content:encoded></item></channel></rss>