<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Bootstrap & Build]]></title><description><![CDATA[Practical insights at the intersection of software engineering, bootstrapping, and self-hosting. Empowering indie hackers to own their infrastructure and business.]]></description><link>https://www.kkyri.com</link><image><url>https://substackcdn.com/image/fetch/$s_!u3j5!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff01b97f9-4957-40aa-9e19-941b49508a01_512x512.png</url><title>Bootstrap &amp; Build</title><link>https://www.kkyri.com</link></image><generator>Substack</generator><lastBuildDate>Wed, 06 May 2026 10:56:24 GMT</lastBuildDate><atom:link href="https://www.kkyri.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Kyriacos Kyriacou]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[kkyri@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[kkyri@substack.com]]></itunes:email><itunes:name><![CDATA[Kyri]]></itunes:name></itunes:owner><itunes:author><![CDATA[Kyri]]></itunes:author><googleplay:owner><![CDATA[kkyri@substack.com]]></googleplay:owner><googleplay:email><![CDATA[kkyri@substack.com]]></googleplay:email><googleplay:author><![CDATA[Kyri]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[From 3 AM Thought to Launch in 7 Days]]></title><description><![CDATA[How I stopped overthinking and shipped a VPS auditing tool in less time than it takes to pick a new JavaScript framework]]></description><link>https://www.kkyri.com/p/from-3-am-thought-to-launch-in-7</link><guid isPermaLink="false">https://www.kkyri.com/p/from-3-am-thought-to-launch-in-7</guid><dc:creator><![CDATA[Kyri]]></dc:creator><pubDate>Mon, 16 Dec 2024 08:38:40 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!EoYI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5174260-e557-4633-8ffa-811c715261d6_3000x2000.heic" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!EoYI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5174260-e557-4633-8ffa-811c715261d6_3000x2000.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!EoYI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5174260-e557-4633-8ffa-811c715261d6_3000x2000.heic 424w, https://substackcdn.com/image/fetch/$s_!EoYI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5174260-e557-4633-8ffa-811c715261d6_3000x2000.heic 848w, https://substackcdn.com/image/fetch/$s_!EoYI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5174260-e557-4633-8ffa-811c715261d6_3000x2000.heic 1272w, https://substackcdn.com/image/fetch/$s_!EoYI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5174260-e557-4633-8ffa-811c715261d6_3000x2000.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!EoYI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5174260-e557-4633-8ffa-811c715261d6_3000x2000.heic" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f5174260-e557-4633-8ffa-811c715261d6_3000x2000.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:240519,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!EoYI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5174260-e557-4633-8ffa-811c715261d6_3000x2000.heic 424w, https://substackcdn.com/image/fetch/$s_!EoYI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5174260-e557-4633-8ffa-811c715261d6_3000x2000.heic 848w, https://substackcdn.com/image/fetch/$s_!EoYI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5174260-e557-4633-8ffa-811c715261d6_3000x2000.heic 1272w, https://substackcdn.com/image/fetch/$s_!EoYI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5174260-e557-4633-8ffa-811c715261d6_3000x2000.heic 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Shipping ain&#8217;t rocket science</figcaption></figure></div><p>As engineers, we often fall into the perfectionism trap. I know this all too well - my history is littered with unfinished SaaS projects, each one stuck in an endless cycle of improvements and tweaks, never quite "ready" for launch.</p><p>But last week, something changed. At 3AM, while thinking about self-hosting, a simple question popped into my head: "What if there was a tool that could instantly tell you if your VPS is secure?"</p><p>Instead of my usual approach of extensive planning, feature-creep, and polishing, I decided to do something different. The next morning, I sat down and built a basic bash script prototype. Nothing fancy - just something that could check for common VPS security issues - and I <a href="https://x.com/kkyrio/status/1865405008461619212">shared it</a> on my X profile.</p><h2>The 7-Day Challenge</h2><p>The unexpected enthusiasm from the community sparked something in me. Rather than falling back into my old perfectionist habits, I decided to turn this momentum into a challenge: Could I ship a complete, working product in just 7 days? No perfectionism allowed. No "just one more feature."</p><p>And I actually did it. In those 7 days, while posting my entire journey on X, I built:</p><ul><li><p>A bash script that runs security checks on a VPS</p></li><li><p>A backend service that receives and processes the checks from the aforementioned script</p></li><li><p>A web frontend that displays the checks in real-time</p></li><li><p><s>Clear explanations and mitigation steps for each security check</s> (actually this one didn't quite cut it for the MVP)</p></li></ul><h2>The Launch</h2><p>After exactly 7 days of focused evening (and late night) work, I launched AuditVPS. No beta period. No endless polishing. Just a working tool that helps self-hosters secure their servers.</p><p>The response was immediate &#8211; 1K website visits within the first day (I&#8217;ve made the <a href="https://insights.kkyr.io/auditvps.com">analytics public</a>, if you&#8217;re interested). This wasn't just validation of the tool's usefulness; it proved that sometimes the best approach is to ship quickly and let real users guide your next steps. And apparently others thought so too &#8211; within 24 hours, I spotted my first copycat. I'd barely launched and someone was already cloning it. If that&#8217;s not validation, I don&#8217;t know what is!</p><h2>Key Learnings</h2><ol><li><p>Perfect is the enemy of done. My previous pattern of infinite refinement had kept many potentially useful tools from ever seeing the light of day.</p></li><li><p>Users value solutions over perfection. A working tool that solves a specific problem today is better than a perfect tool that launches "someday."</p></li><li><p>Real feedback is invaluable. Launching quickly means you can start learning from actual users rather than assumptions.</p></li><li><p>Speed has its costs. While the 7-day sprint was exciting, the intense schedule alongside a full-time job meant sacrificing sleep, exercise, and social time. Next time, I'll aim for a more sustainable pace that lets me ship quickly without burning out.</p></li><li><p>Being part of a community is a game-changer. When I shared my progress, people jumped in to offer help, security suggestions, and encouragement. Accepting their support (thank you!) not only improved the product but helped maintain momentum through the intense week.</p></li></ol><h2>What's Next?</h2><p>AuditVPS is and will remain free, with plans to introduce a sponsorship-based monetization model. I've also open-sourced the core audit script on GitHub because I believe security tools should be transparent and accessible to everyone.</p><p>While I have many ideas for future features, I'm breaking another old habit: instead of building what I think users want, I'm listening to the community first. What security checks would you find most valuable? How could this tool better serve your needs? Leave a comment below or <a href="https://x.com/kkyrio">reach out</a> on X.</p><p>This is just the beginning of the AuditVPS journey. Sometimes, the best projects don't come from months of careful planning, but from a random 3AM thought and the courage to ship quickly.</p><p>Check it out at <a href="http://auditvps.com/">AuditVPS.com</a>, and if you find the project useful, please consider starring it on <a href="https://github.com/healthyhost/audit-vps-script">GitHub</a>. It would really help with exposure (and staying ahead of the copycats!).</p>]]></content:encoded></item><item><title><![CDATA[How to Install Grafana and Prometheus on your VPS: Complete Guide 2024]]></title><description><![CDATA[A comprehensive guide for developers who want complete control and real-time monitoring of their VPS - from system metrics to container performance and everything in between.]]></description><link>https://www.kkyri.com/p/how-to-install-grafana-and-prometheus</link><guid isPermaLink="false">https://www.kkyri.com/p/how-to-install-grafana-and-prometheus</guid><dc:creator><![CDATA[Kyri]]></dc:creator><pubDate>Tue, 12 Nov 2024 10:12:55 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!FI28!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87d7950f-8d61-4ccc-91db-01b4d7da6d1b_3414x1954.heic" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!FI28!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87d7950f-8d61-4ccc-91db-01b4d7da6d1b_3414x1954.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!FI28!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87d7950f-8d61-4ccc-91db-01b4d7da6d1b_3414x1954.heic 424w, https://substackcdn.com/image/fetch/$s_!FI28!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87d7950f-8d61-4ccc-91db-01b4d7da6d1b_3414x1954.heic 848w, https://substackcdn.com/image/fetch/$s_!FI28!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87d7950f-8d61-4ccc-91db-01b4d7da6d1b_3414x1954.heic 1272w, https://substackcdn.com/image/fetch/$s_!FI28!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87d7950f-8d61-4ccc-91db-01b4d7da6d1b_3414x1954.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!FI28!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87d7950f-8d61-4ccc-91db-01b4d7da6d1b_3414x1954.heic" width="1456" height="833" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/87d7950f-8d61-4ccc-91db-01b4d7da6d1b_3414x1954.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:833,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:584610,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!FI28!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87d7950f-8d61-4ccc-91db-01b4d7da6d1b_3414x1954.heic 424w, https://substackcdn.com/image/fetch/$s_!FI28!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87d7950f-8d61-4ccc-91db-01b4d7da6d1b_3414x1954.heic 848w, https://substackcdn.com/image/fetch/$s_!FI28!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87d7950f-8d61-4ccc-91db-01b4d7da6d1b_3414x1954.heic 1272w, https://substackcdn.com/image/fetch/$s_!FI28!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87d7950f-8d61-4ccc-91db-01b4d7da6d1b_3414x1954.heic 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">All the server monitoring you need, no pole installation required</figcaption></figure></div><h2>Why use Grafana and Prometheus on your VPS</h2><p>Ever wondered what's really going on inside your VPS? Grafana and Prometheus are your go-to tools for keeping tabs on your server's performance. They're a fantastic combo for indie hackers like us - they're free, open-source, and they get the job done without the overhead of enterprise solutions.</p><p>Here's what makes them a great option:</p><ul><li><p><strong>See everything in real-time</strong>: Monitor your server metrics as they happen</p></li><li><p><strong>Pretty dashboards</strong>: Grafana turns numbers into useful charts</p></li><li><p><strong>Zero cost</strong>: It's open-source, so no surprise bills or vendor lock-in</p></li><li><p><strong>Large community</strong>: Tons of pre-made dashboards and help when you need it</p></li></ul><p>The best part? You don't need to be a Linux wizard to set this up. If you can follow simple instructions and aren't afraid of the command line, you're good to go. Let's dive in and get your VPS properly monitored.</p><h2>What you&#8217;ll need before we start</h2><p>Before we dive in, let's make sure you've got the basics sorted. Nothing fancy - just Docker and a user account that can run sudo commands.</p><p>Here's your quick checklist:</p><ul><li><p>A user with sudo</p></li><li><p>Docker installed and running</p></li></ul><p>If you've got these three things, you're ready to go. Not sure if you have everything? Run these commands to check:</p><pre><code># Verify sudo access
$ sudo whoami

