{"slug": "blog-ran-on-ubuntu-16-04-for-10-years-i-migrated-it-to-freebsd", "title": "Blog ran on Ubuntu 16.04 for 10 years. I migrated it to FreeBSD", "summary": "After running on an outdated Ubuntu 16.04 LTS Digital Ocean VPS for over ten years, the author migrated their blog to a cheaper, more powerful Hetzner virtual machine running FreeBSD. The old server had been unsupported for at least five years, posing security risks, while the new setup offers double the memory and CPU for less than €6 per month. The author chose FreeBSD to gain hands-on experience with its integrated design, security features, and Jails virtualization system.", "body_md": "This blog has been running on a Digital Ocean VPS for over ten years. A machine hosted in New York City, running **Ubuntu 16.04 LTS**. An LTS that hasn’t been in support for at least 5 years. It was about time to change it. After some considerations, I migrated to a Hetzner virtual machine that is way better than my old Ubuntu one, less than half the price of what I used to pay, and just across the country from me. Not only that, but I took the challenge to move my stack to **FreeBSD**. It’s a long text, but stay for a cool introduction of *FreeBSD Jails* with *Bastille* and some interesting site load benchmarks.\n\n# Motivation\n\nIf you know how releases on Ubuntu work (I’m not very familiar myself), once the release is out of support, the apt package repository is out, so you can’t get any updates from it anymore. There are several implications of running such an outdated system, and the most obvious is that your server is just not as secure anymore. There might be several bots out there just trying to find nodes with vulnerabilities to introduce malicious stuff onto them. Luckily (I think), nothing ever happened. Not that there was anything important to be stolen in there either way. But I remember a long time ago, one WordPress blog that I had, which was also running on an old VPS, randomly got a lot of very suspicious links to casino and gambling spread across the text in the posts.\n\nI was already using a Hetzner VPS as a remote development machine, where I SSH into from anywhere, and it’s been a reliable good VPS for the price. So I decided to start by comparing the specs. This was the droplet running my blog and my other websites:\n\nMy old Digital Ocean server\n\nThat has 2GB of RAM, one vCPU, 50GB disk, 2TB of monthly traffic and runs Ubuntu 16.04 x64. It’s located in their datacenter in New York City. That’s probably why it’s so expensive, I was paying $13 monthly for that.\n\nHetzner is an established European IT company and has big data-centers in Germany, where I live. The cheapest Hetzner server I could get, at only **3.56** euros is already way better than my old one:\n\nA cup of coffee per month\n\nDouble the memory and CPU, slightly less storage space, but ten times more traffic. But I decided to go with a beefier setup, for less than €6 a month:\n\nA *fancier* cup of coffee per month\n\nIt might even be a bit overkill for my sites, but why not?\n\n## The old setup\n\nMy old setup was serving a few more sites than just this blog. Nothing too popular; this blog, the most popular of all sites in there, wouldn’t get more than a couple thousand page views a month. Except when a couple of posts went viral on Hacker News, there wasn’t a lot of traffic. In the end, the machine was basically serving static sites, no fancy CGI or custom code running. The stack was simple. Everything was served with **nginx/1.10.3**, statically. So I’d just basically have several config files in `/etc/nginx/sites-available`\n\nfor each one of the sites. Extra necessary programs like static site generators and a LaTeX suite (e.g.: this blog is generated by **Hugo**) were installed either via `apt`\n\nor `snap`\n\n. In fact, my process to update my blog was:\n\n- write the text locally\n- commit and push to the repository\n- ssh into the server\n- pull the repo updates\n- run\n`hugo`\n\nDuring the first years of this VPS, I also used it to run some tests and do some programming. So it was bloated with a lot of outdated software that I didn’t use anymore. But it worked. And worked quite well.\n\nYep, it was running Linux 4.4!\n\nSo well, in fact, that its uptime was 1491 days when I shut it off! That’s roughly 4 years without interruption!\n\n## Why FreeBSD\n\nNot gonna lie, one of my main motivations was to get my hands on something different. I’ve been reading and watching a lot of stuff about BSDs in general, and I had a short previous experience with FreeBSD, so I thought it would be a good way to put it to a real-world test. FreeBSD is usually praised for its stability, due to its integrated design, security, and **Jails**. We’ll get to that in a bit.\n\nI don’t wanna sound like I knew exactly what I was doing, but when I read about **Jails**, I knew exactly what I wanted to do. **Jails** is a form of virtualization/containerization that’s been part of FreeBSD for over 25 years, way before Docker was even a thing. At first glance, it does exactly what you’d expect from a Docker container: it sandboxes a “minisystem” within it so you can run stuff that doesn’t have access to your host system. The main difference, I think, is that Docker and other container solutions are more suited for “packaging programs”. It’s ephemeral and immutable. It will operate on data that comes from outside the container, but within it, everything will always be the same. Whereas Jails are really subsystems, almost like mini-VMs, but that are in reality sharing the same kernel.\n\nOn top of that, its filesystem, **ZFS**, is actually really good and useful for servers. If you come from a Linux world, you probably have heard about **Btrfs**, which is the newer filesystem that’s been adopted by more and more distros lately. It has some similarities to ZFS: data integrity and snapshots. Except **ZFS** is probably way more mature than Btrfs. If I take frequent snapshots of my system, I don’t need to rely on the VPS provider snapshot or backup system, which I have to pay extra.\n\nMy idea was to have one Jail for each one of my sites with whatever tools they needed to build (like **Hugo**, for the blog) and an instance of nginx to serve it. And one Jail for the main web server that connected all of them with the world via reverse proxy. That way, if one of the jails get compromised, I can just destroy it and create a new one. I’ll get more in detail how to set them up.\n\n## Hetzner VPS\n\nHere are more details about it, for all you *fetchists*:\n\nI don’t know why fastfetch always report more memory being used than the actual values. I’ve never seen more than 3GiB used in btop for this server\n\n# Setup\n\nI’ll go through quickly how I set this server up and what the most important points are. I usually keep a dev journal, so I was documenting as I was doing. However, I wouldn’t take this as a guide because I might have had missed steps. This is more of a taste of what it was to set up a web server with FreeBSD on Hetzner.\n\n## Installing FreeBSD\n\nHetzner offers some images upon the creation of the VM, but they’re fairly limited:\n\nNo BSD\n\nBut I saw a guide explaining how to install it on Hetzner from the [official FreeBSD YouTube channel](https://www.youtube.com/watch?v=8RGbstrTWUo). Totally recommend the channel, by the way.\n\nThe thing is that Hetzner actually provide the FreeBSD image, but it’s just hidden, a couple of steps away. It offers as an ISO image. So, when prompted to select an OS image in the creation process, you just need to pick any, then you’ll eventually wipe it all out. After creating the VM, all you need is to go to the **ISO Images** tab in the Console and mount the image you want:\n\nFreeBSD ISO Images available\n\nI picked **14.3**. And rebooted. Then I basically followed the installer. Don’t remember if I changed anything from the standard options, but I was following the install video from the official FreeBSD channel. Boom, in no time, I had the system installed and running.\n\n### Bastille\n\nI mentioned Jails a few times already. But I’m not only using Jails, but also ** Bastille**, a system that helps managing Jails. The thing is that creating Jails by hand is a bit more complicated than I would like. There are several different steps that need to be taken care of individually, and for every new Jail created. Bastille simplifies that and offers everything you need one command away from\n\n`bastille`\n\n. Such as `bastille list`\n\nto show all the jails in the system, `bastille create`\n\nto create a new one, `bastille console`\n\nto open a shell in a jail, etc.Installing and enabling it is (almost) [as easy as](https://bastillebsd.org/getting-started/):\n\n```\npkg install bastille\nsysrc bastille_enable=\"YES\"\n```\n\nThere are also other Jail managers, I just went with the one with the coolest name.\n\n## The Stack\n\nThe whole idea is having a Jail running **Caddy** to serve all the sites and deal with domains and their SSL certificates. Then each site will have its own jail with whatever is necessary for them to build and serve. The sever jail will reverse proxy all the traffic to the respective jails. So the first thing needed is to configure an internal virtual network adapter. We can think of this stack as something similar to a bunch of virtual machines in a network.\n\nTo set up a virtual network interface:\n\n```\nsudo sysrc cloned_interfaces+=\"lo1\"\nsudo sysrc ifconfig_lo1_name=\"bastille0\"\nsudo service netif cloneup\nsudo sysrc ifconfig_bastille0=\"inet 10.0.0.1 netmask 255.255.255.0\"\n```\n\nIt basically clones a loopback interface, names it “bastille0” and assigns network parameters to it. The jails will work on that network interface, only. The caddy jail will have to access to the outside world, since that’ll be the one listening to the requests. For that, we need to create some internet access rule using PF (Packet Filter), FreeBSD’s *firewall*.\n\nWe need to edit `/etc/pf.conf`\n\n:\n\n```\n# the main interface `vtnet0` and the virtual one we created `bastille0`\next_if = \"vtnet0\"\nint_if = \"bastille0\"\nvpn_if = \"tailscale1\"\n\n# skip internal traffic\nset skip on $int_if\nset skip on $vpn_if\n\n# allow outbound access to the internet from the jails\nnat on $ext_if from 10.0.0.0/24 to any -> ($ext_if)\n\n# redirect HTTP/HTTPS traffic from the web to the server jail (10.0.0.5)\nrdr pass on $ext_if proto tcp from any to any port {80, 443} -> 10.0.0.5\n\n# block everything else\nblock all\n\n# allows outbound traffic from host\npass out quick on $ext_if keep state\n```\n\nAs commented in the config, it’s basically setting all internal traffic on our `bastille0`\n\nand `tailscale1`\n\nnetworks; then setting up outbound traffic for our jails and host system; and then redirecting all the `80`\n\nor `443`\n\n(HTTP and HTTPS) traffic to the internal ip `10.0.0.5`\n\n, which will be our Caddy server.\n\nNow we need to enable PF:\n\n```\nsysrc pf_enable=\"YES\"\nservice pf start\nsysrc gateway_enable=\"YES\"\n```\n\nThat’s all for the networking stack. Let’s create the Caddy server jail.\n\n### Creating the first Jail: Caddy Server\n\nMy old server ran on good old `nginx`\n\n. Except for a bit of `Apache`\n\n, I ran nginx for most of my life. But there’s an annoying thing with nginx that `Caddy`\n\nsolves swiftly: SSL certificates. Back in my old server, I’d have to run `certbot`\n\nevery now and then to renew the domain SSL certificates, and I missed renewals more often than I’d like. Caddy deals with that automatically.\n\nTo create a new jail, first we need to bootstrap with the version of FreeBSD we’ll be using, in my case it’s `14.3-RELEASE`\n\n:\n\n```\nbastille bootstrap 14.3-RELEASE\n```\n\nThen we create the actual jail:\n\n```\nbastille create caddy 14.3-RELEASE 10.0.0.5 bastille0\nbastille start caddy\n```\n\nWe create a jail named `caddy`\n\n, using `14.3-RELEASE`\n\n, with ip `10.0.0.5`\n\non the `bastille0`\n\ninterface.\n\nTo check the state of the jail:\n\n```\n# bastille list\n JID  Name      Boot  Prio  State  Type   IP Address  Published Ports  Release       Tags\n 3    caddy     on    99    Up     thin   10.0.0.5    -                14.3-RELEASE  -\n```\n\nThere, it’s already running! Remember, jails are not supposed to be *ephemeral* like a Docker container, where you set up the whole config of the container and just start it and expect the machine to be exactly the same forever. We’re almost dealing with virtual machines here. We can access a shell at any moment with:\n\n```\n# bastille console caddy\n\n[caddy]:\nroot@caddy:~ #\n```\n\n#### Setting up the Caddy Server\n\nSince we’re already within the `caddy`\n\njail, we just need to install Caddy:\n\n```\npkg install caddy \nsysrc caddy_enable=\"YES\"\nservice caddy start\n```\n\nAll the configuration for Caddy will be in `/usr/local/etc/caddy/Caddyfile`\n\n, within the jail. If we want to be able to access that config file from the host system, we need to mount a directory from the host system into the jail. We can even do it in read-only mode, so the jail can’t change the config. It’s something like:\n\n```\nbastille mount caddy /usr/local/etc/my-caddy-config /usr/local/etc/caddy nullfs ro 0 0\n```\n\nOk, the server is almost all configured, but we don’t have a site yet. So let’s create a site jail, and then we go back to config Caddy.\n\n## The first site: *es.cro.to*\n\nBack during the Pandemic, the Brazilian President decided to act as if Covid-19 was just a cold and didn’t enact any urgent policies for lockdowns. He was constantly saying that the country wouldn’t stop because it couldn’t afford it, and if some people had to die, they would. Completely ignoring Science and even commonsense. History proves how wrong he was. Unsatisfied with that, I wanted to *“protest”*.\n\nSo I created a small page `es.cro.to`\n\n, which literally means “scrotum” in Portuguese, but is often used to describe someone who is a *scumbag*, an *asshole*, or something like that. I got the domain `cro.to`\n\n(which in retrospect, is really cool and sounds rather nice in English) and added a subdomain `es`\n\n. It’s divided in the exact syllables of the word, just like you find in dictionaries, so I added both the definitions of the word and a picture of the creature:\n\nescroto\n\nIt was, of course, being hosted by my old server, and got quite big traffic during that time of lockdown. I decided to start with it, after all it’s just one page with a terribly compressed photo, and it’s not really that relevant at the moment anymore.\n\nThat site is a git [repository](https://github.com/crocidb/escroto), just like all the other sites I’ll run. So I prepared to have all the sites in `/usr/local/www`\n\nin the host system and then just mount the specific site directories to their respective jails, read-only. So once I had this site in `/usr/local/www/escroto`\n\n, I proceeded to create the jail with bastille. This time, I’ll be using a bastille template for nginx: `www/nginx`\n\n. Templates are just small scripts that pre-installs some software. I didn’t have to do it, but why not?\n\n```\nbastille bootstrap https://github.com/bastillebsd/templates\nbastille create escroto 14.3-RELEASE 10.0.0.11 bastille0\nbastille template escroto www/nginx\n```\n\nI created it using IP `10.0.0.11`\n\n. We’ll need that in a second. At this point, that jail’s nginx is already serving the nginx default page. And it’s located at `/usr/local/www/nginx`\n\n(note that FreeBSD puts almost everything in `/usr/local`\n\n).\n\nThen I mounted the host’s website directory to the jail with:\n\n```\nbastille mount escroto /usr/local/www/escroto /usr/local/www/escroto nullfs ro 0 0\n```\n\nNow, from within the jail I can access the site in `/usr/local/www/escroto`\n\n. Since it’s a git repo, it has files (`.git`\n\n) that shouldn’t be served. So I wrote a `deploy.sh`\n\nscript to copy the contents of `/usr/local/www/escroto/*`\n\nto `/usr/local/www/nginx/*`\n\nand delete the `.git`\n\ndirectory:\n\n```\nrm -fr /usr/local/www/nginx/*\ncp -R /usr/local/www/escroto/* /usr/local/www/nginx/\nrm -fr /usr/local/www/nginx/.git\n```\n\nThe idea is that every time I need to deploy a new version of that site, I’ll log into my host server, update the repo and run that script:\n\n```\ncd /usr/local/www/escroto\ngit pull\nbastille cmd escroto /root/deploy.sh\n```\n\nI ended up adding a `/root/deploy.sh`\n\nscript to all my other sites there too.\n\n#### Setting Caddy’s domain to point to that jail\n\nBack to the Caddy config:\n\n```\ncro.to {\n        redir https://es.cro.to{uri} permanent\n}\n\nes.cro.to {\n        reverse_proxy 10.0.0.11\n}\n```\n\nReally simple config. Serve the main domain to redirect to `es.cro.to`\n\n, and then reverse proxy to the IP `10.0.0.11`\n\n, the `escroto`\n\njail.\n\nAfter configuring the DNS records, the site was already online.\n\n# Deploying this Blog\n\nThis blog uses **Hugo** and is a git repository on [GitHub](https://github.com/crocidb/blog). So I cloned it in `/usr/local/www/blog`\n\nand created a new Jail, just like `escroto`\n\n:\n\n```\nbastille create blog 14.3-RELEASE 10.0.0.12 bastille0\nbastille template blog www/nginx\nbastille mount blog /usr/local/www/blog /usr/local/www/blog nullfs ro 0 0\n```\n\nThen I installed Hugo within the jail with:\n\n```\nbastille pkg blog update\nbastille pkg blog install gohugo\n```\n\nThe deploy script is rather similar:\n\n```\nrm -fr /usr/local/www/nginx/*\ncd /usr/local/www/blog\nhugo -d /usr/local/www/nginx\n```\n\nBefore I moved over from the old server, I wanted to do some benchmarks, so I assigned the blog to my old domain:\n\n```\ncrocidb.cro.to {\n        reverse_proxy 10.0.0.12\n}\n```\n\nAnd just as simple as that, my blog was being served on my new server.\n\n# Benchmarking my Servers\n\nBefore I updated my DNS records, I wanted to check if the new server was going to hold the traffic. I mean, on paper it would, but I wouldn’t know if I messed some step. So I set out to find ways to benchmark my sites `crocidb.com`\n\npointing to the old server vs `crocidb.cro.to`\n\n.\n\nFirst I started by thinking *what* I wanted to test. Testing latency is useless because I know that for the majority of the people accessing my blog it would be slightly longer. Usually most of the traffic of this site comes from North America, followed by Europe and then South America. But it really shouldn’t affect the experience. So it’s better to test serving speed and big loads.\n\nThere are some free online tools that can do general tests, like [GTMetrix](https://gtmetrix.com), [Pingdom](https://pingdom.com) and [WebPageTest](https://www.catchpoint.com/webpagetest). But honestly the differences between the two servers were mostly only the latency. All the rest was pretty much the same. So I had to go a bit deeper.\n\nI found out about [wrk](https://github.com/wg/wrk) and [hey](https://github.com/rakyll/hey). Two different tools to benchmark HTTP load. They’ll basically generate thousands of concurrent requests to the website and collect information such as the latency of the requests, error responses, transfer per second, etc. For example, I ran `wrk`\n\non another VPS in Hetzner:\n\n```\nwrk -t4 -c100 -d30s --latency https://crocidb.com/\n```\n\nThis tells `wrk`\n\nto run in `4`\n\nthreads, to keep `100`\n\nconcurrent requests for `30`\n\nseconds. Not really an unrealistic number. The results were:\n\n```\nRunning 30s test @ https://crocidb.com/\n  4 threads and 100 connections\n  Thread Stats   Avg      Stdev     Max   +/- Stdev\n    Latency    89.63ms   18.31ms 189.09ms   95.29%\n    Req/Sec   211.74     38.44   313.00     87.69%\nRequests/sec:    833.41\nTransfer/sec:      8.29MB\n```\n\nOn `crocidb.cro.to`\n\ntest:\n\n```\nRunning 30s test @ https://crocidb.cro.to/\n  4 threads and 100 connections\n  Thread Stats   Avg      Stdev     Max   +/- Stdev\n    Latency     6.75ms    3.59ms  51.98ms   74.41%\n    Req/Sec     3.08k   260.11     4.34k    81.83%\nRequests/sec:  12260.10\nTransfer/sec:    130.80MB\n```\n\nThe old server handled `833`\n\nrequests per second vs `12,260`\n\non the new server. With a latency average of `89ms`\n\nvs `6ms`\n\n. But of course that was not fair: the test machine was in the same data-center as my new server. So I thought… what if I use a VPN and do the tests from several locations?\n\nI use Proton VPN, so I wrote a quick script that would get a location from a list, connect, and run `wrk`\n\n. The results were absurdly underwhelming. It recorded an average of `300`\n\nrequests per second on the old server and `800`\n\non the new one, using these locations:\n\n```\nLOCATIONS=(\n    \"US//New York|US - New York\"\n    \"US//Los Angeles|US - Los Angeles\"\n    \"BR//São Paulo|BR - São Paulo\"\n    \"GB//London|GB - London\"\n    \"DE//Frankfurt|DE - Frankfurt\"\n    \"FR//Paris|FR - Paris\"\n    \"SE//Stockholm|SE - Stockholm\"\n    \"CH//Zurich|CH - Zurich\"\n    \"IN//Mumbai|IN - Mumbai\"\n    \"JP//Tokyo|JP - Tokyo\"\n    \"KR//Seoul|KR - Seoul\"\n    \"SG//Singapore|SG - Singapore\"\n    \"AU//Sydney|AU - Sydney\"\n    \"ZA//Johannesburg|ZA - Johannesburg\"\n    \"AR//Buenos Aires|AR - Buenos Aires\"\n)\n```\n\nSo I ditched the idea of using an end-user VPN. I decided to go for a more robust solution: create real VPS on data-centers across the world.\n\n### Vultr VPS Benchmarks\n\nI needed a different VPS provider than the ones hosting my servers to avoid any infrastructure advantage. The only other VPS provider I’ve ever used outside DigitalOcean and Hetzner was Vultr. Then I decided to create a server for each location and run the tests. I settled for only four different regions, because the process was manual: **London**, **São Paulo**, **Silicon Valley** and **Tokyo**. So I created 4 of the cheapest Fedora VMs available in those locations.\n\nAfter several tests, I found out that `hey`\n\nwas more aligned with what I wanted to do. So my work was basically to ssh into the server, run this script and then copy the results. I started with São Paulo:\n\n```\ndnf install -y tmux vim htop\nwget https://storage.googleapis.com/hey-releases/hey_linux_amd64\nchmod +x hey_linux_amd64\n./hey_linux_amd64 -n 1000000 -c 10000 -t 10 -z 5m -h2 https://crocidb.com/ > crocidb.com.log\n./hey_linux_amd64 -n 1000000 -c 10000 -t 10 -z 5m -h2 https://crocidb.cro.to/ > crocidb.cro.to.log\n```\n\nIt downloads `hey`\n\n, runs it with these unrealistically heavy numbers: `1M`\n\ntotal requests, `10k`\n\nconcurrent requests, `10s`\n\ntimeout (hey would stop if any particular request took longer than 10 seconds) and a total of `5m`\n\n.\n\nOn the very first try, I noticed a real problem with my new FreeBSD server. It was failing rather early, because it couldn’t handle 10k concurrent connections. After a lot of research, I found out that you can check the size of the socket queue with `netstat -Lan`\n\n, and it was all `128`\n\n. Turns out `kern.ipc.somaxconn`\n\nwas stock-set for that number. So I increased it:\n\n```\nsysctl kern.ipc.somaxconn=16384\n```\n\nAfter roughly 10 minutes running, my logs were ready and I went ahead and looked:\n\nComparison of the output of `hey`\n\nfor both of the servers in the São Paulo VPS\n\nLeft side is the old server and right one is the new one. The difference is mindblowing.\n\nWhile both servers returned a substantial amount of errors, the FreeBSD managed to respond to the expected `1M`\n\nrequests, while the Ubuntu one couldn’t return `20k`\n\n! That’s a **MASSIVE** difference.\n\n### Benchmark Analysis\n\ncrocidb.com was running on the old server, while crocidb.cro.to was on the new one\n\nThe fact that the old server could only finish a few of the requests is mindblowing. In fact, it only completed around 7% of them, versus 94% of the FreeBSD server. There is a slight decrease of success rate in the new server on Tokyo, but nothing to worry about, I think.\n\nConsidering the amount of requests per second, the new server is at least 3x better and at most 11x!\n\nRequests per second: the higher the better\n\nHere the variation looks even more dramatic, and my guess is that the latency was much bigger in Tokyo?\n\nLatency Percentiles: `p90`\n\nis especially interesting because it measure that **90%** of the users will experience less latency than that value\n\nLooking at the latency percentiles (e.g.: `p50`\n\nmeans 50% of requests were answered faster than that value), we see an interestingly different shape for each server. The new one has a more *linear* growth until around 90%, which makes it more predictable, while the old server grows more unpredictably.\n\nThat shows me that 90% of people trying to load the main page of my blog will get the content in less than **3.5** seconds, from pretty much anywhere in the world, even under high demand. That’s pretty good.\n\nI didn’t go far enough to understand the issue with Tokyo, but I’m not that worried at the moment. Using the request phase breakdown, where `hey`\n\nanalyzes the duration of each step of the request, there’s an indication that the traffic to Japan is slower:\n\nThe breakdown of the time it took on each phase of the request\n\nBut this data looked way off to me. First because the DNS dial-up and lookup are incredibly low on the second domain. Maybe because it’s a CNAME record? And second because `resp wait`\n\n(pretty much time to first byte) and `resp read`\n\n(transfer time) were weirdly high here. But that could be explained by the fact that it’s only counting successful requests, which can indicate that the first server was quick at first until it basically shut down any new requests.\n\n#### Conclusion\n\nI don’t think this difference has to do specifically with the stack I picked. It’s probably because of misconfiguration on the old Ubuntu system and the fact that the Hetzner VPS I picked has 4 cpu cores, vs one in the DigitalOcean one: much more requests can be processed concurrently. I also don’t think that means a lot either, since it’s very unlikely these servers would ever need to serve that many requests at the same time. Maybe just one web benchmark like WebPageTest would’ve been enough.\n\n# Biting the Bullet\n\nAlthough the benchmark left many unanswered questions, I was really happy with it. So I went ahead and updated the DNS records. This is now officially running on that machine.\n\nIn the end, after many hours experimenting, tinkering, building, and breaking stuff, I figured that it isn’t that complicated to set up a FreeBSD site hosting machine. There are several web hosting services satisfying my constraints that [I could’ve used](https://openbsd.amsterdam/) instead. Or I could have installed [Proxmox](https://www.proxmox.com/en/) to deal with my containers and admin my system with a visual dashboard. Or even [Sylve](https://sylve.io/), the FreeBSD counterpart. But I like the path I took, because I learned a lot throughout it.\n\nSome of the main takeaways:\n\n- The Ubuntu server was really sturdy. It handled really well all the load in my sites for ten years. Four of the last ones without even a reboot. That all without much effort to set it up.\n- Configuring FreeBSD was easier than I thought. I like the idea of centralizing all the system configuration in one place. Also, the online documentation is really good.\n- Configuring a machine to host your own blog can require a lot of knowledge of networking that goes way beyond what a gamedev knows.\n- I had so much fun learning another system. Maybe next time I’ll do OpenBSD or NetBSD.\n\nIn the end, this is all useless, since most of my traffic comes from AI systems crawling it anyway…", "url": "https://wpnews.pro/news/blog-ran-on-ubuntu-16-04-for-10-years-i-migrated-it-to-freebsd", "canonical_source": "https://crocidb.com/post/this-blog-ran-on-ubuntu-16-04-for-10-years-i-migrated-it-to-freebsd/", "published_at": "2026-05-21 18:54:55+00:00", "updated_at": "2026-05-22 08:28:54.382635+00:00", "lang": "en", "topics": ["cloud-computing", "open-source", "cybersecurity"], "entities": ["Digital Ocean", "Ubuntu", "Hetzner", "FreeBSD", "Bastille", "WordPress"], "alternates": {"html": "https://wpnews.pro/news/blog-ran-on-ubuntu-16-04-for-10-years-i-migrated-it-to-freebsd", "markdown": "https://wpnews.pro/news/blog-ran-on-ubuntu-16-04-for-10-years-i-migrated-it-to-freebsd.md", "text": "https://wpnews.pro/news/blog-ran-on-ubuntu-16-04-for-10-years-i-migrated-it-to-freebsd.txt", "jsonld": "https://wpnews.pro/news/blog-ran-on-ubuntu-16-04-for-10-years-i-migrated-it-to-freebsd.jsonld"}}