<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: UBA-code</title>
    <description>The latest articles on DEV Community by UBA-code (@uba-code).</description>
    <link>https://dev.to/uba-code</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3184185%2F43351da0-a44b-4ab4-8317-36cdb2fa44c8.jpg</url>
      <title>DEV Community: UBA-code</title>
      <link>https://dev.to/uba-code</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/uba-code"/>
    <language>en</language>
    <item>
      <title>Cloud-1: How I Automated a Full Cloud Deployment with Ansible and Docker</title>
      <dc:creator>UBA-code</dc:creator>
      <pubDate>Wed, 01 Apr 2026 15:41:28 +0000</pubDate>
      <link>https://dev.to/uba-code/cloud-1-how-i-automated-a-full-cloud-deployment-with-ansible-and-docker-1921</link>
      <guid>https://dev.to/uba-code/cloud-1-how-i-automated-a-full-cloud-deployment-with-ansible-and-docker-1921</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Zero manual SSH sessions. Zero hand-typed commands. One &lt;code&gt;ansible-playbook&lt;/code&gt; call and the whole stack is live.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  A Little Context
&lt;/h2&gt;

&lt;p&gt;As part of my job, spinning up cloud VMs and deploying web applications is routine. But the mandate for my latest project, Cloud-1, was to take this a step further and automate absolutely everything. No clicking around in a dashboard, no executing commands by hand on the server. The entire deployment pipeline had to be flawlessly scripted and repeatable.&lt;/p&gt;