# Check Docker installation
$ docker --version</code></pre><h2>Setting up Prometheus &amp; Grafana</h2><p>Let's get these tools installed and running. We'll use Docker Compose to keep things clean and simple. I'll walk you through it step by step.</p><p>First, create a <code>compose.yaml</code> file:</p><pre><code>services:
  prometheus:
    image: prom/prometheus
    ports:
      - '9090:9090'
    volumes:
      - ./prometheus:/etc/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
    restart: always

  grafana:
    image: grafana/grafana
    ports:
      - '3000:3000'
    depends_on:
      - prometheus
    restart: always

  node_exporter:
    image: quay.io/prometheus/node-exporter:latest
    container_name: node_exporter
    command:
      - '--path.rootfs=/host'
    ports:
      - '9100:9100'
    restart: unless-stopped
    volumes:
      - '/:/host:ro,rslave'</code></pre><p>This sets up three containers:</p><ul><li><p><strong>Prometheus</strong> - collects and stores your metrics</p></li><li><p><strong>Grafana</strong> - visualises those metrics into dashboards</p></li><li><p><strong>Node</strong> <strong>Exporter</strong> - grabs system stats like CPU and memory usage</p></li></ul><p>Next, create a configuration file for Prometheus. Make a new directory called <code>prometheus</code> and create a <code>prometheus.yaml</code> file inside it:</p><pre><code>global:
  scrape_interval: 10s

