Skip to content
Back to projects

2026

web-infra

  • Docker
  • Nginx
  • HAProxy
  • Traefik
  • OpenSSL
  • iptables
  • Locust
  • Bash

Coursework for Servidores Web de Altas Prestaciones (high-performance web servers) at the University of Granada. Each of the five practices builds on the last, so together they make one stack: eight Apache containers on Docker; Nginx, HAProxy and Traefik in front, with round robin, least connections, failover and a scaling script; HTTPS with my own root and intermediate certificate authority; deny-by-default iptables in every container plus rate limits against floods; and load tests with Apache Bench and distributed Locust. About 42 hours across the five.

Eight servers, one address

Practice 1 built the farm: one Apache + PHP image, eight containers from it, on two Docker networks with fixed addresses. The service network is internal, so nothing outside can reach it.

Practice 2 put a balancer in front, the same farm behind Nginx, HAProxy and Traefik in turn, with round robin and least connections. Stop two nodes and the balancer simply deals around them; start them again and they rejoin.

clientnginx192.168.10.50round robinweb1.10.2downweb2.10.3downweb3.10.4downweb4.10.5downweb5.10.6downweb6.10.7downweb7.10.8downweb8.10.9down

A chain of trust I signed myself

Practice 3 moved everything to HTTPS. First a single self-signed certificate; then, as the advanced task, my own small certificate authority: a root that signs an intermediate, which signs the servers' certificate. Apache serves the whole chain in one file.

The servers only speak TLS 1.2 and 1.3 with strong ciphers, and the Nginx balancer reuses TLS sessions and speaks HTTP/2. A browser still says "Not secure", and rightly: it has never heard of my root. The traffic is encrypted all the same.

A root certificate authority signs an intermediate, which signs the server certificate; the three are bundled as cadena-completa.crt for Apache. TLS 1.2 and 1.3 are accepted, TLS 1.0 and 1.1 refused.

Deny by default

Practice 4 gave every container its own firewall. A start script loads the rules before Apache starts: drop everything, then let in only HTTPS from the balancer's address. Reaching a web server directly, or the balancer's status page, now fails.

The balancer got its own limits against floods: at most 20 connections and 10 requests a second per address (bursts of 20), short timeouts, no version in its headers, and no answer to ping.

Rules: input, output and forward policies DROP; accept loopback, established connections and HTTPS from the balancer at 192.168.10.50. Results: nginx to web1 on 443 accepted; the host to web1 on 80 dropped; ping to nginx dropped; a 21st connection from one address dropped; requests beyond 10 a second, after a burst of 20, answered 503.

The benchmark that hit my own firewall

Practice 5 load-tested the finished farm two ways. Apache Bench sent 10,000 HTTPS requests, 100 at a time, from one container: about 1,354 requests a second at 73 ms. But 9,906 of them were 503s. From one address, a benchmark looks exactly like the flood practice 4 was built to stop, and the rate limit did its job.

Locust behaved more like people: 100 users, added 20 a second, spread over six worker containers, each with its own address, pausing between pages. Zero errors. The first number was a measure of my own defences, not of the farm.

Apache Bench: 10,000 HTTPS requests, 100 concurrent, from one address; 9,906 were answered 503 by the rate limit and 94 succeeded, at about 1,354 requests per second and 73 ms mean. Locust: 100 users over six workers, 0% errors.

Least connections looked exactly like round robin in Nginx. With one person clicking refresh there is never more than one open connection, so “the server with the fewest” is just the next one in line. The algorithm only shows its difference under real concurrent load, and with HAProxy, under the same test, it did show.

The bigger surprise was the benchmark (figure 4). Apache Bench reported a healthy rate, yet 9,906 of its 10,000 requests were 503s from the rate limit I had set up in the previous practice. Throughput means little until you have read the failed requests line: a fast “no” is still fast.