<?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: adeutou</title>
    <description>The latest articles on DEV Community by adeutou (@adeutou).</description>
    <link>https://dev.to/adeutou</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F678844%2Ffa818fd6-159d-4b74-872b-5464fb1b32dc.jpeg</url>
      <title>DEV Community: adeutou</title>
      <link>https://dev.to/adeutou</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adeutou"/>
    <language>en</language>
    <item>
      <title>Self-Hosting Like a Pro, Part 1: Hardening a Fresh Ubuntu VPS</title>
      <dc:creator>adeutou</dc:creator>
      <pubDate>Mon, 06 Jul 2026 09:26:36 +0000</pubDate>
      <link>https://dev.to/adeutou/self-hosting-like-a-pro-part-1-hardening-a-fresh-ubuntu-vps-30gb</link>
      <guid>https://dev.to/adeutou/self-hosting-like-a-pro-part-1-hardening-a-fresh-ubuntu-vps-30gb</guid>
      <description>&lt;p&gt;&lt;em&gt;This is the first article in a four-part series where I document how I turned a 10€/month VPS into a production-grade platform hosting my portfolio, a university group webapplication and a SaaS product, all isolated from each other with Kubernetes. In this part, we take a fresh Ubuntu server and lock it down properly before installing anything else.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother with hardening?
&lt;/h2&gt;

&lt;p&gt;The moment your VPS gets a public IP address, it starts receiving attacks. Not "might receive", it &lt;em&gt;starts&lt;/em&gt;. Within minutes, automated bots will probe port 22, try &lt;code&gt;root:root&lt;/code&gt;, &lt;code&gt;admin:admin123&lt;/code&gt; and thousands of other credential combinations. If you skip this step and jump straight to deploying your apps, you are building on sand.&lt;/p&gt;

&lt;p&gt;The good news: an hour of work is enough to eliminate the vast majority of these threats. Here is what we will set up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A non-root user with sudo privileges&lt;/li&gt;
&lt;li&gt;SSH key authentication, with passwords and root login disabled&lt;/li&gt;
&lt;li&gt;UFW as a simple, effective firewall&lt;/li&gt;
&lt;li&gt;Fail2ban to ban brute-force attackers automatically&lt;/li&gt;
&lt;li&gt;Automatic security updates&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;A fresh VPS running Ubuntu 24.04 LTS or newer. I use a Hostinger KVM 2 (2 vCPU, 8 GB RAM, 100 GB NVMe), but any provider works: Hetzner, DigitalOcean, OVH, Contabo.&lt;/li&gt;
&lt;li&gt;The root password or SSH key your provider gave you.&lt;/li&gt;
&lt;li&gt;A terminal on your local machine (macOS, Linux, or WSL on Windows).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Throughout this tutorial, replace &lt;code&gt;YOUR_SERVER_IP&lt;/code&gt; with your server's IP address and &lt;code&gt;deploy&lt;/code&gt; with the username you want to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: First login and system update
&lt;/h2&gt;

&lt;p&gt;Connect as root for the first and last time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@YOUR_SERVER_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update everything before touching anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
apt autoremove &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a kernel update was installed, reboot now:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Wait a minute, then reconnect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create a non-root user
&lt;/h2&gt;

&lt;p&gt;Working as root is like driving without a seatbelt: fine until it isn't. One mistyped &lt;code&gt;rm -rf&lt;/code&gt; and the party is over. Create a dedicated user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adduser deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Choose a strong password (you will still need it for &lt;code&gt;sudo&lt;/code&gt;, even after we disable password SSH logins). Then grant sudo privileges:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;su - deploy
&lt;span class="nb"&gt;sudo whoami&lt;/span&gt;
&lt;span class="c"&gt;# Expected output: root&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: SSH key authentication
&lt;/h2&gt;

&lt;p&gt;Passwords can be brute-forced. SSH keys, in practice, cannot. If you don't have a key pair yet, generate one &lt;strong&gt;on your local machine&lt;/strong&gt; (not on the server):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"deploy@YOUR_SERVER_IP"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ed25519 is the modern default: shorter keys, faster, and at least as secure as 4096-bit RSA.&lt;/p&gt;

&lt;p&gt;Now copy the public key to the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-copy-id deploy@YOUR_SERVER_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test it from your local machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh deploy@YOUR_SERVER_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you log in without being asked for a password, the key works. &lt;strong&gt;Do not proceed to the next step until this works&lt;/strong&gt;, otherwise you will lock yourself out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Harden the SSH daemon
&lt;/h2&gt;

&lt;p&gt;Now we disable everything an attacker could exploit. Open the SSH configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find and change (or add) the following lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;PermitRootLogin&lt;/span&gt; &lt;span class="no"&gt;no&lt;/span&gt;
&lt;span class="k"&gt;PasswordAuthentication&lt;/span&gt; &lt;span class="no"&gt;no&lt;/span&gt;
&lt;span class="k"&gt;PubkeyAuthentication&lt;/span&gt; &lt;span class="no"&gt;yes&lt;/span&gt;
&lt;span class="k"&gt;MaxAuthTries&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;span class="k"&gt;ClientAliveInterval&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
&lt;span class="k"&gt;ClientAliveCountMax&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;X11Forwarding&lt;/span&gt; &lt;span class="no"&gt;no&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick explanation of each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PermitRootLogin no&lt;/code&gt;: root can no longer log in over SSH at all. Attackers love the root account because they don't need to guess the username.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PasswordAuthentication no&lt;/code&gt;: keys only. Brute-force attacks become pointless.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MaxAuthTries 3&lt;/code&gt;: three failed attempts and the connection is dropped.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ClientAliveInterval&lt;/code&gt; / &lt;code&gt;ClientAliveCountMax&lt;/code&gt;: idle sessions are closed after 10 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X11Forwarding no&lt;/code&gt;: we are running a server, not a remote desktop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On Ubuntu 24.04+, also check the drop-in directory, because cloud providers often override settings there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /etc/ssh/sshd_config.d/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see a file like &lt;code&gt;50-cloud-init.conf&lt;/code&gt; containing &lt;code&gt;PasswordAuthentication yes&lt;/code&gt;, edit or remove it, otherwise it silently wins over your main config.&lt;/p&gt;