scrape_configs:
  - job_name: node
    static_configs:
      - targets: ['node_exporter:9100']</code></pre><p>This tells Prometheus two things:</p><ul><li><p>Collect new metrics every 10 seconds</p></li><li><p>Look for those metrics at <code>node_exporter:9100</code> (that's the Node Exporter container we defined earlier)</p></li></ul><p>That's all we need for now. Prometheus will store these metrics, and we'll use Grafana to visualise them in the next step.</p><h2>Accessing your monitoring stack over SSH</h2><p>Time to start your monitoring stack and connect to it securely. I'll show you how to do this without exposing any of the services to the public internet.</p><p>First, start up your containers:</p><pre><code>$ docker compose up -d</code></pre><p>Now, we could just open these ports on our VPS firewall, but that's not the most secure approach. We&#8217;d rather keep the ports locked down, and instead connect to them over SSH, using something called port forwarding.</p><blockquote><p>If you haven&#8217;t already setup a firewall on your VPS, check out my <a href="https://medium.com/@kkyri/8db251139160">article on how to secure your VPS</a>. The only ports that should be exposed are 22 for SSH and 80/443 for HTTP/HTTPS if you&#8217;re running a web server.</p></blockquote><p>Open two terminal windows on your local machine and run these commands (replace <code>user@12.34.45.67</code> with your VPS details):</p><pre><code># Terminal 1 - Grafana tunnel
$ ssh -N -L 3000:127.0.0.1:3000 user@12.34.45.67

# Terminal 2 - Prometheus tunnel
$ ssh -N -L 9090:127.0.0.1:9090 user@12.34.45.67</code></pre><p>This creates secure tunnels from your computer to your VPS. Now you can access everything through your browser on your local machine at:</p><ul><li><p><strong>Grafana</strong>: http://localhost:3000</p></li><li><p><strong>Prometheus</strong>: http://localhost:9090</p></li></ul><p>Let's make sure everything's working. Open Prometheus at <code>http://localhost:9090</code> in your browser, click <strong>Status &gt; Targets</strong>. You should see your node exporter listed as "UP" - that means Prometheus is successfully collecting your system metrics.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0sxl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27df43bb-8b43-45c3-8ca0-045e679ff225_1850x420.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0sxl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27df43bb-8b43-45c3-8ca0-045e679ff225_1850x420.png 424w, https://substackcdn.com/image/fetch/$s_!0sxl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27df43bb-8b43-45c3-8ca0-045e679ff225_1850x420.png 848w, https://substackcdn.com/image/fetch/$s_!0sxl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27df43bb-8b43-45c3-8ca0-045e679ff225_1850x420.png 1272w, https://substackcdn.com/image/fetch/$s_!0sxl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27df43bb-8b43-45c3-8ca0-045e679ff225_1850x420.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0sxl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27df43bb-8b43-45c3-8ca0-045e679ff225_1850x420.png" width="1456" height="331" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/27df43bb-8b43-45c3-8ca0-045e679ff225_1850x420.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:331,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:93367,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0sxl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27df43bb-8b43-45c3-8ca0-045e679ff225_1850x420.png 424w, https://substackcdn.com/image/fetch/$s_!0sxl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27df43bb-8b43-45c3-8ca0-045e679ff225_1850x420.png 848w, https://substackcdn.com/image/fetch/$s_!0sxl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27df43bb-8b43-45c3-8ca0-045e679ff225_1850x420.png 1272w, https://substackcdn.com/image/fetch/$s_!0sxl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27df43bb-8b43-45c3-8ca0-045e679ff225_1850x420.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Prometheus successfully communicating with Node Exporter</figcaption></figure></div><p>Looking good? Let's set up Grafana next.</p><h2>Connecting Grafana to your data</h2><p>Let's get Grafana talking to Prometheus. First thing's first - log into Grafana:</p><ol><li><p>Open <code>http://localhost:3000</code> in your browser</p></li><li><p>Use these default credentials:</p><ol><li><p>Username: <code>admin</code></p></li><li><p>Password: <code>admin</code></p></li></ol></li><li><p>Set a new, strong password when prompted - especially important if you ever plan to expose this to the internet</p></li></ol><p>Now, let's point Grafana to your Prometheus data:</p><ol><li><p>Click <strong>Connections &gt; Data sources</strong> in the left sidebar</p></li><li><p>Hit <strong>Add data source</strong></p></li><li><p>Choose <strong>Prometheus</strong> from the list</p></li><li><p>For the <strong>URL</strong>, enter <code>http://prometheus:9090</code></p></li><li><p>Scroll down and click <strong>Save &amp; test</strong></p></li></ol><p>You should see a green success message. If you do, congrats! Grafana can now see all your metrics. If not, double-check that URL - the most common gotcha is using <code>localhost</code> instead of <code>prometheus</code> as the hostname.</p><h2>Creating your first dashboard</h2><p>Rather than building a dashboard from scratch (which takes time), let's grab a pre-made one that works perfectly with our Node Exporter.</p><p>Here&#8217;s how:</p><ol><li><p>Click <strong>Dashboards</strong> in the left sidebar</p></li><li><p>Hit <strong>New &gt; Import</strong> in the top right</p></li><li><p>Enter <code>1860</code> - this is the ID for the dashboard template <a href="https://grafana.com/grafana/dashboards/1860-node-exporter-full/">Node Exporter Full</a></p></li><li><p>Click <strong>Load</strong></p></li><li><p>Give your dashboard a name if you want, or keep the default</p></li><li><p>Select your Prometheus data source from the dropdown at the bottom</p></li><li><p>Click <strong>Import</strong></p></li></ol><p>You should now see a complete dashboard showing all sorts of system metrics: CPU usage, memory, disk space, network traffic, basically everything you would ever need to monitor host-level resources.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!EkGd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0748f065-d3cb-4917-a4f8-8b2b31bfa679_1324x634.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!EkGd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0748f065-d3cb-4917-a4f8-8b2b31bfa679_1324x634.png 424w, https://substackcdn.com/image/fetch/$s_!EkGd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0748f065-d3cb-4917-a4f8-8b2b31bfa679_1324x634.png 848w, https://substackcdn.com/image/fetch/$s_!EkGd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0748f065-d3cb-4917-a4f8-8b2b31bfa679_1324x634.png 1272w, https://substackcdn.com/image/fetch/$s_!EkGd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0748f065-d3cb-4917-a4f8-8b2b31bfa679_1324x634.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!EkGd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0748f065-d3cb-4917-a4f8-8b2b31bfa679_1324x634.png" width="1324" height="634" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0748f065-d3cb-4917-a4f8-8b2b31bfa679_1324x634.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:634,&quot;width&quot;:1324,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:218403,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!EkGd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0748f065-d3cb-4917-a4f8-8b2b31bfa679_1324x634.png 424w, https://substackcdn.com/image/fetch/$s_!EkGd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0748f065-d3cb-4917-a4f8-8b2b31bfa679_1324x634.png 848w, https://substackcdn.com/image/fetch/$s_!EkGd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0748f065-d3cb-4917-a4f8-8b2b31bfa679_1324x634.png 1272w, https://substackcdn.com/image/fetch/$s_!EkGd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0748f065-d3cb-4917-a4f8-8b2b31bfa679_1324x634.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The memory visualisation in the dashboard</figcaption></figure></div><h2>Adding Docker metrics to your dashboard</h2><p>Since we're running everything in Docker anyway, let's monitor our containers too. We'll tell Docker to expose its metrics and get Prometheus to collect them.</p><p>First, let's configure Docker to make its metrics available:</p><pre><code># Create or edit the Docker daemon config
$ sudo vim /etc/docker/daemon.json

# Add this configuration
{
  "metrics-addr": "0.0.0.0:9323"
}

# Restart Docker to apply changes
$ sudo systemctl daemon-reload</code></pre><p>Quick test to make sure it's working:</p><pre><code>$ curl localhost:9323/metrics</code></pre><p>You should see a bunch of metrics like <code>builder_builds_failed_total</code> and others.</p><p>Here comes the tricky part - if you're using UFW with <code>ufw-docker</code> (as I recommend to do in my <a href="https://www.kkyri.com/p/how-to-secure-docker-web-app-over">Docker article</a>), we need to allow Prometheus talk to Docker. We'll do it safely such that inbound traffic to Docker is only allowed by internal IPs:</p><pre><code># Allow Docker's internal network to access the metrics
$ sudo ufw allow from 172.16.0.0/12 to any port 9323
$ sudo ufw reload</code></pre><blockquote><p>&#128161; Quick networking note: 172.16.0.0/12 is Docker's internal network range. We're only allowing connections from inside this network, so it cannot be accessed from the public internet.</p></blockquote><p>Now let's tell Prometheus about these new metrics. Edit your Prometheus config:</p><pre><code>$ vim prometheus/prometheus.yaml</code></pre><p>Add this under <code>scrape_configs</code>:</p><pre><code>  - job_name: docker
    static_configs:
      - targets: ['host.docker.internal:9323']</code></pre><p>We also need to modify our <code>compose.yaml</code> file to tell Prometheus about this special Docker host:</p><pre><code>$ vim compose.yaml

# add the following as a direct child under the prometheus service
    extra_hosts:
      - "host.docker.internal:host-gateway"</code></pre><p>Restart Prometheus to pick up the changes:</p><pre><code>$ docker compose restart prometheus</code></pre><p>To verify everything's working, check <code>http://localhost:9090/targets</code> - you should see a Docker target marked as "UP".</p><h2>Creating a custom dashboard</h2><p>Let's make a simple dashboard to monitor your Docker containers. We'll build this one from scratch.</p><p>Here's how:</p><ol><li><p>Go to <strong>Dashboards</strong> and click <strong>New &gt; New dashboard</strong></p></li><li><p>Click the big <strong>+ Add visualization</strong> button</p></li><li><p>Pick Prometheus as your data source</p></li><li><p>Change the visualization type from <strong>Time series</strong> to <strong>Stat</strong> in the top right</p></li><li><p>In the query box at the bottom, paste this:</p></li></ol><pre><code>engine_daemon_container_states_containers</code></pre><ol start="6"><li><p>Under <strong>Options</strong>, find the <strong>Legend</strong> section and set it to <strong>Custom</strong> with value <code>{{state}}</code></p></li><li><p>Hit <strong>Run queries</strong></p></li></ol><p>You should now see a clean visualisation showing how many containers you have running, paused, or stopped. Neat, right?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RIZa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7129a2ac-5ce9-42ba-b5b9-710e53b83325_1988x1374.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RIZa!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7129a2ac-5ce9-42ba-b5b9-710e53b83325_1988x1374.png 424w, https://substackcdn.com/image/fetch/$s_!RIZa!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7129a2ac-5ce9-42ba-b5b9-710e53b83325_1988x1374.png 848w, https://substackcdn.com/image/fetch/$s_!RIZa!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7129a2ac-5ce9-42ba-b5b9-710e53b83325_1988x1374.png 1272w, https://substackcdn.com/image/fetch/$s_!RIZa!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7129a2ac-5ce9-42ba-b5b9-710e53b83325_1988x1374.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RIZa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7129a2ac-5ce9-42ba-b5b9-710e53b83325_1988x1374.png" width="1456" height="1006" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7129a2ac-5ce9-42ba-b5b9-710e53b83325_1988x1374.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1006,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:222475,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!RIZa!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7129a2ac-5ce9-42ba-b5b9-710e53b83325_1988x1374.png 424w, https://substackcdn.com/image/fetch/$s_!RIZa!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7129a2ac-5ce9-42ba-b5b9-710e53b83325_1988x1374.png 848w, https://substackcdn.com/image/fetch/$s_!RIZa!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7129a2ac-5ce9-42ba-b5b9-710e53b83325_1988x1374.png 1272w, https://substackcdn.com/image/fetch/$s_!RIZa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7129a2ac-5ce9-42ba-b5b9-710e53b83325_1988x1374.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Custom visualisation with status of Docker containers</figcaption></figure></div><blockquote><p>&#128161; Want to explore more Docker metrics? Head to Prometheus at <code>http://localhost:9090/graph</code> and try this query: <code>{job="docker"}</code>. This shows you every Docker metric available to interact with!</p></blockquote><p>Give your dashboard a name, hit save, and you're done! Feel free to experiment - add more panels, try different visualisations, or even add exporters for metrics from other systems. That's the good part about Grafana and Prometheus - you can customise everything to show exactly what you want to see.</p><h2>And that&#8217;s a wrap</h2><p>You've just set up a proper monitoring system for your VPS - pat yourself on the back! Yes, it took a bit more work than clicking an install button, but now you've got something super flexible up and running.</p><p>Here's what you've accomplished:</p><ul><li><p>Got Prometheus, Grafana, and Node Exporter running using Docker Compose</p></li><li><p>Set up Docker to expose its own metrics</p></li><li><p>Configured your firewall safely to allow internal collection of Docker metrics</p></li><li><p>Got Prometheus collecting metrics from both your system and Docker</p></li><li><p>Set up two dashboards:</p><ul><li><p>A pre-made one for host-level metrics</p></li><li><p>A custom one for Docker stats</p></li></ul></li></ul><blockquote><p>&#128161; Pro tip: If you&#8217;re feeling adventurous, look into Prometheus alerting. You can set up alerts for things like high CPU usage, low disk space, when containers crash, and much more.</p></blockquote><p>Your VPS is no longer a black box - you can now see exactly what's going on inside it. Happy self-hosting!</p>]]></content:encoded></item><item><title><![CDATA[How to Secure Your Docker Applications with Nginx and HTTPS]]></title><description><![CDATA[Transform your Docker application into a production-ready service with a custom domain, HTTPS, and automated TLS certificate renewal - all using free, industry-standard tools.]]></description><link>https://www.kkyri.com/p/how-to-secure-docker-web-app-over</link><guid isPermaLink="false">https://www.kkyri.com/p/how-to-secure-docker-web-app-over</guid><dc:creator><![CDATA[Kyri]]></dc:creator><pubDate>Tue, 29 Oct 2024 09:48:56 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!yDiX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff408dc58-cacf-4aa7-baa9-3bc1e428f4ad_6000x4000.heic" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yDiX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff408dc58-cacf-4aa7-baa9-3bc1e428f4ad_6000x4000.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yDiX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff408dc58-cacf-4aa7-baa9-3bc1e428f4ad_6000x4000.heic 424w, https://substackcdn.com/image/fetch/$s_!yDiX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff408dc58-cacf-4aa7-baa9-3bc1e428f4ad_6000x4000.heic 848w, https://substackcdn.com/image/fetch/$s_!yDiX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff408dc58-cacf-4aa7-baa9-3bc1e428f4ad_6000x4000.heic 1272w, https://substackcdn.com/image/fetch/$s_!yDiX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff408dc58-cacf-4aa7-baa9-3bc1e428f4ad_6000x4000.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yDiX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff408dc58-cacf-4aa7-baa9-3bc1e428f4ad_6000x4000.heic" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f408dc58-cacf-4aa7-baa9-3bc1e428f4ad_6000x4000.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2019080,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yDiX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff408dc58-cacf-4aa7-baa9-3bc1e428f4ad_6000x4000.heic 424w, https://substackcdn.com/image/fetch/$s_!yDiX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff408dc58-cacf-4aa7-baa9-3bc1e428f4ad_6000x4000.heic 848w, https://substackcdn.com/image/fetch/$s_!yDiX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff408dc58-cacf-4aa7-baa9-3bc1e428f4ad_6000x4000.heic 1272w, https://substackcdn.com/image/fetch/$s_!yDiX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff408dc58-cacf-4aa7-baa9-3bc1e428f4ad_6000x4000.heic 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The satisfying sight of a padlock in the address bar</figcaption></figure></div><h1>Introduction</h1><p>This article builds on my <a href="https://www.kkyri.com/p/simple-way-to-deploy-your-services">previous guide</a> about deploying a Docker web server on a VPS. While functional, the current setup exposes the server directly on port 80 without TLS encryption. We'll address these limitations by adding Nginx as a reverse proxy and securing traffic with TLS using LetsEncrypt certificates.</p><p>Prerequisites:</p><ul><li><p>A Docker web server running on port 80</p></li><li><p>A domain or subdomain with DNS management access</p></li></ul><h1>Understanding Nginx and deployment options</h1><h2>Overview</h2><p>Nginx is a mature web server that functions both as a reverse proxy and static file server, amongst other things. As a reverse proxy, it provides an additional security layer by concealing backend implementation details and offers features like rate limiting, TLS termination, request routing, and much more.</p><h2>Deployment Approaches</h2><p>When setting up Nginx as a reverse proxy, you'll need to choose between installing it directly on your host system or running it as a containerized service. Each approach has distinct implications for how Nginx interacts with your backend services, manages configurations, and handles TLS certificates. The choice largely depends on your existing infrastructure and personal preferences.</p><h3>Host installation</h3><p>Running Nginx directly on the host offers several advantages:</p><ul><li><p>Ability to proxy to any service, containerized or not</p></li><li><p>Direct access to host filesystem for static content</p></li><li><p>Simpler initial setup and configuration</p></li></ul><p>Key limitations:</p><ul><li><p>Management and monitoring separate from Docker ecosystem (via systemd)</p></li><li><p>Docker services must expose host ports for Nginx connectivity</p></li><li><p>Scaling up a Docker service using replicas will lead to port conflicts, since two containers cannot listen on the same host port. To work around this each replica needs to listen on a different host port and Nginx needs to be configured to be aware of them, so that it can balance the load across them.</p></li></ul><h3>Docker installation</h3><p>Deploying Nginx in a container provides:</p><ul><li><p>Native Docker network integration.</p></li><li><p>Automatic DNS resolution between containers</p></li><li><p>Can make use of Docker&#8217;s built-in load balancing for scaled services. Services are scaled up and down without having to modify the Nginx configuration. (That being said, Nginx load balancing is more feature-rich than Docker&#8217;s.)</p></li></ul><p>However, this approach requires:</p><ul><li><p>Volume management for configuration and TLS certificates</p></li><li><p>More complex initial configuration</p></li><li><p>Additional networking setup to proxy traffic to non-containerized services running on the host</p></li></ul><p>This article will focus on the first method, host installation, as it provides greater flexibility for proxying both containerized and traditional services, and is the simpler of the two.</p><h2>Initial setup</h2><p>Before installing Nginx, we need to prepare the host system. Since Nginx will handle incoming traffic on port 80, we'll need to stop any existing services using that port. In our case, that's the Docker container from the <a href="https://www.kkyri.com/p/simple-way-to-deploy-your-services">previous article</a>:</p><pre><code>$ docker stop app</code></pre><p>Next, we'll configure the firewall to allow HTTP and HTTPS traffic. Ubuntu's UFW (Uncomplicated Firewall) provides a straightforward way to manage these rules:</p><pre><code><code>$ sudo ufw status</code></code></pre><p>Expected output:</p><pre><code><code>Status: active

To                         Action      From
--                         ------      ----
Nginx Full                 ALLOW       Anywhere
Nginx Full (v6)            ALLOW       Anywhere (v6)</code></code></pre><p>If you don't see these Nginx rules, add them:</p><pre><code><code>$ sudo ufw allow 'Nginx HTTP'</code></code></pre><p>This single rule enables both HTTP (port 80) and HTTPS (port 443) traffic. If your firewall isn't active yet:</p><pre><code>$ sudo ufw enable</code></pre><h2>Installing Nginx</h2><p>On Debian systems, install Nginx using apt:</p><pre><code>$ sudo apt update
$ sudo apt install nginx</code></pre><blockquote><p>For other distributions, refer to the <a href="https://nginx.org/en/docs/install.html">official installation instructions</a></p></blockquote><p>After installation, Nginx starts automatically. Verify the service status:</p><pre><code>$ systemctl status nginx</code></pre><p>Expected output:</p><pre><code>&#9679; nginx.service - A high performance web server and reverse proxy server
     ...
     Active: active (running) since Sun 2024-10-13 16:26:23 UTC; ...</code></pre><p>To verify the installation, we'll access Nginx's default landing page. First, find your VPS's public IP address:</p><pre><code>$ curl ip.now</code></pre><p>Example output:</p><pre><code>12.34.56.78</code></pre><p>Visit this IP address in your browser. You should see the default Nginx welcome page, confirming that both the installation and port 80 access are working correctly.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!gp3l!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2c0499-7421-41b0-a35f-d11a636863b8_577x212.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!gp3l!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2c0499-7421-41b0-a35f-d11a636863b8_577x212.heic 424w, https://substackcdn.com/image/fetch/$s_!gp3l!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2c0499-7421-41b0-a35f-d11a636863b8_577x212.heic 848w, https://substackcdn.com/image/fetch/$s_!gp3l!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2c0499-7421-41b0-a35f-d11a636863b8_577x212.heic 1272w, https://substackcdn.com/image/fetch/$s_!gp3l!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2c0499-7421-41b0-a35f-d11a636863b8_577x212.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!gp3l!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2c0499-7421-41b0-a35f-d11a636863b8_577x212.heic" width="577" height="212" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ec2c0499-7421-41b0-a35f-d11a636863b8_577x212.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:212,&quot;width&quot;:577,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:27950,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!gp3l!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2c0499-7421-41b0-a35f-d11a636863b8_577x212.heic 424w, https://substackcdn.com/image/fetch/$s_!gp3l!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2c0499-7421-41b0-a35f-d11a636863b8_577x212.heic 848w, https://substackcdn.com/image/fetch/$s_!gp3l!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2c0499-7421-41b0-a35f-d11a636863b8_577x212.heic 1272w, https://substackcdn.com/image/fetch/$s_!gp3l!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec2c0499-7421-41b0-a35f-d11a636863b8_577x212.heic 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">nginx default page</figcaption></figure></div><h1>Understanding Nginx configuration</h1><h2>Default configuration</h2><p>Let's examine Nginx's default configuration at <code>/etc/nginx/sites-available/default</code> to understand how Nginx handles requests:</p><pre><code><code>$ cat /etc/nginx/sites-available/default</code></code></pre><p>The key components are:</p><pre><code><code>listen 80 default_server;
listen [::]:80 default_server;</code></code></pre><p>This configures Nginx to accept both IPv4 and IPv6 connections on port 80. The <code>default_server</code> directive makes this block handle any requests that don't match other server blocks.</p><pre><code><code>root /var/www/html;</code></code></pre><p><code>root</code> defines the base directory for static file serving. We&#8217;ll explore this soon.</p><pre><code><code>index index.html index.htm index.nginx-debian.html;</code></code></pre><p><code>index</code> the file lookup order within the root directory.</p><pre><code><code>server_name _;</code></code></pre><p>The underscore (<code>_</code>) is a catch-all character that makes this server block catch all unmatched domain requests. Currently, there are no other server blocks, so this will catch all requests.</p><pre><code><code>location / {
    try_files $uri $uri/ =404;
}</code></code></pre><p>This location block implements a simple request handling logic:</p><ol><li><p>Attempt to serve the exact URI</p></li><li><p>Try serving it as a directory</p></li><li><p>Return 404 if nothing matches</p></li></ol><p>To find the default landing page, take a look at the directory that <code>root</code> is pointing to:</p><pre><code><code>$ ls /var/root/html
index.nginx-debian.html</code></code></pre><p>While we'll be configuring Nginx as a reverse proxy rather than a static file server, understanding this default configuration provides context for our next steps.</p><h2>Configuring Nginx as a reverse proxy</h2><p>While we could modify the default configuration to set up our reverse proxy, it's better to create a separate configuration file for each domain. This approach makes it easier to manage multiple services and domains on a single VPS.</p><p>We'll create a new configuration file named after our domain. Using the domain name as the filename is a common convention that makes it clear which service the configuration belongs to:</p><pre><code>$ sudo vim /etc/nginx/sites-available/api.kkyri.com</code></pre><blockquote><p>You should replace <code>api.kkyri.com</code> with your own domain</p></blockquote><p>Add this configuration:</p><pre><code>server {
    listen 80;
    listen [::]:80;

    server_name api.kkyri.com;

    location / {
        proxy_pass http://localhost:8080;
        include proxy_params;
    }
}</code></pre><p>Let's break down what makes this configuration different from the default:</p><ul><li><p><code>server_name api.kkyri.com</code> tells Nginx to apply this configuration only for requests to this specific domain (it looks at the request&#8217;s <code>Host</code> header to determine this)</p></li><li><p><code>proxy_pass</code> forwards requests to our application instead of serving static files</p></li><li><p><code>include proxy_params</code> adds standard proxy headers like <code>X-Real-IP</code> and <code>X-Forwarded-For</code>, which help our application understand the origin of the request</p></li></ul><p>This domain-based configuration approach is particularly powerful - you can host multiple services on your VPS by creating additional server blocks with different domain names, each proxying to their respective services. For example, I could add another configuration for <code>docs.kkyri.com</code>, which points to a different underlying service.</p><p>Enable the configuration by creating a symbolic link in <code>sites-enabled</code>:</p><pre><code>$ sudo ln -s /etc/nginx/sites-available/api.kkyri.com /etc/nginx/sites-enabled/</code></pre><p>Validate the configuration:</p><pre><code>$ sudo nginx -t</code></pre><p>Before restarting Nginx, we'll need to ensure our application is running on port 8080. Let's handle that in the next section.</p><h2>Setting up the backend service</h2><p>Now we need to reconfigure our Docker service to run on port 8080 instead of 80, matching our Nginx configuration.</p><p>Update the port mapping in your <code>compose.yaml</code>:</p><pre><code>services:
  app:
    image: app:latest
    container_name: app
    ports:
      - "8080:8080" # Changed from 80:8080
    ...</code></pre><blockquote><p>The port mapping format is <code>HOST_PORT:CONTAINER_PORT</code>. We're changing only the host port since that's what Nginx will connect to. The container's internal port remains the same.</p></blockquote><p>Deploy the changes:</p><pre><code>$ docker compose up -d</code></pre><p>Finally, apply the new Nginx configuration:</p><pre><code>$ sudo systemctl reload nginx</code></pre><p>At this point, Nginx should be forwarding requests from port 80 to your application running on port 8080. The next step is configuring DNS to route traffic to your VPS.</p><h2>Configuring DNS records</h2><p>While Nginx is now set up to proxy requests, we need to configure DNS to route domain traffic to our VPS. Currently, accessing your VPS's IP address directly still shows the default Nginx page because the request isn&#8217;t originating from the domain name we configured.</p><p>At your domain registrar's DNS settings, you'll need to add one of the following:</p><ul><li><p>An <code>A</code> record if your VPS has a static IP:</p></li></ul><pre><code>Type: A