&lt;p&gt;The result is a clean DevOps project that touches Ansible, Docker Compose, Nginx, Certbot (Let's Encrypt), DuckDNS, and automated certificate renewal. In this post I'll walk through the design and the most interesting implementation details.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture at a Glance
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Local Machine (control node)
  │
  │  ansible-playbook
  │  ──────────────────────►  Cloud VM (Ubuntu)
  │                              │
  │                              ├─ Docker
  │                              │   ├─ WordPress
  │                              │   ├─ Nginx (reverse-proxy + SSL termination)
  │                              │   ├─ MariaDB
  │                              │   └─ Certbot (one-shot certificate issuer)
  │                              │
  │                              └─ anacron (monthly cert renewal)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything lives in two Ansible playbooks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Playbook&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker_playbook.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Install Docker &amp;amp; add the ubuntu user to the docker group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deploy_playbook.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clone the app repo, inject secrets, start containers, configure DNS &amp;amp; SSL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step 1 — Inventory: Targeting the Servers
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;inventory.ini&lt;/code&gt; is deliberately minimal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[myhosts]&lt;/span&gt;
&lt;span class="err"&gt;15.237.214.130&lt;/span&gt;
&lt;span class="err"&gt;15.236.239.28&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two IP addresses, one group. Ansible will run every play against both hosts in parallel. Swapping servers is a one-line change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2 — Bootstrapping Docker (the &lt;code&gt;docker_playbook&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Before we can run any containers, the host needs Docker. Rather than baking a custom AMI, the playbook installs it idempotently every time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Docker&lt;/span&gt;
  &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myhosts&lt;/span&gt;
  &lt;span class="na"&gt;remote_user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install required system packages&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.apt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;pkg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apt-transport-https&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ca-certificates&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;software-properties-common&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;anacron&lt;/span&gt;   &lt;span class="c1"&gt;# needed later for cert renewal&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Add Docker GPG apt Key&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.apt_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://download.docker.com/linux/ubuntu/gpg&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Add docker repository&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.apt_repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deb&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;[arch=amd64]&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://download.docker.com/linux/ubuntu&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;focal&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;stable"&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install docker&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.apt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker-ce&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Start &amp;amp; enable docker service&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;started&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Add the current user to docker group&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu&lt;/span&gt;
        &lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
        &lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key design choice here is &lt;strong&gt;idempotency&lt;/strong&gt; — running this playbook twice won't break anything. &lt;code&gt;state: latest&lt;/code&gt; and &lt;code&gt;state: present&lt;/code&gt; ensure Ansible only acts when necessary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3 — Deploying the Application (the &lt;code&gt;deploy_playbook&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;This is where the magic happens. The playbook has eight logically ordered stages.&lt;/p&gt;

&lt;h3&gt;
  
  
  3a. Clone the App Repository
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Clone the repository of source files&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.git&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com/UBA-code/cloud-1-blog.git"&lt;/span&gt;
    &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$HOME/site&lt;/span&gt;
    &lt;span class="na"&gt;force&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;force: true&lt;/code&gt; ensures any local changes on the server are wiped and replaced with the canonical version from GitHub. The server is always in sync with the repo — no config drift.&lt;/p&gt;

&lt;h3&gt;
  
  
  3b. Inject Secrets Securely
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Copy the .env into wordpress directory&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.copy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../secrets/.env&lt;/span&gt;
    &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$HOME/site&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file lives in a &lt;code&gt;secrets/&lt;/code&gt; directory on the &lt;strong&gt;control node&lt;/strong&gt; (local machine) and is explicitly listed in &lt;code&gt;.gitignore&lt;/code&gt;. It is never committed to version control. Ansible copies it to the server at deploy time — so secrets travel over SSH but never touch a git remote.&lt;/p&gt;

&lt;h3&gt;
  
  
  3c. Start the Containers
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and run the containers&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cmd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker compose up --build -d&lt;/span&gt;
    &lt;span class="na"&gt;chdir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/home/ubuntu/site&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--build&lt;/code&gt; ensures any image changes are picked up. &lt;code&gt;-d&lt;/code&gt; puts containers in detached (daemon) mode. One command brings up WordPress, MariaDB, Nginx, and Certbot simultaneously.&lt;/p&gt;

&lt;h3&gt;
  
  
  3d. Dynamic DNS with DuckDNS
&lt;/h3&gt;

&lt;p&gt;Cloud VMs get a new IP on every boot. Hardcoding IPs in DNS is a maintenance nightmare. The solution: &lt;strong&gt;DuckDNS&lt;/strong&gt;, a free dynamic DNS provider with a dead-simple API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update DuckDNS with the new machine IP&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="s"&gt;curl "https://www.duckdns.org/update?domains=cloud-2&amp;amp;token=&amp;lt;token&amp;gt;&amp;amp;ip=&amp;amp;verbose=true"&lt;/span&gt;
  &lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;duckdns_response&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Print the DuckDNS response&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.debug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;msg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;duckdns_response.stdout&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leaving &lt;code&gt;ip=&lt;/code&gt; empty tells DuckDNS to auto-detect the caller's public IP — perfect for ephemeral cloud instances.&lt;/p&gt;

&lt;p&gt;To ensure the DNS stays updated across reboots, the same &lt;code&gt;curl&lt;/code&gt; command is appended to &lt;code&gt;.bashrc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ensure DuckDNS update is added to .bashrc&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.lineinfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$HOME/.bashrc"&lt;/span&gt;
    &lt;span class="na"&gt;line&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;curl&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"https://www.duckdns.org/update?domains=cloud-2&amp;amp;token=&amp;lt;token&amp;gt;&amp;amp;ip=&amp;amp;verbose=true"'&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;
    &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3e. Generating Let's Encrypt Certificates
&lt;/h3&gt;

&lt;p&gt;HTTPS is non-negotiable. Certbot runs as a Docker container in one-shot mode using the webroot challenge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate Let's Encrypt certificates&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cmd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;docker compose run --rm certbot certonly&lt;/span&gt;
      &lt;span class="s"&gt;--webroot --webroot-path=/var/www/certbot&lt;/span&gt;
      &lt;span class="s"&gt;--email you@example.com&lt;/span&gt;
      &lt;span class="s"&gt;--agree-tos --no-eff-email&lt;/span&gt;
      &lt;span class="s"&gt;-d cloud-2.duckdns.org&lt;/span&gt;
      &lt;span class="s"&gt;--non-interactive&lt;/span&gt;
    &lt;span class="na"&gt;chdir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/home/ubuntu/site&lt;/span&gt;
  &lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;certbot_output&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
  &lt;span class="na"&gt;failed_when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;certbot_output.rc&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;!=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"unauthorized"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;certbot_output.stdout'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;failed_when&lt;/code&gt; condition is a nice touch: if Certbot exits non-zero &lt;em&gt;because&lt;/em&gt; the domain already has a valid certificate ("unauthorized" in Certbot parlance), Ansible doesn't fail the entire play. Idempotency again.&lt;/p&gt;

&lt;h3&gt;
  
  
  3f. Switching Nginx to HTTPS Mode
&lt;/h3&gt;

&lt;p&gt;The deployment uses a two-phase Nginx config:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initial config&lt;/strong&gt; (&lt;code&gt;nginx.conf&lt;/code&gt;) — HTTP only, serves the Let's Encrypt challenge at &lt;code&gt;/.well-known/acme-challenge/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Final config&lt;/strong&gt; (&lt;code&gt;nginx.conf.final&lt;/code&gt;) — Full HTTPS with SSL certs mounted as Docker volumes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once the certificate is issued, the playbook swaps them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Switch Nginx config to final SSL version&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cmd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mv&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;nginx.conf&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;nginx.conf.initial&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;mv&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;nginx.conf.final&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;nginx.conf"&lt;/span&gt;
    &lt;span class="na"&gt;chdir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$HOME/site/nginx-conf/&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Reload Nginx&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cmd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker compose exec nginx nginx -s reload&lt;/span&gt;
    &lt;span class="na"&gt;chdir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/home/ubuntu/site&lt;/span&gt;
  &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;certbot_output.rc&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"unauthorized"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;certbot_output.stdout'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A graceful reload (&lt;code&gt;nginx -s reload&lt;/code&gt;) applies the new config with zero downtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4 — Automatic Certificate Renewal
&lt;/h2&gt;

&lt;p&gt;Let's Encrypt certificates expire every 90 days. Forgetting to renew means a hard HTTPS outage. The solution is a tiny shell script deployed to the server and wired into &lt;strong&gt;anacron&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

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

docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; /home/ubuntu/site/docker-compose.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  run &lt;span class="nt"&gt;--rm&lt;/span&gt; certbot renew &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /var/log/renew_cert_log.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anacron (unlike cron) is designed for machines that aren't always on — it catches up on missed jobs. The playbook registers a weekly renewal task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Add certbot renew task to anacron&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.lineinfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/anacrontab&lt;/span&gt;
    &lt;span class="na"&gt;line&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;7&lt;/span&gt;&lt;span class="nv"&gt;  &lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cron.renew_task&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/home/ubuntu/renew_certificate.sh"&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;7&lt;/code&gt; means the job runs if it hasn't run in the last 7 days. The logfile at &lt;code&gt;/var/log/renew_cert_log.log&lt;/code&gt; captures every renewal attempt for easy debugging.&lt;/p&gt;




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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cloud-1/
├── .gitignore               # Excludes secrets/.env
├── inventory.ini            # Target hosts
├── playbooks/
│   ├── docker_playbook.yaml # Bootstrap Docker on hosts
│   └── deploy_playbook.yaml # Full application deployment
├── scripts/
│   └── renew_certificate.sh # SSL cert auto-renewal
└── secrets/
    └── .env.example         # Template — copy to .env and fill in values
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Running It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Configure your servers&lt;/span&gt;
vi inventory.ini

&lt;span class="c"&gt;# 2. Set up your secrets&lt;/span&gt;
&lt;span class="nb"&gt;cp &lt;/span&gt;secrets/.env.example secrets/.env
vi secrets/.env

&lt;span class="c"&gt;# 3. Bootstrap Docker on the servers (only needed once)&lt;/span&gt;
ansible-playbook &lt;span class="nt"&gt;-i&lt;/span&gt; inventory.ini playbooks/docker_playbook.yaml

&lt;span class="c"&gt;# 4. Deploy the full application&lt;/span&gt;
ansible-playbook &lt;span class="nt"&gt;-i&lt;/span&gt; inventory.ini playbooks/deploy_playbook.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. A few minutes later, WordPress is live at &lt;code&gt;https://cloud-2.duckdns.org&lt;/code&gt; with a valid Let's Encrypt certificate and automatic renewal configured.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure as Code pays off immediately.&lt;/strong&gt; When a server was wiped and re-provisioned, bringing it back up was a single command instead of an hour of manual work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency is not optional.&lt;/strong&gt; Every task was written so re-running the playbook is safe. &lt;code&gt;state: present&lt;/code&gt;, &lt;code&gt;state: latest&lt;/code&gt;, &lt;code&gt;lineinfile&lt;/code&gt; with duplicate detection — these aren't conveniences, they're correctness requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets management is simple but effective.&lt;/strong&gt; No Vault, no KMS at this scale. The pattern of keeping secrets local and copying them over SSH is good enough for most small projects and dramatically reduces the attack surface compared to committing &lt;code&gt;.env&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two-phase Nginx config solves the chicken-and-egg SSL problem.&lt;/strong&gt; You need HTTP available to issue the certificate, but you want HTTPS everywhere after. The swap trick elegantly handles this without a complex conditional setup.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;A few natural extensions for anyone picking up this project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD integration&lt;/strong&gt; — Trigger &lt;code&gt;deploy_playbook.yaml&lt;/code&gt; from a GitHub Actions workflow on every push to &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt; — Add a Prometheus + Grafana stack as additional Docker Compose services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load balancing&lt;/strong&gt; — Add more hosts to &lt;code&gt;inventory.ini&lt;/code&gt; and put Nginx (or an AWS ALB) in front of them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backup automation&lt;/strong&gt; — Dump the MariaDB database and upload it to S3 on a schedule&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Cloud-1 is a 42 School project. The full source is on &lt;a href="https://github.com/UBA-code/cloud-1-blog" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ansible</category>
      <category>docker</category>
      <category>cloud</category>
    </item>
    <item>
      <title>🍿 Hypertube: Building a Netflix-Inspired Streaming App Powered by BitTorrent</title>
      <dc:creator>UBA-code</dc:creator>
      <pubDate>Sun, 29 Mar 2026 21:22:10 +0000</pubDate>
      <link>https://dev.to/uba-code/hypertube-building-a-netflix-inspired-streaming-app-powered-by-bittorrent-33ln</link>
      <guid>https://dev.to/uba-code/hypertube-building-a-netflix-inspired-streaming-app-powered-by-bittorrent-33ln</guid>
      <description>&lt;p&gt;&lt;em&gt;What if you could build your own Netflix — without any licensing deals, content budgets, or monthly subscription fees? That was the challenge behind **Hypertube&lt;/em&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Idea
&lt;/h2&gt;

&lt;p&gt;Streaming platforms have conditioned us to expect instant, high-quality video — but that experience is expensive to build and expensive to maintain. What if the same seamless experience could be delivered using peer-to-peer technology that's been around for decades?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hypertube&lt;/strong&gt; is a full-stack web application that does exactly that. It's a modern, dark-mode streaming platform — aesthetically inspired by Netflix — that uses &lt;strong&gt;BitTorrent&lt;/strong&gt; to source and stream movies directly inside your browser. No subscriptions. No ads. No waiting for downloads to finish.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Hypertube Can Do
&lt;/h2&gt;

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

&lt;p&gt;The application brings together a full cinematic experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔍 &lt;strong&gt;Advanced Search&lt;/strong&gt; — Filter thousands of movies by quality, genre, and rating across multiple torrent sources&lt;/li&gt;
&lt;li&gt;▶️ &lt;strong&gt;Instant Streaming&lt;/strong&gt; — Start watching in seconds with smart buffering. The video plays while it downloads in the background&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Multi-language Subtitles&lt;/strong&gt; — Dozens of subtitle languages, auto-synced to the video&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Community Features&lt;/strong&gt; — Comment on films, discuss your favorites, and connect with other cinephiles&lt;/li&gt;
&lt;li&gt;🔐 &lt;strong&gt;Flexible Authentication&lt;/strong&gt; — Log in with GitHub, Google, GitLab, Discord, or 42 — or just use email&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Authentication: Five OAuth Providers and Email
&lt;/h2&gt;

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

&lt;p&gt;The sign-in flow was built with real-world security in mind. Users can authenticate via five different OAuth2 providers or a classic email/password flow — all handled through &lt;strong&gt;Passport.js strategies&lt;/strong&gt; on the NestJS backend.&lt;/p&gt;

&lt;p&gt;Access tokens are short-lived and JWTs are revoked on logout through a dedicated &lt;code&gt;revoked-tokens&lt;/code&gt; module, preventing token replay attacks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Browsing: A Cinematic Movie Grid
&lt;/h2&gt;

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

&lt;p&gt;The main browsing experience presents movies in a rich poster grid — sorted by popularity, rating, or genre. Each card shows the movie's quality badge, IMDb rating, and release year at a glance. The interface feels instantly familiar thanks to its dark-mode Netflix-like design, built with &lt;strong&gt;React&lt;/strong&gt;, &lt;strong&gt;TypeScript&lt;/strong&gt;, and &lt;strong&gt;Tailwind CSS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Filtering and searching happen in real time, with query results pulled from multiple torrent sources and enriched with metadata from public movie APIs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Movie Details: Everything at a Glance
&lt;/h2&gt;

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

&lt;p&gt;Every movie gets its own dedicated page with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full synopsis, cast, and director info&lt;/li&gt;
&lt;li&gt;Available quality options (720p, 1080p, etc.)&lt;/li&gt;
&lt;li&gt;Subtitle language selector&lt;/li&gt;
&lt;li&gt;A one-click &lt;strong&gt;Stream Now&lt;/strong&gt; button&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The detail page fetches metadata and torrent magnet links simultaneously, so by the time you hit &lt;em&gt;play&lt;/em&gt;, everything is ready.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Magic: BitTorrent Meets HLS Streaming
&lt;/h2&gt;

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

&lt;p&gt;This is where Hypertube gets genuinely interesting from an engineering perspective.&lt;/p&gt;

&lt;p&gt;Traditional streaming services encode videos into HLS segments and serve them from CDNs. Hypertube does something different: it downloads the torrent file &lt;strong&gt;on-demand on the server&lt;/strong&gt;, pipes the video data through &lt;strong&gt;FFmpeg&lt;/strong&gt; for real-time transcoding, and serves it as an &lt;strong&gt;HLS stream&lt;/strong&gt; to the browser.&lt;/p&gt;

&lt;p&gt;Here's the flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User clicks "Play"
       ↓
Backend resolves torrent → starts downloading in-memory
       ↓
FFmpeg transcodes incoming bytes → .m3u8 + .ts segments
       ↓
Browser loads HLS playlist → video plays instantly
       ↓
Download continues in background while you watch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is a zero-wait streaming experience backed by the BitTorrent network — no file storage required, no CDN bill.&lt;/p&gt;




&lt;h2&gt;
  
  
  Community: Comments &amp;amp; Discussions
&lt;/h2&gt;

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

&lt;p&gt;Hypertube isn't just a player — it's a community. Each movie has a threaded comment section where users can discuss the film, share thoughts, and leave ratings. Comments are tied to authenticated accounts, keeping discussions meaningful.&lt;/p&gt;




&lt;h2&gt;
  
  
  Watch History: Never Lose Your Place
&lt;/h2&gt;

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

&lt;p&gt;Every movie you stream is automatically logged to your watch history. You can resume from where you left off, revisit favorites, and track your watching habits — all without any manual bookmarking.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack Deep Dive
&lt;/h2&gt;

&lt;p&gt;Hypertube is a modern full-stack application split cleanly into two services:&lt;/p&gt;

&lt;h3&gt;
  
  
  🖥️ Frontend
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;React + TypeScript&lt;/td&gt;
&lt;td&gt;UI components and type safety&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tailwind CSS&lt;/td&gt;
&lt;td&gt;Utility-first styling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vite&lt;/td&gt;
&lt;td&gt;Lightning-fast dev server and bundler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React Router&lt;/td&gt;
&lt;td&gt;Client-side routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React Icons&lt;/td&gt;
&lt;td&gt;Icon library&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  ⚙️ Backend
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NestJS (TypeScript)&lt;/td&gt;
&lt;td&gt;Modular REST API framework&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Passport.js&lt;/td&gt;
&lt;td&gt;OAuth2 + JWT authentication strategies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FFmpeg&lt;/td&gt;
&lt;td&gt;Real-time video transcoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HLS&lt;/td&gt;
&lt;td&gt;Adaptive HTTP video streaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swagger&lt;/td&gt;
&lt;td&gt;Auto-generated API documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  🐳 Infrastructure
&lt;/h3&gt;

&lt;p&gt;Both the frontend and backend are &lt;strong&gt;fully Dockerized&lt;/strong&gt;, with separate &lt;code&gt;docker-compose&lt;/code&gt; configurations for development and production. This makes the entire stack reproducible with a single command:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;






&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────┐
│              React Frontend              │
│     (Vite + TypeScript + Tailwind)      │
└──────────────────┬──────────────────────┘
                   │ REST API / HLS
┌──────────────────▼──────────────────────┐
│             NestJS Backend               │
│  ┌─────────┐ ┌──────────┐ ┌──────────┐ │
│  │  Auth   │ │  Movies  │ │ Comments │ │
│  │ Module  │ │  Module  │ │  Module  │ │
│  └─────────┘ └────┬─────┘ └──────────┘ │
│              ┌────▼─────┐               │
│              │ Torrent  │               │
│              │  Module  │               │
│              └────┬─────┘               │
│              ┌────▼─────┐               │
│              │  FFmpeg  │               │
│              │ (HLS out)│               │
│              └──────────┘               │
└─────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backend is cleanly separated into NestJS modules: &lt;code&gt;auth&lt;/code&gt;, &lt;code&gt;movies&lt;/code&gt;, &lt;code&gt;comments&lt;/code&gt;, &lt;code&gt;torrent&lt;/code&gt;, &lt;code&gt;users&lt;/code&gt;, and &lt;code&gt;mails&lt;/code&gt; — making the codebase easy to navigate and extend.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Building Hypertube touched on some genuinely tricky engineering problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Streaming before the download completes&lt;/strong&gt; — The hardest part was piping torrent data through FFmpeg fast enough that the HLS playlist stays ahead of the playback position.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OAuth across five providers&lt;/strong&gt; — Each OAuth provider has subtle differences. Passport.js strategies abstract most of it, but edge cases (like handling missing email fields from some providers) required custom logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Token revocation with JWTs&lt;/strong&gt; — JWTs are stateless by nature. Implementing a revoked-tokens store means the backend must check every request against it — a trade-off between security and performance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Responsive dark-mode UI&lt;/strong&gt; — Getting the poster grid, player, and detail pages to feel premium across all screen sizes required careful Tailwind component design.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;Hypertube is open source. If you want to run it locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone the repository&lt;/span&gt;
git clone https://github.com/UBA-code/hypertube

&lt;span class="c"&gt;# Fill in your OAuth credentials&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.preview .env.dev

&lt;span class="c"&gt;# Start everything with Docker&lt;/span&gt;
docker-compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend runs at &lt;code&gt;http://localhost:5173&lt;/code&gt; and the API at &lt;code&gt;http://localhost:3000/api&lt;/code&gt; (with Swagger docs included).&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Hypertube is a proof of concept that modern streaming UX doesn't require a billion-dollar infrastructure budget. By combining BitTorrent's decentralized network with server-side HLS transcoding via FFmpeg, the app delivers a genuinely responsive streaming experience built entirely on open-source technology.&lt;/p&gt;

&lt;p&gt;It was a challenging project — touching OAuth, real-time video processing, peer-to-peer networking, and modern frontend design all at once — but the end result is something that actually works and looks great doing it.&lt;/p&gt;

&lt;p&gt;If you found this interesting, feel free to ⭐ the repo and share your thoughts in the comments below.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with ❤️ using React, NestJS, FFmpeg, and BitTorrent.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;© 2025 Hypertube. All rights reserved.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Building Taskmaster: A Go-Powered Process Supervisor from Scratch</title>
      <dc:creator>UBA-code</dc:creator>
      <pubDate>Tue, 24 Mar 2026 20:46:54 +0000</pubDate>
      <link>https://dev.to/uba-code/building-taskmaster-a-go-powered-process-supervisor-from-scratch-12fn</link>
      <guid>https://dev.to/uba-code/building-taskmaster-a-go-powered-process-supervisor-from-scratch-12fn</guid>
      <description>&lt;p&gt;&lt;em&gt;How two 42 School students reimagined process management with Go's concurrency model&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;If you've ever managed a production server, you know the pain: a critical process crashes at 3 AM, no one notices until morning, and your users are left with a broken experience. Tools like &lt;a href="http://supervisord.org/" rel="noopener noreferrer"&gt;Supervisor&lt;/a&gt; were built to solve this — but they come with Python overhead, complex configurations, and aging architectures.&lt;/p&gt;

&lt;p&gt;We decided to build our own. &lt;strong&gt;Taskmaster&lt;/strong&gt; is a lightweight, production-ready process supervisor written in Go — and building it taught us more about operating systems, concurrency, and daemon design than any textbook could.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Taskmaster?
&lt;/h2&gt;

&lt;p&gt;Taskmaster is a process control daemon. It manages the full lifecycle of your processes — starting, stopping, restarting, and monitoring them — all through a simple interactive shell. Think of it as a modern, Go-native alternative to Supervisor or systemd service management, without the complexity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;./bin/taskmaster config.yaml
Taskmaster&amp;gt; status
Task          Status    PID    Uptime       Restarts  Command
nginx         RUNNING   1234   2m15s        0         /usr/local/bin/nginx
worker_1      RUNNING   1235   2m15s        1         python3 worker.py
worker_2      STOPPED   -      -            0         python3 worker.py

Taskmaster&amp;gt; start worker_2
Process &lt;span class="s1"&gt;'worker_2'&lt;/span&gt; started with PID 1240

Taskmaster&amp;gt; logs worker_1 5
&lt;span class="o"&gt;[&lt;/span&gt;2026-02-02 10:15:25] Processing task &lt;span class="c"&gt;#1&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;2026-02-02 10:15:26] Task completed
&lt;span class="o"&gt;[&lt;/span&gt;2026-02-02 10:15:27] Waiting &lt;span class="k"&gt;for &lt;/span&gt;tasks...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single YAML file to configure everything. A single binary to run it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Go?
&lt;/h2&gt;

&lt;p&gt;The choice of Go wasn't arbitrary. Process supervision is inherently concurrent — you need to monitor dozens of processes simultaneously without blocking. Traditionally, this is solved with threads and shared memory, which leads to complex locking, race conditions, and hard-to-debug crashes.&lt;/p&gt;

&lt;p&gt;Go offers a better model: &lt;strong&gt;goroutines and channels&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Goroutines&lt;/strong&gt; are lightweight (a few KB of stack vs. MB for threads) and can be spawned in the thousands without issue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Channels&lt;/strong&gt; provide safe, structured communication between concurrent components — no mutexes, no shared state hell.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This aligned perfectly with our architecture: one goroutine per managed process, communicating via channels. Elegant, efficient, and easy to reason about.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Deep Dive
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The High-Level Picture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│                        Main Process                          │
│  ┌──────────────┐  ┌─────────────┐  ┌──────────────────┐   │
│  │   CLI Loop   │  │   Config    │  │  Signal Handler  │   │
│  │  (readline)  │  │   Parser    │  │    (SIGHUP)      │   │
│  └──────────────┘  └─────────────┘  └──────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                            │
                ┌───────────┴───────────┐
                │    Task Manager       │
                └───────────┬───────────┘
                            │
        ┌───────────────────┼───────────────────┐
        │                   │                   │
   ┌────▼────┐         ┌────▼────┐        ┌────▼────┐
   │ Process │         │ Process │        │ Process │
   │ Monitor │   ...   │ Monitor │        │ Monitor │
   │(gorout.)│         │(gorout.)│        │(gorout.)│
   └────┬────┘         └────┬────┘        └────┬────┘
        │                   │                   │
   ┌────▼────┐         ┌────▼────┐        ┌────▼────┐
   │  Child  │         │  Child  │        │  Child  │
   │ Process │         │ Process │        │ Process │
   └─────────┘         └─────────┘        └─────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main process runs the interactive CLI (using the &lt;code&gt;readline&lt;/code&gt; library for history and completion) and listens for system signals. It owns a &lt;strong&gt;Task Manager&lt;/strong&gt; which holds the state of all configured processes. Each managed process gets its own goroutine — a &lt;code&gt;StartTaskManager&lt;/code&gt; — that is entirely responsible for that process's lifecycle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Goroutine per Process
&lt;/h3&gt;

&lt;p&gt;Each &lt;code&gt;StartTaskManager&lt;/code&gt; goroutine does three things independently:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Listens&lt;/strong&gt; for control commands over a buffered channel (&lt;code&gt;CmdChan&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitors&lt;/strong&gt; the child process for exits and unexpected crashes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handles restarts&lt;/strong&gt; based on the configured policy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because each process is isolated in its own goroutine, a crash or slowdown in one process monitor cannot affect others. The system stays responsive even when managing hundreds of processes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Channel-Based Communication
&lt;/h3&gt;

&lt;p&gt;The CLI never directly kills or starts a process. Instead, it sends a message over a channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLI ──── "stop nginx" ───► nginx's CmdChan ──► goroutine acts on it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This decoupling means the CLI remains non-blocking regardless of how long a process takes to shut down. The goroutine handles timeouts, fallback signals, and cleanup entirely on its own.&lt;/p&gt;

&lt;p&gt;Three channel types power the system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Command Channels&lt;/strong&gt; — carry control messages (&lt;code&gt;start&lt;/code&gt;, &lt;code&gt;stop&lt;/code&gt;, &lt;code&gt;restart&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Done Channels&lt;/strong&gt; — signal that a child process has exited&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout Channels&lt;/strong&gt; — implement deadlines for startup grace periods and graceful shutdowns&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Process State Machine
&lt;/h3&gt;

&lt;p&gt;Every process moves through a well-defined set of states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[STOPPED] → start → [STARTED] → (after successfulStartTimeout) → [RUNNING]
                        │
                        └─ unexpected exit ──► [FATAL]
                                                   │
                                         restart policy applies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;STOPPED&lt;/strong&gt;: Not running (intentionally or not yet started)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;STARTED&lt;/strong&gt;: Running but in the startup grace period&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RUNNING&lt;/strong&gt;: Confirmed healthy and past the startup timeout&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FATAL&lt;/strong&gt;: Crashed and restart attempts exhausted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;successfulStartTimeout&lt;/code&gt; parameter is a key reliability feature. A process that starts and immediately crashes is different from one that runs for 30 seconds before failing — Taskmaster treats them differently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuration: Simple and Expressive
&lt;/h2&gt;

&lt;p&gt;Taskmaster tasks are defined in a single YAML file. Here's a real-world example managing a web server and a pool of background workers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web_server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/nginx&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-g&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'daemon&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;off;'"&lt;/span&gt;
    &lt;span class="na"&gt;instances&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;autoLaunch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;on-failure&lt;/span&gt;
    &lt;span class="na"&gt;expectedExitCodes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;successfulStartTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="na"&gt;restartsAttempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="na"&gt;stopingSignal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SIGTERM&lt;/span&gt;
    &lt;span class="na"&gt;gracefulStopTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;stdout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/log/taskmaster/nginx.out.log&lt;/span&gt;
    &lt;span class="na"&gt;stderr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/log/taskmaster/nginx.err.log&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080"&lt;/span&gt;
      &lt;span class="na"&gt;ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;production"&lt;/span&gt;
    &lt;span class="na"&gt;workingDirectory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/www&lt;/span&gt;

  &lt;span class="na"&gt;worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python3&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;worker.py"&lt;/span&gt;
    &lt;span class="na"&gt;instances&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;autoLaunch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;restartsAttempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;gracefulStopTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;
    &lt;span class="na"&gt;stdout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/log/taskmaster/worker.out.log&lt;/span&gt;
    &lt;span class="na"&gt;stderr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/log/taskmaster/worker.err.log&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth highlighting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;instances: 5&lt;/code&gt;&lt;/strong&gt; — Taskmaster automatically spawns 5 copies of &lt;code&gt;worker.py&lt;/code&gt; and names them &lt;code&gt;worker_1&lt;/code&gt; through &lt;code&gt;worker_5&lt;/code&gt;. You manage them individually or all at once with &lt;code&gt;restart all&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;restart: on-failure&lt;/code&gt;&lt;/strong&gt; vs &lt;strong&gt;&lt;code&gt;restart: always&lt;/code&gt;&lt;/strong&gt; — The distinction matters in production. &lt;code&gt;on-failure&lt;/code&gt; only restarts if the exit code isn't in &lt;code&gt;expectedExitCodes&lt;/code&gt;. An intentional &lt;code&gt;exit(0)&lt;/code&gt; won't trigger a restart. &lt;code&gt;always&lt;/code&gt; is for long-running daemons that should never stop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;gracefulStopTimeout: 15&lt;/code&gt;&lt;/strong&gt; — When you issue a &lt;code&gt;stop&lt;/code&gt;, Taskmaster sends the configured signal and waits up to 15 seconds for a clean exit. If the process hasn't stopped, it gets &lt;code&gt;SIGKILL&lt;/code&gt;. No zombie processes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hot Reload: Zero-Downtime Config Changes
&lt;/h2&gt;

&lt;p&gt;One of the features we're most proud of: you can change your configuration file and apply it &lt;strong&gt;without restarting the daemon or killing your processes&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Taskmaster&amp;gt; reload
Configuration reloaded.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, this sends a &lt;code&gt;SIGHUP&lt;/code&gt; signal (or you can do it from outside with &lt;code&gt;kill -HUP &amp;lt;pid&amp;gt;&lt;/code&gt;). The config parser re-reads the YAML, diffs it against the current state, and applies changes incrementally. New tasks get started; removed tasks get stopped; modified tasks get restarted. Running tasks that haven't changed? They keep running, untouched.&lt;/p&gt;




&lt;h2&gt;
  
  
  Signal Propagation and Graceful Shutdown
&lt;/h2&gt;

&lt;p&gt;Getting shutdown right is tricky. We had to handle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The daemon receiving &lt;code&gt;SIGTERM&lt;/code&gt; (e.g., from the OS on shutdown)&lt;/li&gt;
&lt;li&gt;Propagating the right signal to each child process&lt;/li&gt;
&lt;li&gt;Waiting for all children to exit before the daemon itself exits&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We used Go's &lt;code&gt;sync.WaitGroup&lt;/code&gt; for this. Every goroutine registers itself with a global &lt;code&gt;WaitGroup&lt;/code&gt; before starting, and signals done when it exits. The main process waits on this group before terminating — guaranteeing that no child processes are left orphaned.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Simplified version of the shutdown flow&lt;/span&gt;
&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c"&gt;// Block until all process monitors are done&lt;/span&gt;
&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;Building Taskmaster from scratch gave us a deep appreciation for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Go's concurrency model is genuinely different.&lt;/strong&gt; Not just syntactically different from threads — conceptually different. "Don't communicate by sharing memory; share memory by communicating" isn't just a motto. It's a design philosophy that produces cleaner, more correct code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. UNIX process management is a deep topic.&lt;/strong&gt; Process groups, session leaders, signal inheritance, file descriptor leaks, zombie processes — every one of these is a footgun waiting to go off. We hit most of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Small surface area wins.&lt;/strong&gt; Taskmaster has one config file, one binary, and one shell. No agents, no web UIs, no databases. This simplicity makes it auditable, embeddable, and easy to debug.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;Taskmaster is open source and available on GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/UBA-code/taskmaster.git
&lt;span class="nb"&gt;cd &lt;/span&gt;taskmaster
make build
./bin/taskmaster  &lt;span class="c"&gt;# Generates an example config and starts the shell&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're building something in Go that needs process management, or if you're curious about how supervisors work under the hood, we hope Taskmaster serves as a useful reference.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with ❤️ by &lt;a href="https://github.com/UBA-code" rel="noopener noreferrer"&gt;Yassine Bel Hachmi&lt;/a&gt; and &lt;a href="https://github.com/hidhmmou" rel="noopener noreferrer"&gt;Hassan Idhmmououhya&lt;/a&gt; as part of the 42 School curriculum.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;⭐ Star the repo if you find it useful!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>devops</category>
      <category>linux</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
