Lessons from six months of self-hosting

It has been about six months since I started running services on my VPS. Time for an honest look at what worked, what did not, and what I would do differently. What works well Static sites are effortless. Hugo plus Nginx has been zero-maintenance. I deploy with a script, and it just works. No crashes, no updates to worry about, no dependencies to manage. This has been the most clearly worthwhile thing I self-host. ...

March 21, 2026 · 3 min · Martin Lindqvist

Quick note on HTTP security headers

I recently ran my site through securityheaders.com and realized I was missing a few useful headers. Here is the set I ended up adding to my Nginx config. The headers add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self';" always; What each one does: X-Content-Type-Options prevents browsers from guessing the MIME type of a response. Without this, a browser might interpret a text file as HTML and execute scripts in it. X-Frame-Options prevents your site from being embedded in an iframe on another domain. This mitigates clickjacking attacks. Referrer-Policy controls how much referrer information is sent when navigating away from your site. strict-origin-when-cross-origin is a sensible default. Permissions-Policy explicitly disables browser features your site does not use. A static blog has no need for camera or microphone access. Content-Security-Policy restricts where resources can be loaded from. The policy above only allows resources from the same origin, which is exactly right for a self-contained static site with no external dependencies. The always keyword Note the always parameter on each header. Without it, Nginx only adds headers to successful responses (2xx and 3xx). With always, headers are included on error pages too. This matters because error pages are also potential attack vectors. ...

March 8, 2026 · 2 min · Martin Lindqvist

Optimizing images for the web with WebP

I recently converted all images on this site from PNG and JPEG to WebP. The results were better than I expected, so here are some quick notes. Why WebP WebP produces smaller files than PNG and JPEG at comparable visual quality. Browser support is effectively universal now — every modern browser has supported it for years. There is no reason to keep serving older formats for new content. Converting with cwebp The cwebp command-line tool (part of the libwebp package) handles conversion: ...

February 27, 2026 · 2 min · Martin Lindqvist

Monitoring server uptime with free tools

Knowing when your server is down before your users do is the bare minimum of responsible self-hosting. I have tried several monitoring approaches and settled on a setup that is simple, free, and has been reliable for months. Uptime Kuma Uptime Kuma is the core of my monitoring stack. It is a self-hosted monitoring tool that checks HTTP endpoints, TCP ports, DNS records, and ping targets at configurable intervals. Installation is one Docker command: ...

February 15, 2026 · 4 min · Martin Lindqvist

WireGuard vs OpenVPN: my experience

I use a VPN tunnel to administer my VPS and to connect back to my home network when I am away. Over the past year I have used both OpenVPN and WireGuard for this purpose. Here is how they compare in practice. The setup My use case is straightforward: I need a secure tunnel from my laptop to my VPS for SSH, web dashboards, and occasionally reaching services on my home LAN. Nothing exotic — just point-to-point connectivity between machines I control. ...

February 2, 2026 · 4 min · Martin Lindqvist

Speed testing your server from multiple locations

Running a speed test from your own machine tells you about one path. To get a broader picture of your server’s connectivity, you need to test from multiple locations. Here are the approaches I use. ping.pe ping.pe is one of the simplest tools for this. Enter your server’s IP or hostname and it runs ping, MTR, and port checks from dozens of locations simultaneously. The results show up in a live grid. ...

January 26, 2026 · 4 min · Martin Lindqvist

Setting up a self-hosted speed test with LibreSpeed

I wanted an independent way to test the connection speed to my server without relying on third-party services. LibreSpeed is an open-source speed test that you can host yourself. There is also a Go backend called speedtest-go that is self-contained and easy to deploy. This post covers setting up speedtest-go with a systemd service and an Nginx reverse proxy. Why self-host a speed test Public speed test services measure the connection between you and their servers, which is useful but does not tell you much about the connection to your specific server. A self-hosted instance measures exactly what you care about: the bandwidth and latency between a client and your VPS. ...

January 11, 2026 · 5 min · Martin Lindqvist

Self-hosting in 2025: what's worth it

I have been running various services on my VPS for a while now. Some have been worth the effort, others were not. Here is where I have landed. Worth self-hosting Static sites and personal projects. This is the easiest category. A static site generator plus Nginx is trivial to maintain. No database, no runtime dependencies, minimal attack surface. Updates are a git pull and a rebuild. I cannot think of a reason to use a managed service for this. ...

January 5, 2026 · 3 min · Martin Lindqvist

Caching strategies for static sites

Serving a static site is fast by default, but proper caching headers make a noticeable difference for repeat visitors. These are the headers I configure and why. Cache-Control This is the primary header that controls caching behavior. For static assets like CSS, JS, images, and fonts: location ~* \.(css|js|png|jpg|jpeg|webp|svg|woff|woff2|ico)$ { add_header Cache-Control "public, max-age=2592000, immutable"; } max-age=2592000 is 30 days. The immutable directive tells the browser not to revalidate the resource even on a normal reload. This works well when your build tool hashes filenames — if the content changes, the filename changes, so the old cached version is never requested again. ...

December 14, 2025 · 2 min · Martin Lindqvist

What I learned about BGP routing

I spent the last few weeks reading about BGP after running into a routing issue that I could not explain with traceroute alone. These are rough notes, not a tutorial. The basics BGP (Border Gateway Protocol) is how autonomous systems on the internet exchange routing information. An autonomous system (AS) is a network or group of networks operated by a single organization, identified by an AS number (ASN). For example, Cloudflare is AS13335, Hetzner is AS24940. ...

November 22, 2025 · 3 min · Martin Lindqvist