Name: api.kkyri.com
Value: 12.34.56.78</code></pre><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!blZV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F906209e5-7867-4d3d-a7a2-1443a65ef655_1436x156.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!blZV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F906209e5-7867-4d3d-a7a2-1443a65ef655_1436x156.heic 424w, https://substackcdn.com/image/fetch/$s_!blZV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F906209e5-7867-4d3d-a7a2-1443a65ef655_1436x156.heic 848w, https://substackcdn.com/image/fetch/$s_!blZV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F906209e5-7867-4d3d-a7a2-1443a65ef655_1436x156.heic 1272w, https://substackcdn.com/image/fetch/$s_!blZV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F906209e5-7867-4d3d-a7a2-1443a65ef655_1436x156.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!blZV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F906209e5-7867-4d3d-a7a2-1443a65ef655_1436x156.heic" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/906209e5-7867-4d3d-a7a2-1443a65ef655_1436x156.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:19230,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!blZV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F906209e5-7867-4d3d-a7a2-1443a65ef655_1436x156.heic 424w, https://substackcdn.com/image/fetch/$s_!blZV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F906209e5-7867-4d3d-a7a2-1443a65ef655_1436x156.heic 848w, https://substackcdn.com/image/fetch/$s_!blZV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F906209e5-7867-4d3d-a7a2-1443a65ef655_1436x156.heic 1272w, https://substackcdn.com/image/fetch/$s_!blZV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F906209e5-7867-4d3d-a7a2-1443a65ef655_1436x156.heic 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Example A record on Cloudflare</figcaption></figure></div><ul><li><p>A <code>CNAME</code> record if your VPS provider gives you a domain name:</p></li></ul><pre><code>Type: CNAME
Name: api.kkyri.com
Value: kkyri-api.provider.com</code></pre><p>Most VPS providers assign static IPs, making the <code>A</code> record the more common choice. However, if you're using a platform that provides a domain name by default instead, use a <code>CNAME</code> record.</p><p>After configuring DNS, changes can take anywhere from a few seconds to several hours to propagate through the DNS network. Once propagation is complete, visiting your domain should show your application's response:</p><pre><code>Hello, World!</code></pre><h2>Debugging connection issues</h2><p>If your application isn't accessible through your domain, there are several layers to check. Let's go through them systematically.</p><h3>DNS resolution</h3><p>First, verify your DNS configuration using the <code>dig</code> command:</p><pre><code># For A records
$ dig api.kkyri.com A