&lt;p&gt;Validate the configuration and restart the daemon:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Keep your current session open&lt;/strong&gt; and test the new rules from a second terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh deploy@YOUR_SERVER_IP        &lt;span class="c"&gt;# should work (key)&lt;/span&gt;
ssh root@YOUR_SERVER_IP          &lt;span class="c"&gt;# should be refused&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Firewall with UFW
&lt;/h2&gt;

&lt;p&gt;UFW (Uncomplicated Firewall) lives up to its name. The strategy is simple: deny everything inbound, then open only what we need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default deny incoming
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default allow outgoing
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow OpenSSH
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80/tcp    &lt;span class="c"&gt;# HTTP&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/tcp   &lt;span class="c"&gt;# HTTPS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order matters: &lt;strong&gt;allow OpenSSH before enabling the firewall&lt;/strong&gt;, or you will cut the branch you are sitting on. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable
sudo &lt;/span&gt;ufw status verbose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Status: active
Default: deny (incoming), allow (outgoing), disabled (routed)

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW IN    Anywhere
80/tcp                     ALLOW IN    Anywhere
443/tcp                    ALLOW IN    Anywhere
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Ports 80 and 443 will later be handled by our reverse proxy (Traefik, in Part 2). Everything else, including databases, monitoring dashboards and internal services, stays invisible from the internet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Fail2ban
&lt;/h2&gt;

&lt;p&gt;Even with keys-only authentication, bots will keep hammering your SSH port and polluting your logs. Fail2ban watches the logs and bans IPs that misbehave.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;fail2ban &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never edit &lt;code&gt;jail.conf&lt;/code&gt; directly (it gets overwritten on updates). Create a local override instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/fail2ban/jail.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[DEFAULT]&lt;/span&gt;
&lt;span class="py"&gt;bantime&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1h&lt;/span&gt;
&lt;span class="py"&gt;findtime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10m&lt;/span&gt;
&lt;span class="py"&gt;maxretry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5&lt;/span&gt;

&lt;span class="nn"&gt;[sshd]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;bantime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;24h&lt;/span&gt;
&lt;span class="py"&gt;maxretry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Translation: any IP that fails SSH authentication 3 times within 10 minutes is banned for 24 hours. Enable and start the service:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Check that the SSH jail is active:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;fail2ban-client status sshd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Come back after a day and run that command again. Seeing the list of banned IPs grow is oddly satisfying, and a good reminder of why this whole article exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Automatic security updates
&lt;/h2&gt;

&lt;p&gt;A hardened server that runs unpatched software is only hardened on paper. Ubuntu's &lt;code&gt;unattended-upgrades&lt;/code&gt; installs security patches automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;unattended-upgrades &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg-reconfigure &lt;span class="nt"&gt;-plow&lt;/span&gt; unattended-upgrades
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Select "Yes" in the dialog. To verify the configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/apt/apt.conf.d/20auto-upgrades
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;APT&lt;/span&gt;::&lt;span class="n"&gt;Periodic&lt;/span&gt;::&lt;span class="n"&gt;Update&lt;/span&gt;-&lt;span class="n"&gt;Package&lt;/span&gt;-&lt;span class="n"&gt;Lists&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt;;
&lt;span class="n"&gt;APT&lt;/span&gt;::&lt;span class="n"&gt;Periodic&lt;/span&gt;::&lt;span class="n"&gt;Unattended&lt;/span&gt;-&lt;span class="n"&gt;Upgrade&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, only the security repository is enabled for automatic upgrades, which is exactly what we want on a server: security patches yes, surprise major version bumps no.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final checklist
&lt;/h2&gt;

&lt;p&gt;Before moving on, verify each item:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Root login refused&lt;/span&gt;
ssh root@YOUR_SERVER_IP
&lt;span class="c"&gt;# Expected: Permission denied&lt;/span&gt;

&lt;span class="c"&gt;# 2. Password auth refused (from a machine without your key)&lt;/span&gt;
ssh &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;PubkeyAuthentication&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no deploy@YOUR_SERVER_IP
&lt;span class="c"&gt;# Expected: Permission denied (publickey)&lt;/span&gt;

&lt;span class="c"&gt;# 3. Firewall active&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw status | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;
&lt;span class="c"&gt;# Expected: Status: active&lt;/span&gt;

&lt;span class="c"&gt;# 4. Fail2ban running&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl is-active fail2ban
&lt;span class="c"&gt;# Expected: active&lt;/span&gt;

&lt;span class="c"&gt;# 5. Auto-updates configured&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;unattended-upgrade &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="nt"&gt;--debug&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all five checks pass, congratulations: your VPS just went from "free real estate for botnets" to a solid foundation.&lt;/p&gt;

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

&lt;p&gt;In &lt;strong&gt;Part 2&lt;/strong&gt;, we install the actual platform: Docker, K3s (a lightweight Kubernetes distribution that fits comfortably in 8 GB of RAM), Traefik v3 as reverse proxy, and cert-manager for automatic Let's Encrypt certificates. By the end of it, any app we deploy will get HTTPS with zero manual certificate handling.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
