I always thought Linux looked cool from the outside, but I never really understood why developers became obsessed with it until I actually started using it properly.
For most of my life, I was just a normal Windows 10 user. My workflow was simple VS Code, browsers, localhost development, and deploying apps without thinking too much about what was happening underneath. Everything worked, but after building more projects, especially AI and web applications, I started feeling disconnected from the actual systems running my applications.
I knew how to build projects.
I didn’t know how infrastructure worked.
That’s when I started exploring Linux seriously.
Discovering Omarchy
After trying different Linux distributions, I found Omarchy and instantly liked it. The system felt extremely fast, minimal, and surprisingly clean. Compared to Windows, everything felt more focused. The terminal experience, window management, keyboard shortcuts, and multiple pane workflow completely changed how I worked.
The coolest part was how efficient everything felt once you got comfortable with it.
I used Omarchy for around two months before eventually breaking the entire setup. One bad configuration later, none of the UI loaded anymore and I was left staring at a shell screen with absolutely no idea how to recover the system properly.
That moment honestly taught me something important.
Linux gives you freedom, but it also expects responsibility.
Even after breaking everything though, I still loved the experience enough to come back again. This time I decided to run Omarchy inside VirtualBox on Windows 10 so I could safely experiment without destroying my main environment again.
Setting Up the VM
Once the virtual machine was running, I finally had a playground where I could:
- experiment freely
- break things safely
- learn networking
- build servers manually
- understand infrastructure properly
The goal was simple:
Build a small self-hosted infrastructure setup using:
- Docker
- Nginx
- load balancing
- networking
- benchmarking
Installing Nginx and Creating the Project
Docker was already included with Omarchy, so I mainly needed Nginx.
sudo pacman -S nginx
Then I created the project folder:
mkdir nginx-lb
cd nginx-lb
Inside the folder I created:
- 3 web apps
- an Nginx config
- a Docker Compose setup
The structure looked like this:
Each app simply had an index.html file.
app1/index.html
<h1>App 1</h1>
app2/index.html
<h1>App 2</h1>
app3/index.html
<h1>App 3</h1>
Simple, but enough to visually verify load balancing.
Docker Compose Setup
Then I created the Docker Compose configuration using nano.
Omarchy already comes with NeoVim pre-installed, and honestly the setup looks extremely cool. Once you get comfortable with it, the workflow feels insanely fast. But in real server environments, especially when you're quickly editing configs over SSH, you'll often see people using something simple like nano.
It’s quick, lightweight, always available, and efficient for fast configuration changes.
version: '3'
services:
app1:
image: nginx
volumes:
- ./app1:/usr/share/nginx/html
app2:
image: nginx
volumes:
- ./app2:/usr/share/nginx/html
app3:
image: nginx
volumes:
- ./app3:/usr/share/nginx/html
nginx-lb:
image: nginx
container_name: nginx-lb
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
At this point I finally started understanding why containers are so important.
Everything became isolated, reproducible, and easy to manage.
Understanding Load Balancing Through Nginx
This was probably the coolest part of the entire project.
The Nginx configuration looked like this:
events {}
http {
upstream backend {
server app1:80;
server app2:80;
server app3:80;
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}
}
That tiny file completely changed how I viewed backend systems.
Instead of one server handling everything, Nginx distributes requests between multiple backend containers.
This is load balancing.
Not theory anymore.
An actual working system running inside my VM.
Running the Containers
Once everything was ready, I started the infrastructure:
docker compose up -d
The containers started successfully:
docker ps
And when I opened localhost inside the VM, different apps started appearing depending on which backend handled the request.
That moment felt genuinely satisfying because I wasn’t just running applications anymore.
I was running infrastructure.
NAT vs Bridged Adapter
This part confused me for hours.
The setup worked perfectly inside the VM, but Windows couldn’t access it initially.
That’s when I learned about NAT and Bridged networking.
NAT + Port Forwarding
NAT creates an isolated internal network for the VM.
The host machine cannot directly access the virtual machine unless ports are forwarded manually.
So I configured port forwarding in VirtualBox like this:
- Host Port:
8080 - Guest Port:
80
Which basically meant:
Windows localhost:8080 -> VM port 80
After configuring this properly, I could finally access the Nginx load balancer directly from Windows.
Bridged Adapter
Bridged mode felt even cooler.
Instead of staying inside an isolated virtual network, the VM became a real device on my local network and got its own IP address.
Something like:
ip a
Example IP:
10.93.5.4
Now I could directly access the server from Windows using:
http://10.93.5.4
That honestly felt insane the first time it worked.
Networking finally stopped feeling abstract.
Stress Testing With wrk
Now came the fun part.
Benchmarking the infrastructure.
I used wrk to generate traffic and test how the setup behaved with different numbers of backend containers behind the Nginx load balancer.
The command I used was:
wrk -t4 -c100 -d30 http://127.0.0.1
Where:
-
-t4→ 4 threads -
-c100→ 100 concurrent connections -
-d30→ run test for 30 seconds
The interesting part was comparing the performance between:
- 1 backend container
- 2 backend containers
- 3 backend containers
1 Container Test
Running 30s test @ http://127.0.0.1
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 21.78ms 28.04ms 238.82ms 86.21%
Req/Sec 1.36k 1.06k 8.34k 75.55%
141456 requests in 30.05s, 34.13MB read
Socket errors: connect 0, read 0, write 0, timeout 102
Requests/sec: 4706.85
Transfer/sec: 1.14MB
With a single backend container, the setup still handled a surprisingly large amount of traffic. But the latency was noticeably higher and there were some request timeouts under heavy load.
2 Container Test
Running 30s test @ http://127.0.0.1
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 10.39ms 10.07ms 146.04ms 89.23%
Req/Sec 1.76k 1.01k 6.95k 82.19%
110705 requests in 30.10s, 26.71MB read
Socket errors: connect 0, read 0, write 0, timeout 100
Requests/sec: 3678.10
Transfer/sec: 0.89MB
Adding a second backend container reduced the average latency significantly. The load balancer was now distributing traffic across multiple services instead of pushing everything into a single container.
Even though the requests/sec result was lower during this run, the overall responsiveness and latency consistency improved.
3 Container Test
Thread Stats Avg Stdev Max +/- Stdev
Latency 13.85ms 11.87ms 247.23ms 87.54%
Req/Sec 1.74k 672.33 6.35k 75.85%
207100 requests in 30.03s, 49.97MB read
Requests/sec: 6895.35
Transfer/sec: 1.66MB
With three backend containers running behind Nginx, the setup handled the highest total request throughput.
This was the point where load balancing finally felt real to me.
Instead of one application trying to handle everything alone, requests were being distributed across multiple backend services. Even inside a small VM setup, you could clearly see how scaling horizontally changes system behavior.
What I Learned From Benchmarking
The most interesting part wasn’t just the numbers.
It was understanding the concept behind them.
Before this, “scaling” always sounded like some massive enterprise topic only huge companies cared about. But after running these tests manually, I finally understood the core idea:
Instead of making one server infinitely stronger, you can distribute traffic across multiple smaller services.
That’s basically the foundation of modern infrastructure.
What I Learned
The biggest thing I learned from this entire process is how much complexity modern platforms hide from developers.
When we deploy apps today, we rarely think about:
- reverse proxies
- ports
- traffic routing
- containers
- networking
- load balancing
But building things manually teaches an entirely different level of understanding.
I also realized infrastructure engineering feels very different from normal application development. A lot of the work is debugging, configuration, networking, and slowly understanding how systems communicate with each other.
Sometimes nothing works for hours.
Sometimes you break your entire Linux setup.
Sometimes networking feels cursed.
But when everything finally connects together correctly, it feels incredibly rewarding.
And honestly, building a self-hosted infrastructure server inside a Linux VM might be one of the coolest things I’ve learned so far.




Top comments (0)