;; ANSWER SECTION:
api.kkyri.com    60    IN    A    12.34.56.78

# For CNAME records
$ dig api.kkyri.com CNAME

;; ANSWER SECTION:
api.kkyri.com    60    IN    CNAME    kkyri-api.provider.com</code></pre><p>Look for the ANSWER sections and verify they&#8217;re pointing to the expected IP address or domain.</p><h3>Nginx logs</h3><p>Nginx maintains two primary log files that are useful for debugging:</p><ul><li><p>Access logs show incoming requests. Each request should result in a log line:</p></li></ul><pre><code>$ sudo tail -f /var/log/nginx/access.log
192.168.1.1 - - [27/Oct/2024:21:15:23 +0000] "GET / HTTP/1.1" 200 ...</code></pre><ul><li><p>Error logs show configuration issues or other errors:</p></li></ul><pre><code>$ sudo cat /var/log/nginx/error.log</code></pre><h3>Application logs</h3><p>If DNS resolves correctly and Nginx logs show incoming requests, check your application:</p><ol><li><p>Verify the container is running:</p></li></ol><pre><code>$ docker ps
CONTAINER ID   IMAGE  STATUS         PORTS                   NAMES
9e28276170ab   app    Up 30 minutes  0.0.0.0:8080-&gt;8080/tcp  app-1</code></pre><ol start="2"><li><p>Check container logs:</p></li></ol><pre><code>$ docker logs app-1
Server is listening on :8080...</code></pre><p>If it&#8217;s not doing it already, consider modifying your application code to log incoming requests, to assist you with debugging. Remember to restart your container after any code changes.</p><p>This layered approach helps isolate whether the issue is with DNS resolution, Nginx configuration, or the application itself.</p><h1>Enabling HTTPS with Certbot</h1><h2>Prerequisites</h2><p>Before proceeding, ensure:</p><ul><li><p>Your domain is correctly pointing to your VPS (verify with <code>dig</code> and/or your browser)</p></li><li><p>Nginx is properly configured and serving requests</p></li><li><p>Port 80 and 443 is open in your firewall</p></li></ul><h2>Installing Certbot</h2><p>To serve traffic over HTTPS, we need TLS certificates. Let's Encrypt provides these certificates for free, and we'll use Certbot to automate their management. We'll also use Certbot's Nginx plugin to automatically configure TLS in our Nginx server.</p><p>Install Certbot and the Nginx plugin:</p><pre><code>$ sudo apt install certbot python3-certbot-nginx</code></pre><p>Generate a certificate for your domain:</p><pre><code>$ sudo certbot --nginx -d api.kkyri.com</code></pre><blockquote><p>The domain you pass to Certbot must be the same domain you configured in your Nginx server block.</p></blockquote><p>This command does several things:</p><ul><li><p>Verifies your domain ownership</p></li><li><p>Generates TLS certificates</p></li><li><p>Updates your Nginx configuration to use these certificates</p></li><li><p>Sets up automatic HTTP to HTTPS redirection</p></li></ul><p>Certificates are valid for 90 days. Certbot sets up automatic renewal via a systemd timer:</p><pre><code>$ sudo systemctl status certbot.timer
&#9679; certbot.timer - Run certbot twice daily
     Loaded: loaded (/lib/systemd/system/certbot.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Sat 2024-10-12 10:49:21 UTC; 2min 52s ago
    Trigger: Sat 2024-10-12 16:19:52 UTC; 5h 27min left
   Triggers: &#9679; certbot.service

Oct 12 10:49:21 vps systemd[1]: Started Run certbot twice daily.</code></pre><p>(Optional) To test the renewal process:</p><pre><code>$ sudo certbot renew --dry-run</code></pre><p>This will simulate a renewal for your domain.</p><p>To understand the changes that Certbot made to your Nginx configuration, open it:</p><pre><code>$ cat /etc/nginx/sites-available/api.kkyri.com</code></pre><p>Certbot has added:</p><ul><li><p>Certificate paths</p></li><li><p>TLS configuration</p></li><li><p>HTTP to HTTPS redirect</p></li><li><p>Port 443 listener</p></li></ul><p>Note that Nginx still communicates with your backend service over HTTP internally. This is secure as it happens within your local network, and it simplifies your application architecture by handling TLS termination at the Nginx level.</p><p>Your site should now be accessible via HTTPS, with browser security indicators confirming the valid certificate.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!oaOs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa529243e-1af8-4887-8fce-5ac884476326_1166x334.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!oaOs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa529243e-1af8-4887-8fce-5ac884476326_1166x334.heic 424w, https://substackcdn.com/image/fetch/$s_!oaOs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa529243e-1af8-4887-8fce-5ac884476326_1166x334.heic 848w, https://substackcdn.com/image/fetch/$s_!oaOs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa529243e-1af8-4887-8fce-5ac884476326_1166x334.heic 1272w, https://substackcdn.com/image/fetch/$s_!oaOs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa529243e-1af8-4887-8fce-5ac884476326_1166x334.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!oaOs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa529243e-1af8-4887-8fce-5ac884476326_1166x334.heic" width="1166" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a529243e-1af8-4887-8fce-5ac884476326_1166x334.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1166,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:29129,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!oaOs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa529243e-1af8-4887-8fce-5ac884476326_1166x334.heic 424w, https://substackcdn.com/image/fetch/$s_!oaOs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa529243e-1af8-4887-8fce-5ac884476326_1166x334.heic 848w, https://substackcdn.com/image/fetch/$s_!oaOs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa529243e-1af8-4887-8fce-5ac884476326_1166x334.heic 1272w, https://substackcdn.com/image/fetch/$s_!oaOs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa529243e-1af8-4887-8fce-5ac884476326_1166x334.heic 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">A padlock. Nice!</figcaption></figure></div><h1>Summary</h1><p>Well done! The setup is now complete. You have:</p><ul><li><p>A reverse proxy handling incoming traffic</p></li><li><p>Automatic HTTPS encryption with Let's Encrypt certificates, and automatic renewal</p></li><li><p>Your domain properly configured and routing to your VPS</p></li><li><p>Routing completely decoupled from your service</p></li></ul><p>The infrastructure you&#8217;ve configured thus far is secure and maintainable while remaining simple. From here, you could explore features like rate limiting, monitoring, or hosting additional services on the same VPS.</p><p>The best part? You did it all yourself. Until next time!</p>]]></content:encoded></item><item><title><![CDATA[Simple Way to Deploy Your Services on a VPS Using Docker]]></title><description><![CDATA[By the end of this guide, you'll confidently run your applications on a VPS using Docker's power and simplicity, complete with a 'one-click' deployment script for instant updates.]]></description><link>https://www.kkyri.com/p/simple-way-to-deploy-your-services</link><guid isPermaLink="false">https://www.kkyri.com/p/simple-way-to-deploy-your-services</guid><dc:creator><![CDATA[Kyri]]></dc:creator><pubDate>Tue, 22 Oct 2024 08:42:41 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!660r!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3e46555-5e05-43b4-bfe4-f54ce525059a_5977x3985.heic" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!660r!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3e46555-5e05-43b4-bfe4-f54ce525059a_5977x3985.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!660r!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3e46555-5e05-43b4-bfe4-f54ce525059a_5977x3985.heic 424w, https://substackcdn.com/image/fetch/$s_!660r!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3e46555-5e05-43b4-bfe4-f54ce525059a_5977x3985.heic 848w, https://substackcdn.com/image/fetch/$s_!660r!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3e46555-5e05-43b4-bfe4-f54ce525059a_5977x3985.heic 1272w, https://substackcdn.com/image/fetch/$s_!660r!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3e46555-5e05-43b4-bfe4-f54ce525059a_5977x3985.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!660r!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3e46555-5e05-43b4-bfe4-f54ce525059a_5977x3985.heic" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c3e46555-5e05-43b4-bfe4-f54ce525059a_5977x3985.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2095943,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!660r!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3e46555-5e05-43b4-bfe4-f54ce525059a_5977x3985.heic 424w, https://substackcdn.com/image/fetch/$s_!660r!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3e46555-5e05-43b4-bfe4-f54ce525059a_5977x3985.heic 848w, https://substackcdn.com/image/fetch/$s_!660r!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3e46555-5e05-43b4-bfe4-f54ce525059a_5977x3985.heic 1272w, https://substackcdn.com/image/fetch/$s_!660r!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3e46555-5e05-43b4-bfe4-f54ce525059a_5977x3985.heic 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Containers: your new best friend</figcaption></figure></div><h1>Introduction</h1><p>You've got your <a href="https://www.kkyri.com/p/how-to-secure-your-new-vps-a-step-by-step-guide">VPS up and running</a>, but now comes the real challenge: deploying your web app. With the amount of options out there, it's easy to get lost in the possibilities.</p><p>This guide will walk you through a battle-tested method using Docker. We'll cover the essentials:</p><ol><li><p>Docker and container basics</p></li><li><p>Building your own Docker images</p></li><li><p>Deploying containers on your VPS</p></li></ol><p>I've designed this tutorial to be hands-on, so feel free to follow along. Whether you're a seasoned developer or just getting started, I think you&#8217;ll find value in adopting this deployment strategy. Let's dive in.</p><h1>Docker: Swiss Army Knife of self-hosting</h1><h2>What is Docker?</h2><p>Docker is a containerization platform that facilitates application deployment. It packages an application with its code, dependencies, and settings into a container, ensuring consistent environments across various platforms - from your local machine to your VPS.</p><h2>Why Docker?</h2><p>As an indie hacker, you might think you need a complex stack of deployment tools, but Docker alone can take you surprisingly far. This battle-tested containerization platform offers everything most solo developers need:</p><ol><li><p><strong>Dead-simple deployment</strong>: One command to package and run your entire app</p></li><li><p><strong>Zero cost</strong>: Completely free and open-source</p></li><li><p><strong>Self-healing services</strong>: Automatic restarts when things crash</p></li><li><p><strong>Built-in logging</strong>: All your logs in one place, no extra tools needed</p></li><li><p><strong>Performance insights</strong>: Monitor memory and CPU usage out of the box</p></li><li><p><strong>Resource control</strong>: Set limits to keep your services from misbehaving</p></li></ol><p>Before reaching for Coolify or other &#8220;all-in-one&#8221; tools, remember that Docker by itself can handle most deployment scenarios you'll encounter as a solo developer. Keep it simple, ship faster.</p><h2>Setting Up Docker</h2><p>You'll need Docker on both your local machine and VPS. Let's get started.</p><p>Installation on your local machine:</p><ol><li><p>Visit the <a href="https://docs.docker.com/engine/install/">official Docker website</a></p></li><li><p>Follow the installation guide for your OS</p></li><li><p>Verify the installation:</p></li></ol><pre><code>docker --version</code></pre><p>Installation on your VPS (Ubuntu):</p><ol><li><p>Follow <a href="https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04">Digital Ocean's guide</a> (steps 1 &amp; 2)</p></li><li><p>Verify the installation:</p></li></ol><pre><code>docker run hello-world</code></pre><p>You should see a "Hello from Docker!" message, confirming a successful setup.</p><blockquote><p>If your VPS is not running Ubuntu, check out the <a href="https://docs.docker.com/engine/install/">official installation guide</a> for your operating system.</p></blockquote><h1>Building your Docker image</h1><h2>From code to image</h2><p>Remember the <code>hello-world</code> container you ran earlier? That was a running instance of a Docker image. An image is the blueprint in Docker, containing your application code and everything it needs to run.</p><p>Let's create an image for a web app. We'll use a simple Go web server, but the same principles apply to any language. The beauty of Docker is that once you have an image, the deployment process is identical regardless of the underlying programming language used. </p><blockquote><p>Feel free to follow along with Go - just make sure you have it <a href="https://go.dev/doc/install">installed first</a>. Alternatively, use a language of your choice. If you choose to use a different language, some steps during image creation might differ slightly.</p></blockquote><p>To setup your service, start by creating a directory <strong>on your local machine</strong>:</p><pre><code>mkdir app
cd app</code></pre><p>Initialize a Go module:</p><pre><code>go mod init app</code></pre><p>Create a <code>main.go</code> file with a basic HTTP server:</p><pre><code>package main

import (
    "fmt"
    "net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, World!")
}

func main() {
    http.HandleFunc("/", helloHandler)
    fmt.Println("Server is listening on :8080...")
    http.ListenAndServe(":8080", nil)
}</code></pre><p>Test your server:</p><pre><code>go run main.go</code></pre><p>You should see "Server is listening on :8080...". Once confirmed, kill the server. Now we're ready to create an image out of this.</p><h2>Creating an image</h2><p>There are two primary methods to create a Docker image: manually defining a Dockerfile or using a higher-level tool like nixpacks. Let's explore both options.</p><h3>Option 1: The Dockerfile approach</h3><p>This method is more hands-on and language-specific, but it often results in faster builds and smaller image sizes.</p><p>On your local machine, create a Dockerfile in your app directory:</p><pre><code>FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod ./
RUN go mod download
COPY *.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -o app

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/app ./
EXPOSE 8080
CMD ["./app"]</code></pre><p>This Dockerfile uses a multi-stage build to create a compact image. It exposes port 8080, matching our web server's configuration.</p><p>Now that you have a Dockerfile, build the image:</p><pre><code>docker build --platform linux/amd64 -t app:latest .</code></pre><p>We specify the <code>linux/amd64</code> platform to ensure compatibility with our VPS.</p><p>Verify the image:</p><pre><code>docker images</code></pre><p>You should see output similar to:</p><pre><code>REPOSITORY  TAG              IMAGE ID       CREATED         SIZE
app         latest           3875e8cecccf   5 seconds ago   8.2MB</code></pre><p>(Optional) Test the image:</p><pre><code>docker run -it app</code></pre><p>You should see the "Server is listening..." output. Note that this may not work on your local machine if your architecture differs from <code>linux/amd64</code>.</p><p>If you're interested in an alternative approach, continue to the next section about nixpacks. Otherwise, feel free to skip ahead to learn how to run this on your VPS.</p><h3>Option 2: Simplifying with nixpacks</h3><p>If you&#8217;re looking for a more streamlined approach, nixpacks offers an abstraction layer that eliminates the need for a Dockerfile, simplifying the image-building process.</p><p>Install nixpacks (for macOS users):</p><pre><code>brew install nixpacks</code></pre><blockquote><p>For other operating systems, consult the <a href="https://nixpacks.com/docs/install">official nixpacks website</a>.</p></blockquote><p>Build your image:</p><pre><code>CGO_ENABLED=0 nixpacks build . --platform linux/amd64 --name app</code></pre><blockquote><p><strong>Note:</strong> the <code>CGO_ENABLED=0</code> is only required if you&#8217;re building a Go app.</p></blockquote><p>Verify the image:</p><pre><code><code>docker images</code></code></pre><p>You should see output similar to:</p><pre><code>REPOSITORY  TAG              IMAGE ID       CREATED         SIZE
app         latest           7ca012c83587   9 seconds ago   33.7MB</code></pre><blockquote><p><strong>Comparing approaches</strong></p><p>If you've followed both options, you'll notice the nixpacks image size is significantly larger (about 300% in this case). This illustrates the trade-off: nixpacks offers simplicity and language agnosticism, while a custom Dockerfile allows for optimization at the cost of complexity.</p></blockquote><p>(Optional) Test the image:</p><pre><code><code>docker run -it app</code></code></pre><p>You should see the "Server is listening..." output. Note that this may not work if your local machine architecture differs from <code>linux/amd64</code>.</p><h1>Deployment: bring it all together</h1><h2>Docker Compose: your orchestration friend</h2><p>Docker Compose extends Docker's functionality by managing single and multi-container applications. Using a single YAML file, it lets you define, connect, and run multiple services in one go. It also handles the entire container lifecycle - from startup and shutdown to networking and volume management. Since it comes bundled with Docker, we'll use it to streamline our application setup and deployment.</p><p>On your VPS, create a <code>compose.yaml</code> file:</p><pre><code>services:
  app:
    image: app:latest
    container_name: app
    ports:
      - "8080:8080"
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256MB</code></pre><p>This configuration maps port 8080 from the host to the container and sets CPU and memory resource limits. While these latter ones are optional, they prevent a single container from hogging your entire VPS resources.</p><p>With this file, all we need is the Docker image present on our VPS.</p><h2>Transferring your image to the VPS</h2><p>To get your locally-built image onto your VPS, copy the image over using SSH. On your local machine:</p><pre><code>docker save app:latest | gzip | ssh user@12.34.56.78 docker load</code></pre><blockquote><p><strong>Note:</strong> This may take some time depending on your image size and network speed.</p></blockquote><p>Verify the transfer:</p><pre><code>ssh user@12.34.56.78
docker images</code></pre><p>You should see output similar to:</p><pre><code>REPOSITORY  TAG                 IMAGE ID       CREATED         SIZE
app         latest              5dd6c8c8a032   7 minutes ago   15.8MB</code></pre><p>Now that all the pieces are in place, let's get your application up and running.</p><h2>Launching your Dockerized app</h2><p>On your VPS, navigate to the directory containing your <code>compose.yaml</code> file and run:</p><pre><code>docker compose up -d</code></pre><blockquote><p>The <code>-d</code> flag runs it in detached mode, allowing you to continue using the terminal.</p></blockquote><p>Verify the running container:</p><pre><code>docker ps</code></pre><p>You should see output similar to:</p><pre><code>CONTAINER ID   IMAGE       COMMAND   PORTS                   NAMES
17bec024dcce   app:latest  "./app"   0.0.0.0:8080-&gt;8080/tcp  app</code></pre><p>Monitor your app by viewing logs:</p><pre><code>docker logs app</code></pre><p>Or by checking its resource usage:</p><pre><code>docker stats app</code></pre><p>Test the service is running:</p><pre><code>curl localhost:8080</code></pre><p>Expected output:</p><pre><code>Hello, World!</code></pre><h2>Streamlining deployments</h2><p>To simplify future updates, create a deployment script <code>deploy.sh</code> on your local machine:</p><pre><code>#!/bin/bash
docker build --platform linux/amd64 -t app:latest .
docker save app:latest | gzip | ssh user@12.34.56.78 docker load
ssh user@12.34.56.78 docker compose up -d app</code></pre><blockquote><p><strong>Note:</strong> You can substitute the Docker build command with nixpacks if preferred.</p></blockquote><p>Make the script executable:</p><pre><code>chmod +x deploy.sh</code></pre><p>Whenever you&#8217;re ready to deploy, run the script:</p><pre><code>deploy.sh</code></pre><p>This script encapsulates the entire deployment process - building, transferring, and launching your updated application.</p><p>Congratulations! You've successfully set up a streamlined Docker deployment pipeline for your VPS. With this foundation, you can easily manage and update your application as it evolves.</p><blockquote><p>An alternative way to manage deployments is through <a href="https://docs.docker.com/engine/manage-resources/contexts/">Docker contexts</a>, which leverages the platform's layered architecture and results in smaller file transfers over the network. I'll explore this approach in depth in a future article.</p></blockquote><h2>The issue with ufw</h2><p>If you've set up your VPS security following <a href="https://www.kkyri.com/p/how-to-secure-your-new-vps-a-step-by-step-guide">my previous article</a>, you're probably using ufw (Uncomplicated Firewall). <strong>Here's a crucial security note</strong>: Docker bypasses ufw rules by default. This means ports you thought were protected might actually be exposed to the internet - for example, port 8080 could be accessible even if ufw blocks it. </p><p>But don't worry - there's a <a href="https://github.com/chaifeng/ufw-docker">straightforward fix</a> for this behaviour. Edit the ufw rules file:</p><pre><code>sudo vim /etc/ufw/after.rules</code></pre><p>Add the following rules at the end of the file:</p><pre><code># BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:ufw-docker-logging-deny - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j ufw-user-forward

-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16

-A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN

-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 172.16.0.0/12

-A DOCKER-USER -j RETURN

-A ufw-docker-logging-deny -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW DOCKER BLOCK] "
-A ufw-docker-logging-deny -j DROP

COMMIT
# END UFW AND DOCKER</code></pre><p>Save and close the file. Reload ufw:</p><pre><code>sudo ufw reload</code></pre><p>With this fix in place, your VPS should no longer be exposing port 8080. This means that your web server is no longer accessible. </p><p>To make it accessible, let's expose it on the standard HTTP port. Update your <code>compose.yaml</code> file to map the host port to <code>80</code>:</p><pre><code>services:
  app:
    image: app:latest
    container_name: app
    ports:
      - "80:8080" &lt;-- this line changed
    ... (resource config omitted)</code></pre><p>Restart your app:</p><pre><code>docker compose up -d</code></pre><p>Your app should now be accessible on port 80 using the VPS public ip address, assuming your firewall allows it.</p><blockquote><p>Check your firewall's allowed ports with <code>sudo ufw status</code>. If port 80 isn't listed, enable it by running <code>sudo ufw allow 'Nginx Full'</code>.</p></blockquote><h2>Future improvements</h2><p>While our web server is functional, it's exposed directly to the internet - not the best practice for production deployments. A better approach is placing it behind a battle-tested reverse proxy, which offers three major benefits:</p><ol><li><p><strong>Better security</strong>: Acts as a shield between your application and the internet</p></li><li><p><strong>Simple HTTPS setup</strong>: Makes SSL/TLS implementation straightforward</p></li><li><p><strong>Flexible routing</strong>: Route different paths to different web servers (like <code>api.yourdomain.com</code> to your API server and <code>app.yourdomain.com</code> to your frontend)</p></li></ol><p>Next week, I'll guide you through setting this up with <a href="https://nginx.org/en/">nginx</a>. You'll learn how to configure it with your Docker containers and secure everything using HTTPS with LetsEncrypt certificates - all explained step by step.</p><h1>Congratulations</h1><p>Pat yourself on the back for reaching this far! You've hit some major milestones:</p><ul><li><p>Set up Docker from scratch</p></li><li><p>Containerized your application</p></li><li><p>Learned Docker Compose</p></li><li><p>Deployed to your VPS</p></li></ul><p>You've built a solid foundation for production deployments. See you in the next guide, where we'll take it even further!</p>]]></content:encoded></item><item><title><![CDATA[How to Secure Your New VPS: A Step-by-Step Guide]]></title><description><![CDATA[From sensible user management to automated security patches, by the end of this guide you'll sleep confidently with a VPS you know is secure.]]></description><link>https://www.kkyri.com/p/how-to-secure-your-new-vps-a-step-by-step-guide</link><guid isPermaLink="false">https://www.kkyri.com/p/how-to-secure-your-new-vps-a-step-by-step-guide</guid><dc:creator><![CDATA[Kyri]]></dc:creator><pubDate>Tue, 15 Oct 2024 08:42:41 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!4Aji!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed2055c5-2a8c-485e-af0c-cafaec780dd7_870x580.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4Aji!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed2055c5-2a8c-485e-af0c-cafaec780dd7_870x580.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4Aji!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed2055c5-2a8c-485e-af0c-cafaec780dd7_870x580.jpeg 424w, https://substackcdn.com/image/fetch/$s_!4Aji!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed2055c5-2a8c-485e-af0c-cafaec780dd7_870x580.jpeg 848w, https://substackcdn.com/image/fetch/$s_!4Aji!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed2055c5-2a8c-485e-af0c-cafaec780dd7_870x580.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!4Aji!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed2055c5-2a8c-485e-af0c-cafaec780dd7_870x580.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4Aji!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed2055c5-2a8c-485e-af0c-cafaec780dd7_870x580.jpeg" width="870" height="580" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ed2055c5-2a8c-485e-af0c-cafaec780dd7_870x580.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:580,&quot;width&quot;:870,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:64573,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4Aji!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed2055c5-2a8c-485e-af0c-cafaec780dd7_870x580.jpeg 424w, https://substackcdn.com/image/fetch/$s_!4Aji!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed2055c5-2a8c-485e-af0c-cafaec780dd7_870x580.jpeg 848w, https://substackcdn.com/image/fetch/$s_!4Aji!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed2055c5-2a8c-485e-af0c-cafaec780dd7_870x580.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!4Aji!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed2055c5-2a8c-485e-af0c-cafaec780dd7_870x580.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">No trespassing allowed</figcaption></figure></div><h1>Introduction</h1><p>So, your $5 VPS just arrived. All you've got is an IP address, a username, and a password. Now what?</p><p>In this guide, I'll walk you through fortifying your VPS from zero to hero&#8212;covering essential security measures and then going beyond the basics to create a secure environment. We'll tackle everything from user management and SSH configuration to setting up a firewall and implementing a couple handy automations to keep your server in top shape.</p><p><strong>Note</strong>: This guide is tailored for Ubuntu servers and assumes you've gone with a budget VPS provider&#8212;the kind that typically leaves most of the setup to you. If you're running on a big-name cloud platform like AWS or Azure, you might find some parts of this guide redundant, as they often come with pre-configured settings.</p><h1>Securing access</h1><h2>First things first: updating your server</h2><p>Start by connecting to your VPS using the IP address and credentials from your hosting provider: </p><pre><code>$ ssh root@12.34.56.78</code></pre><blockquote><p><em>Replace the user and IP address with the one from your provider.</em></p></blockquote><p>Enter the password when prompted.</p><p>Once you're in, update your server to the latest software versions:</p><pre><code>$ sudo apt update 
$ sudo apt upgrade </code></pre><p>This ensures you're working with the most recent, secure versions of all installed packages.</p><h2>Setting up a non-root user</h2><p>If your hosting service provided you with a <em>root</em> user, it's crucial to create a separate user for connecting and running your services: </p><pre><code>$ sudo adduser username</code></pre><blockquote><p><em>Replace 'username' with your preferred username</em></p></blockquote><p>You'll be prompted to set and confirm a password for the new user. You'll also be asked for additional information - all of this is optional, so feel free to skip by pressing Enter.</p><p>Next, grant the new user sudo privileges:</p><pre><code><code>$ sudo usermod -aG sudo username</code></code></pre><p>Now, switch to the new user and verify sudo access:</p><pre><code>$ su - username
$ sudo whoami</code></pre><p>If the last command returns <em>root</em>, you've successfully set up sudo access for your new user.</p><h2>Setting up SSH authentication</h2><p>You initially connected to the server using password authentication. Now, we'll switch to key-based authentication for improved security. </p><p><strong>On your local machine</strong>, generate an SSH key pair:</p><pre><code>$ ssh-keygen -t ed25519 -C "email@example.com"</code></pre><blockquote><p><em>Replace &#8216;email@example.com&#8217; with your own address</em></p></blockquote><p>Press Enter to save the key in the default location. You can optionally enter a passphrase - if you do, you'll need to type it each time you SSH to the VPS. I personally leave it empty for convenience. </p><p>Next, from your local machine, copy your public key to your VPS: </p><pre><code>$ ssh-copy-id -i ~/.ssh/ed25519.pub username@12.34.56.78</code></pre><blockquote><p><em>Replace 'username' with the user you created earlier.</em></p></blockquote><p>Enter the user's password when prompted.</p><p>Verify that you can connect using the new key:</p><pre><code>$ ssh -i ~/.ssh/ed25519 -o PasswordAuthentication=no username@12.34.56.78</code></pre><p>To avoid specifying the path of the key each time, add it to your local SSH agent:</p><pre><code>$ eval "$(ssh-agent -s)"
$ ssh-add ~/.ssh/ed25519</code></pre><p>Now you can SSH to your VPS simply with:</p><pre><code>$ ssh username@12.34.56.78</code></pre><p>(Optional) For even more convenience, add an alias to your ~/.bashrc or ~/.zshrc: </p><pre><code>alias ssh-vps='ssh username@12.34.56.78'</code></pre><p>Save and close the file. Then, source the configuration into your current session:</p><pre><code>$ source ~/.zhsrc</code></pre><p>Now you can connect to your VPS by simply typing <code>ssh-vps</code> in your terminal.</p><h2>Disabling password authentication</h2><p>Now that you've enabled SSH key access, it's time to disable password authentication and root login. First, connect to your VPS using the new user: </p><pre><code>$ ssh username@12.34.56.78</code></pre><p>Open the SSH daemon configuration file:</p><pre><code>$ sudo vim /etc/ssh/sshd_config</code></pre><p>Make the following changes: </p><ol><li><p>Find <code>PermitRootLogin yes</code> and change it to <code>PermitRootLogin no</code>. </p></li><li><p>Find <code>ChallengeResponseAuthentication</code> and set it to <code>no</code>.</p></li><li><p>Locate <code>PasswordAuthentication</code> and set it to <code>no</code>. </p></li><li><p>Find <code>UsePAM</code> and set it to <code>no</code>. </p></li></ol><p>Your <code>sshd_config</code> file should now include these lines:</p><pre><code>PermitRootLogin no
ChallengeResponseAuthentication no
PasswordAuthentication no
UsePAM no</code></pre><p>Save and close the file.</p><p>&#9888;&#65039; <strong>Caution:</strong> Before proceeding, ensure you've created a non-root user and set up SSH key authentication as detailed in the previous section, or you risk permanently locking yourself out of your VPS.</p><p>Apply the new configuration by restarting the SSH daemon:</p><pre><code>$ sudo service sshd reload</code></pre><p>Verify that root login is now disabled. <strong>From your local machine</strong>, attempt to log in as root:</p><pre><code>$ ssh root@12.34.56.78</code></pre><p>This should fail with a <em>Permission denied</em> error message.</p><h1>Fortifying your VPS</h1><h2>Configuring a firewall</h2><p>A firewall is crucial for preventing unauthorized access to your VPS ports. We'll follow the principle of least privilege, opening only the ports we need. </p><p>Install UFW (Uncomplicated Firewall):</p><pre><code>$ sudo apt install ufw</code></pre><p>Check the firewall status:</p><pre><code>$ sudo ufw status verbose</code></pre><p><strong>It should be inactive. If not, disable it temporarily:</strong></p><pre><code>$ sudo ufw disable</code></pre><p>List known UFW app policies:</p><pre><code>$ sudo ufw app list</code></pre><p><strong>Allow OpenSSH (this is critical to maintain access)</strong>:</p><pre><code>$ sudo ufw allow 'OpenSSH'</code></pre><p><strong>&#9888;&#65039; Double-check that OpenSSH is allowed</strong> (this prevents lockouts):</p><pre><code>$ sudo ufw show added</code></pre><p>You should see the following:</p><pre><code>ufw allow OpenSSH</code></pre><p><strong>Do not proceed unless you see this.</strong></p><p>Set default rules:</p><pre><code>$ sudo ufw default deny incoming&nbsp;
$ sudo ufw default allow outgoing</code></pre><p>This allows all outgoing traffic but denies all incoming traffic (except SSH).</p><p>(Optional) If you&#8217;re planning to run a web server, allow ports 80 and 443:</p><pre><code>$ sudo ufw allow 'Nginx Full'</code></pre><p>Finally, enable the firewall:</p><pre><code>$ sudo ufw enable</code></pre><p>&#9888;&#65039; <strong>Caution:</strong> The system may warn about disrupting SSH connections. If you've confirmed OpenSSH is allowed (as we did earlier), it's safe to proceed&#8212;enter 'y' when prompted.</p><p>Verify the firewall is running:</p><pre><code>$ sudo ufw status verbose</code></pre><pre><code>Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp (OpenSSH)           ALLOW IN    Anywhere
22/tcp (OpenSSH (v6))      ALLOW IN    Anywhere (v6)</code></pre><p>Your VPS is now protected by a firewall.</p><h2>Setting up Fail2Ban</h2><p>Fail2Ban monitors your authentication logs and bans IP addresses that make too many failed login attempts. We'll configure it to monitor SSH connection attempts.</p><p>Install Fail2ban:</p><pre><code>$ sudo apt install fail2ban</code></pre><p>Check its status (it should be disabled by default):</p><pre><code>$ sudo systemctl status fail2ban.service</code></pre><pre><code>&#9675; fail2ban.service - Fail2Ban Service
     Loaded: loaded (/lib/systemd/system/fail2ban.service; disabled; vendor preset: enabled)
     Active: inactive (dead)
       Docs: man:fail2ban(1)</code></pre><p>Before enabling it, you&#8217;ll configure Fail2Ban to monitor SSH access logs.</p><p>Navigate to the Fail2Ban config directory:</p><pre><code>$ cd /etc/fail2ban</code></pre><p>Create a local configuration file:</p><pre><code>$ sudo cp jail.conf jail.local</code></pre><p>Edit the local configuration:</p><pre><code>$ sudo vim jail.local</code></pre><p>In the <code>[sshd]</code> section, add or modify these lines:</p><pre><code>[sshd]
enabled = true
mode = aggressive
...</code></pre><p>This enables SSH monitoring and sets the mode to aggressive, which applies stricter rules to cover a broader range of potential threats.</p><p>Feel free to explore and adjust other settings as needed. The defaults are generally suitable for most cases. When you&#8217;re done, save and close the file.</p><p>Enable Fail2Ban to run on system startup:</p><pre><code>$ sudo systemctl enable fail2ban</code></pre><p>Start Fail2Ban manually:</p><pre><code>$ sudo systemctl start fail2ban</code></pre><p>Verify it&#8217;s running:</p><pre><code>$ sudo systemctl status fail2ban</code></pre><p>(Optional) To verify Fail2Ban is working, you can attempt to connect repeatedly with an invalid SSH key. The error should change from <em>Permission denied</em> to <em>Connection refused</em> when banned. <strong>Warning:</strong> Perform this test from a different machine than your local one to avoid locking yourself out. If you do get locked out, the default ban time is 10 minutes. </p><p>Your VPS now has an additional layer of protection against brute-force attacks.</p><h1>Automating the boring stuff</h1><h2>Staying secure: automated security updates</h2><p>Ubuntu provides <code>unattended-upgrades</code>, a tool that automatically retrieves and installs security patches and essential upgrades for your server. </p><p>Install it (if not pre-installed):</p><pre><code>$ sudo apt install unattended-upgrades</code></pre><p>Verify it&#8217;s running:</p><pre><code>$ sudo systemctl status unattended-upgrades.service</code></pre><p>(Optional) Configure automatic reboots. Some upgrades require a reboot to take effect. By default, automatic reboots are disabled. To change this, open its configuration file:</p><pre><code>$ sudo vim /etc/apt/apt.conf.d/50unattended-upgrades</code></pre><p>Find the line <code>Unattended-Upgrade::Automatic-Reboot</code> and set it to <code>true</code>. </p><p>&#9888;&#65039; <strong>Caution:</strong> Enabling automatic reboots will make your VPS and its services temporarily unavailable during reboots. Personally, I keep this disabled and reboot manually when needed. When you SSH into the VPS, you'll see a message if a reboot is required for updates. </p><p>If you made changes, reload the service:</p><pre><code>$ sudo systemctl reload unattended-upgrades.service</code></pre><p>Your VPS will now automatically stay up-to-date with the latest security patches and essential upgrades.</p><h1>Conclusion: a bulletproof VPS</h1><p>Congratulations on making it this far! Your VPS is now in significantly better shape than when you started. Let's recap what you've accomplished:</p><ol><li><p>Updated your server's software to the latest version</p></li><li><p>Disabled password authentication and set up a more secure key-based authentication mechanism</p></li><li><p>Added a firewall to control access to your server's ports</p></li><li><p>Installed Fail2Ban to automatically block IP addresses making unauthorised connection attempts</p></li><li><p>Configured automatic security upgrades and patches</p></li></ol><p>You've transformed your bare-bones VPS into a robust, secure environment. Now you're ready to start deploying your SaaS with confidence.</p><p>Until next time, happy and secure hosting!</p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://www.kkyri.com/p/how-to-secure-your-new-vps-a-step-by-step-guide?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Thank you for reading Bootstrap &amp; Build! This post is public so feel free to share it.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.kkyri.com/p/how-to-secure-your-new-vps-a-step-by-step-guide?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.kkyri.com/p/how-to-secure-your-new-vps-a-step-by-step-guide?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div>]]></content:encoded></item></channel></rss>