<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Ján Regeš</title>
    <description>The latest articles on Forem by Ján Regeš (@janreges).</description>
    <link>https://forem.com/janreges</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F607877%2Fb1fae9bb-0950-4ede-892a-649ca39f3dbf.jpg</url>
      <title>Forem: Ján Regeš</title>
      <link>https://forem.com/janreges</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/janreges"/>
    <language>en</language>
    <item>
      <title>How to build a CDN (3/3): security, monitoring and practical tips</title>
      <dc:creator>Ján Regeš</dc:creator>
      <pubDate>Wed, 10 Jan 2024 12:46:02 +0000</pubDate>
      <link>https://forem.com/janreges/how-to-build-a-cdn-33-security-monitoring-and-practical-tips-11e0</link>
      <guid>https://forem.com/janreges/how-to-build-a-cdn-33-security-monitoring-and-practical-tips-11e0</guid>
      <description>&lt;p&gt;In the first two articles you learned &lt;a href="https://dev.to/janreges/how-to-build-a-cdn-1-3-introduction-and-basic-components-345o"&gt;what components&lt;/a&gt; you can build a CDN from and how to set up &lt;a href="https://dev.to/janreges/how-to-build-a-cdn-23-server-and-reverse-proxy-configuration-16md"&gt;servers and reverse proxies&lt;/a&gt; (CDN cache).&lt;/p&gt;

&lt;p&gt;In the third and last article of this series, we would like to add tips and recommendations on how to secure your own CDN, protect it from attacks, how to monitor it or how to develop it further.&lt;/p&gt;

&lt;p&gt;In the very end you will find various interesting facts or experiences that implementing our own CDN has taught us as well as some off-topic information. At the same time, I apologize for the very late publication of this third article. Some of the information given in the conclusion is not completely up-to-date, but I believe it's useful. I wish you a pleasant reading :)&lt;/p&gt;

&lt;h2&gt;
  
  
  Security
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Since your reverse proxy will also forward attacker requests to Origin servers, deploy one of the available &lt;strong&gt;WAF (Web Application Firewall)&lt;/strong&gt; to reject obvious attacker requests outright and not send them to Origin unnecessarily. In many situations, this can also prevent a cache poisoning attack. If you choose Nginx, we recommend &lt;a href="https://github.com/SpiderLabs/ModSecurity-nginx" rel="noopener noreferrer"&gt;ModSecurity&lt;/a&gt;, or &lt;a href="https://waf.nemesida-security.com/" rel="noopener noreferrer"&gt;Nemesida WAF&lt;/a&gt;. Even their basic OWASP TOP 10 rule sets will do a good service. The downside with Nemesida is that it also needs RabbitMQ to run, but the upside is that it has a background process that continuously updates the rules according to a maintained database of known vulnerabilities.&lt;/li&gt;
&lt;li&gt;If you also want to send file types from the CDN that are subject to CORS (e.g. fonts), then your CDN needs to return the correct &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; header for CORS requests with the &lt;code&gt;Origin&lt;/code&gt; header in the request. We have this configurable per-origin and by default only allow loading from the origin domain. The value &lt;code&gt;*&lt;/code&gt; is not recommended. The correct way to do this is to have a set of &lt;strong&gt;trusted origins&lt;/strong&gt; in your webserver or application configuration and only return the header for the trusted origin. It's also a good idea to be aware of what possible caching of CORS headers can do, so consider using the &lt;code&gt;Vary: Origin&lt;/code&gt; header as well.&lt;/li&gt;
&lt;li&gt;For CDNs, as with regular application servers, we recommend setting security headers. If you have a CDN mainly for static content, then especially the &lt;code&gt;X-Content-Type-Options: nosniff&lt;/code&gt; header, possibly also &lt;code&gt;X-Frame-Options&lt;/code&gt; or &lt;code&gt;X-XSS-Protection&lt;/code&gt; which make sense mainly for HTML, but possibly also for SVG or XML. Don't forget also HSTS and the &lt;code&gt;Strict-Transport-Security&lt;/code&gt; header, so that the browser already enforces HTTPS internally and doesn't allow downgrades to HTTP.&lt;/li&gt;
&lt;li&gt;To make sure your CDN is not vulnerable to &lt;a href="https://portswigger.net/research/practical-web-cache-poisoning" rel="noopener noreferrer"&gt;cache poisoning&lt;/a&gt;, we recommend setting various buffers and limit values much more strictly than origin servers usually do. At the same time, if you can afford it, it is better to ignore incoming HTTP headers and forward only a few relevant ones to the origins (e.g. &lt;code&gt;Accept&lt;/code&gt;, &lt;code&gt;Accept-Encoding&lt;/code&gt;, &lt;code&gt;Origin&lt;/code&gt;, &lt;code&gt;Referer&lt;/code&gt;, &lt;code&gt;User-Agent&lt;/code&gt;). It is also worth considering not to cache the HTTP code &lt;strong&gt;400 Bad Request&lt;/strong&gt; and definitely not to cache e.g. &lt;strong&gt;413 Request Entity Too Large&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;When deploying &lt;strong&gt;TLS v1.3&lt;/strong&gt; with &lt;strong&gt;0-RTT (early data)&lt;/strong&gt;, you need to consider the risk of &lt;a href="https://tools.ietf.org/html/rfc8470" rel="noopener noreferrer"&gt;Replay attack&lt;/a&gt;. Since our CDN is optimized and strict only for static content and blocks POST/PUT/PATCH/DELETE requests, the risk of real resulting abuse is almost zero. Furthermore, data modification in the application should never implement a GET request, but at least a POST with a CSRF token, which should additionally have a one-time validity (nonce).&lt;/li&gt;
&lt;li&gt;You can defend against &lt;a href="https://en.wikipedia.org/wiki/DNS_spoofing" rel="noopener noreferrer"&gt;DNS spoofing&lt;/a&gt; by having Nginx upstreams to originals set to IP addresses, not hostnames. We host projects for most clients on our own clustered solutions that allow sites to be accessed through a primary as well as a secondary datacenter and through multiple different IP addresses. So even a CDN upstream to a single original load-balances loading across 2-3 IP addresses at different ISPs. If you must already use hostnames, we recommend using at least &lt;a href="http://www.thekelleys.org.uk/dnsmasq/doc.html" rel="noopener noreferrer"&gt;Dnsmasq&lt;/a&gt; as a local DNS cache.&lt;/li&gt;
&lt;li&gt;This will not protect you from a DDoS attack, but you can defend against a DoS attack from a single IP address by setting &lt;a href="https://www.nginx.com/blog/rate-limiting-nginx/" rel="noopener noreferrer"&gt;&lt;strong&gt;rate-limiting&lt;/strong&gt;&lt;/a&gt; (maximum number of requests per second or minute from a single IP) and &lt;a href="https://docs.nginx.com/nginx/admin-guide/security-controls/controlling-access-proxied-http/" rel="noopener noreferrer"&gt;&lt;strong&gt;connection-limiting&lt;/strong&gt;&lt;/a&gt; (maximum number of open concurrent connections from a single IP). We recommend that you study and understand the &lt;strong&gt;burst&lt;/strong&gt; and &lt;strong&gt;delay&lt;/strong&gt; or &lt;strong&gt;nodelay&lt;/strong&gt; parameters, which fundamentally affect the behavior when an IP address starts to exceed the limits. We typically use multiple levels of rate-limiting on application servers. Also, in the case of POST/PUT/PATCH/DELETE requests, we limit the number of requests per 1 minute as a matter of principle - this effectively prevents brute-force attacks.&lt;/li&gt;
&lt;li&gt;In addition to the HSTS header, force an immediate redirect from HTTP to HTTPS.&lt;/li&gt;
&lt;li&gt;If the request comes to a URL where the domain is an IP address or another unsupported domain, use &lt;code&gt;return 444;&lt;/code&gt; – Nginx immediately terminates such a connection.&lt;/li&gt;
&lt;li&gt;Be aware of the risk and implement at least basic protection against looping - for example, refuse to process a URL that includes in the path any of the domains that the CDN "listens" on.&lt;/li&gt;
&lt;li&gt;If you don't want someone to be able to insert content from a specific origin into foreign pages (and thus draw your data), you can use the &lt;code&gt;valid_referers&lt;/code&gt; directive, which will set the variable &lt;code&gt;$invalid_referer&lt;/code&gt; according to your rules.&lt;/li&gt;
&lt;li&gt;Test your HTTPS configuration correctly at &lt;a href="https://www.ssllabs.com/" rel="noopener noreferrer"&gt;SSLLabs.com&lt;/a&gt; - you should easily achieve an A+ grade. You can also check security headers at &lt;a href="https://securityheaders.com/" rel="noopener noreferrer"&gt;SecurityHeaders.com&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In case you don't have a router in front of the servers that would forward/NAT only selected ports to your server, don't forget to set &lt;strong&gt;iptables/nftables firewall&lt;/strong&gt;. By default, everything should be disabled and only TCP ports 80 and 443 explicitly enabled. Furthermore, you can enable IPsec, SSH, etc. from your IP addresses. In terms of security, it has worked for us for a long time to bind all services that are possible, to bind only to the loopback interface and to route only selected ports from the outside using DNAT in the firewall. You can set some high per-ip rate-limiting even with DNAT at the network level, it is nicely described in the article &lt;a href="https://making.pusher.com/per-ip-rate-limiting-with-iptables/#fix-2-rate-limiting-with-the-limit-module" rel="noopener noreferrer"&gt;Per-IP rate limiting with iptables&lt;/a&gt;. We recommend completely disabling ICMP as well. But you'll probably at least enable echo-request because of the various online tools for measuring latencies in different parts of the world, such as &lt;a href="https://www.cdnperf.com/tools/cdn-latency-benchmark" rel="noopener noreferrer"&gt;CDN Latency Benchmark&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  DDoS attack protection
&lt;/h2&gt;

&lt;p&gt;The most expensive and effective DDoS protection for your CDN would be to use anycast IP addresses (which most commercial CDN providers don't even have) and use robust DDoS protection from commercial providers who have very powerful devices on the backbone that "protects" and in in case of detection of an attack, they activate mitigation and "cleaning" of the traffic of your IP ranges (scrubbing). Some of these solutions manage to clean even the largest DDoS attacks with a power of up to hundreds of Gbps. However, these solutions cost thousands of USD per month, and you definitely cannot afford them at all PoPs in the world. From our experience, we recommend &lt;a href="https://www.netscout.com/arbor" rel="noopener noreferrer"&gt;NetScout's Arbor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Just for the sake of interest, I will state that from February 25 to 27, 2018, one of our hosted Czech clients was the target of a 230Gbps DDoS attack, built on &lt;a href="https://blog.cloudflare.com/memcrashed-major-amplification-attacks-from-port-11211/" rel="noopener noreferrer"&gt;memcrashed&lt;/a&gt; (enables amplification of a UDP attack by up to tens of thousands of times, not tens/hundreds as with DNS or NTP amplification attacks). The first big memcrashed attack came on Cloudflare, and as the first in the Czech Republic, just 1 day later, we had to deal with it. If you don't pay for robust DDoS protection, expect that in the event of a massive attack, only the ISP will call you, saying that in order to protect the entire data center and all its clients, they must completely block your IP subnets on the backbone network (blackhole) until the end of the attack.&lt;/p&gt;

&lt;p&gt;So, if you are serious about CDN and its high availability, you need to have DDoS protection arranged at least for the main PoPs. At worst, at least a few of them should be able to withstand even the biggest attack. If you use &lt;a href="https://dev.to/janreges/how-to-build-a-cdn-1-3-introduction-and-basic-components-345o#geodns-with-failover-support"&gt;GeoDNS with auto-failover&lt;/a&gt; as I described in the first article and if you follow the rule of always returning at least 2 IP addresses of independent providers in each world location, CDN users would necessarily not even notice some DDoS attacks.&lt;/p&gt;

&lt;p&gt;What we based our DDoS protection design on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We have very rich statistics from all our PoPs, possibly even from routers for some. We therefore have a detailed overview and trends of legitimate traffic - the number of open connections, packets, unique IP addresses and GEO information about them. We also collect and monitor NetFlow data for some PoPs. Having detailed information about legitimate traffic and its peaks is key - only on the basis of it is it possible to make correct decisions and propose optimal limits for activating mitigation.&lt;/li&gt;
&lt;li&gt;From all the DDoS attacks that we have completed in the past, we know that more than 99% of the source IP addresses involved were outside the Czech Republic - that means outside the country of our majority visitors.&lt;/li&gt;
&lt;li&gt;We certainly cannot afford anycast IP addresses for our PoPs. However, there are a few providers on the market that offer physical or virtual servers with anycast IP addresses.&lt;/li&gt;
&lt;li&gt;Anycast IP addresses and robust DDoS protection are provided by our DNS providers (Constellix, Cloudflare and ClouDNS). They have such a robust infrastructure that DDoS attacks on their NS servers should withstand.&lt;/li&gt;
&lt;li&gt;We have robust DDoS protection capable of handling hundreds of Gbps at some PoPs in the Czech Republic. Other PoPs have to make do with any robust DDoS protection of the entire network of a specific ISP (most of them have it at least for an additional fee).&lt;/li&gt;
&lt;li&gt;Due to the nature of the CDN (different resolved IP addresses in different parts of the world), a higher resistance to DDoS attacks can seemingly follow. It is true, but only partially. Finding out all the IP addresses that your CDN domain/hostname resolves to in different corners of the world is a matter of minutes. The attacker therefore needs to direct the attack to multiple IP addresses (which are also not anycast), so attacking the entire CDN network is only several times more difficult (or requires more power), but not impossible. But if they attack only the domain, then attack sources from, for example, Asia will really only affect PoPs in Asia, so in our case the impact on legitimate primary visitors is almost zero.&lt;/li&gt;
&lt;li&gt;Except for a few exceptions with 10 Gbps, we have a maximum of 1 or 2×1 Gbps line (bonding) everywhere. That's a pretty thin pipe, however, most of the world's DDoS attacks are statistically smaller attacks around 2-5 Gbps, so if we have firewalls on routers or in Linux set up optimally, we can withstand it quite decently.&lt;/li&gt;
&lt;li&gt;We have GeoDNS available with minute health checks and automatic failover, so in the event of a successful attack (unavailability of some IP addresses/ports) we can connect backup PoPs to the CDN network, which the attacker did not know about until now (DNS translation has never shown them), or it is familiar PoPs, but with robust DDoS protection.&lt;/li&gt;
&lt;li&gt;We know that 90% of legitimate traffic at some PoPs consists of traffic from IP addresses of a specific country/continent. We can take this into account for setting geo-based limiting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A couple of tips on how to handle DDoS protection at the end server level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When you use a Linux firewall or have Linux-based routers, drop all &lt;strong&gt;unwanted traffic directly in the RAW table&lt;/strong&gt; (UDP, ICMP or TCP ports other than 80/443). For UDP, only allow responses from the IP whitelist of the DNS servers you use. In this way, you can protect end devices (servers or routers) against UDP amplification attacks and ICMP flood as effectively as possible. If you only do it in the standard filter, which is after connection-tracking, it's already too late. The processor had to deal with each connection or packet (prerouting, connection tracking, mangle, nat, filter) and each open connection also allocates memory.&lt;/li&gt;
&lt;li&gt;TCP SYN flood on port 80/443 can be prevented by using rate limiting (in iptables &lt;strong&gt;limit&lt;/strong&gt; or &lt;strong&gt;dst-limit&lt;/strong&gt;), where you say how many new connections with the &lt;code&gt;SYN&lt;/code&gt; flag per time (typically a second or a minute) ) you accept (globally or with respect to the src/dest IP address or port). Similar to Nginx, the key here is to properly understand the meaning of the &lt;strong&gt;burst&lt;/strong&gt; setting and understand the &lt;a href="https://en.wikipedia.org/wiki/Leaky_bucket" rel="noopener noreferrer"&gt;leaky bucket&lt;/a&gt; algorithm. Be sure to activate &lt;strong&gt;SYN cookies&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You can protect L7 itself (HTTP/HTTPS traffic) with rate and connection limiting on the firewall and secondarily on Nginx (however, it will never be as effective as a firewall).&lt;/li&gt;
&lt;li&gt;For PoPs where you know the vast majority of legitimate traffic is local traffic, download the IP subnets of the country/countries (e.g. from &lt;a href="http://ip2location.com/" rel="noopener noreferrer"&gt;ip2location.com&lt;/a&gt;). E.g. on PoPs in the Czech Republic, you can have more benevolent rate-limiting for the Czech Republic, but you can be significantly stricter for other countries. When the size of the attack does not exceed the throat of your pipe (connectivity), most likely the majority of visitors from the Czech Republic will not even notice the outage, and you will filter out foreign attacking IP addresses. With good routers, you can easily ensure this, including the dynamic creation of an IP blacklist (which you then directly filter in the RAW table). If you only use a firewall in Linux, you can use &lt;strong&gt;ipset&lt;/strong&gt; to manage these IP lists. Whichever firewall you use, study the meaning of the definition of so-called "chains" in order to minimize the number of firewall rules that connections/packets must go through for their final approval or rejection. Use DROP, not REJECT, to reject. If your firewall allows it and you have a lot of memory, you can also use TARPIT for some TCP situations and slow down the attacker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extra tip (our non-standard, but functional solution for medium-sized DDoS attacks on L7):&lt;/strong&gt; A DDoS attack on L7 is a situation where an attacker sends thousands of HTTP or HTTPS requests to your servers per second from thousands of completely unique IP addresses various around the world. Usually these L7 attacks are "only" hundreds of Mbps or units of Gbps, so you can handle it. To give you an idea - if an attacker is to generate 1 Gbps of traffic at the input with 500B (bytes) of HTTP/HTTPS requests, he needs to generate 250,000 requests consistently per second. The proposed solution is optimally implemented on the router, or on the SW firewall of your server (iptables/nftables and ipset). The solution consists in defining several levels of connection-limit rules with different high limits for different sized IP subnets (e.g. . &lt;code&gt;/3&lt;/code&gt;, &lt;code&gt;/8&lt;/code&gt;, &lt;code&gt;/16&lt;/code&gt;, &lt;code&gt;/24&lt;/code&gt;) and when the number of open connections from a given IP subnet exceeds the limits, you add the IP address (or, in extreme cases, the entire IP subnet) to the temporary blacklist (technically, in the case of Linux &lt;code&gt;ipset&lt;/code&gt; with timeout), which ensures the DROP of all source traffic already directly at the input, in the RAW table. Usually, even in the case of a DDoS attack, several requests come from each source IP address at the same time. IP subnet &lt;code&gt;/3&lt;/code&gt; will temporarily block an eighth of global IP addresses, or even all 8 &lt;code&gt;/3&lt;/code&gt; IP subnets, if it is a really extensive DDoS attack. But if you set it in combination with the previous recommendation and traffic from the Czech Republic IP addresses (or domestic IP addresses of the given PoPs), you enable higher limits and these rules are processed earlier, the majority of visitors get to the reverse proxy (cache) and the CDN will be work, although it will send content more slowly due to saturated connectivity. From other corners of the world (some high-traffic IP subnets), however, you will temporarily drop traffic on the given PoPs, and the attacker will feel that he has brought down your servers (= a successful DDoS attack), because he will have ports 80 and 443 unavailable. Of course, then you need to have your origin servers on the IP whitelist, the IP addresses through which monitoring, IPsec, DNS, etc. connect to the servers. This solution is a bit strange and we invented it ourselves, but it works very well even in a real DDoS attack. However, it is necessary to set the individual limit levels with balance and based on the maximum number of open TCP connections during peaks, which the monitoring will show you. Then, for example, you can set the limit of the number of open TCP connections for the entire huge &lt;code&gt;/3&lt;/code&gt; IP subnet to 5-10 times the previous peak peak time. This will not limit legitimate traffic and you may be able to withstand a DDoS attack.&lt;/li&gt;
&lt;li&gt;If you have the options, test your DoS and DDoS protections, analyze the behavior, monitor the related load. There are also online tools that, for a fee, can generate quite a lot of traffic from a large number of unique IP addresses and it is not something immoral from the dark net.&lt;/li&gt;
&lt;li&gt;Design some active mechanisms that will immediately notify you of an ongoing attack - for example, by monitoring the size of the blacklist queue.&lt;/li&gt;
&lt;li&gt;In any case, when designing these protections, it is good to know how/why and how long TCP connections are open, what governs it and how it behaves from a TCP point of view in the case of the majority of HTTP2 traffic today. In the event of a reaction to an active attack, you can automatically temporarily reduce various timeouts in the TCP stack or on the web server, start sending the &lt;code&gt;Connection: close&lt;/code&gt; header, etc.&lt;/li&gt;
&lt;li&gt;It is also worth mentioning here the possibility of using &lt;a href="https://www.fail2ban.org/" rel="noopener noreferrer"&gt;Fail2Ban&lt;/a&gt;, however, the way it is detected and how it works in the case of a large-scale DDoS attack, where tens/hundreds of thousands of lines start appearing in the log a second, not much help. Logging alone then easily writes 10 MB/s to the disk, and if you did not have log buffering access turned on, extreme IOPS.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Monitoring
&lt;/h2&gt;

&lt;p&gt;Regardless of how many servers your CDN consists of, you need to actively and passively monitor them.&lt;/p&gt;

&lt;p&gt;We use Nagios for active monitoring and Munin for quick basic graphs of vital signs. In Munin, we can also quickly view trend charts for several years. This is simply not possible with the &lt;strong&gt;Kibana&lt;/strong&gt; listed below (part of the &lt;a href="https://www.elastic.co/elastic-stack/" rel="noopener noreferrer"&gt;&lt;strong&gt;Elastic stack&lt;/strong&gt;&lt;/a&gt;), due to the size of the indexes, or it is necessary to use transformations/rollup into archive indexes.&lt;/p&gt;

&lt;p&gt;For more live statistics we use 2 other tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We use &lt;strong&gt;collectd&lt;/strong&gt; to collect metrics of all vital functions (CPU, RAM, IOPS, storage, network, Nginx) - we send everything to Kibana.&lt;/li&gt;
&lt;li&gt;Using &lt;strong&gt;filebeat&lt;/strong&gt;, we send all access and error logs to another Kibana. From &lt;strong&gt;Ansible&lt;/strong&gt;, we generate Nginx vhosts so that each origin has its own access and error log.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In individual Kibanas, we have dashboards summarizing CDN traffic as a whole as well as breakdowns by individual servers (PoPs). Thanks to the evaluation of absolutely all metrics from access logs, we have detailed information about, for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cache hit-ratio&lt;/li&gt;
&lt;li&gt;statistics of IP addresses and rendering of traffic to the GEO map of the world&lt;/li&gt;
&lt;li&gt;statistics of HTTP codes&lt;/li&gt;
&lt;li&gt;statistics of data transfers (we collect the sizes of requests and responses)&lt;/li&gt;
&lt;li&gt;response time statistics&lt;/li&gt;
&lt;li&gt;breakdown by servers (PoPs) or individual GEO locations&lt;/li&gt;
&lt;li&gt;breakdown by origin domains&lt;/li&gt;
&lt;li&gt;breakdown by content types (JS/CSS/images/fonts/audio/video)&lt;/li&gt;
&lt;li&gt;breakdown by specific URLs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We recommend monitoring the DNS resolving of your CDN domains as well, so that you can constantly check whether the GeoDNS providers always return the expected sets of IP addresses. We implemented this monitoring as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nagios monitors the controls listed below every minute and immediately notifies us by e-mail and SMS of unexpected conditions or slow responses of NS (name servers).&lt;/li&gt;
&lt;li&gt;We wrote a Nagios plugin, which receives the NS server (e.g. &lt;code&gt;ns11.constellix.com&lt;/code&gt;, or perhaps &lt;code&gt;8.8.8.8&lt;/code&gt;), the tested domain (e.g. &lt;code&gt;my.cdn.com&lt;/code&gt;), a set of expected IP addresses, min. the number of IP addresses, how many of the set must occur in resolving and, of course, the maximum response time and timeout of the NS server. In the event that DNS resolve does not contain the expected set of IP addresses in min. number, or the domain is resolved to another IP address/addresses, or resolving takes a long time, notifications are sent.&lt;/li&gt;
&lt;li&gt;In this way, every minute we test absolutely all authoritative NS servers of our GeoDNS providers (6× NS Constellix and 4× NS ClouDNS).&lt;/li&gt;
&lt;li&gt;Every minute we also check the correct functionality of DNS resolving on the popular recursive cache NS servers of Google (8.8.8.8) and Cloudflare (1.1.1.1) to make sure that there is no hitch on the way between the authoritative and recursive DNS servers.&lt;/li&gt;
&lt;li&gt;We carry out this monitoring both from our servers in the Czech Republic and in other countries through NRPE agents, while, for example, in the case of a plugin running on a German server, it is checked that the DNS has been translated to the IP addresses of our German POPs.&lt;/li&gt;
&lt;li&gt;We record the results of all these checks in daily-rotated logs and, if necessary, serve as a basis for retroactive analysis of problems or anomalies.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Other useful tools
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;vnstat&lt;/strong&gt; is recommended for quick network traffic statistics on individual servers. Commands such as &lt;code&gt;vnstat -l&lt;/code&gt; for live info, or statistical &lt;code&gt;vnstat -h&lt;/code&gt;, &lt;code&gt;vnstat -d&lt;/code&gt; or &lt;code&gt;vnstat -m&lt;/code&gt; are also often useful. &lt;strong&gt;iptraf-ng&lt;/strong&gt; again for a detailed analysis of current traffic. For an overview of TCP connections, use &lt;code&gt;ss -s&lt;/code&gt; or e.g. &lt;code&gt;ss -at&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For a quick live overview of what the server is currently doing in all important areas, we prefer &lt;strong&gt;dstat&lt;/strong&gt;, specifically with the &lt;code&gt;dstat -ta&lt;/code&gt; switches. And of course &lt;strong&gt;htop&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If you don't have experience with Kibana yet, take a look at &lt;a href="https://grafana.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Grafana&lt;/strong&gt;&lt;/a&gt; with InfluxDB. We've been using Kibana for years and have hundreds of custom visualizations and dashboards in it (that's why it was our first choice), but our latest experience is that Grafana with InfluxDB is overall faster, especially for long-term dashboards. However, the concept of working with data, creating visualizations and dashboards is quite different.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tips and highlights from the implementation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;When implementing some functionalities, you will definitely encounter one inconvenience in Nginx – the &lt;code&gt;add_header&lt;/code&gt; directive does not behave inheritably. If you set &lt;code&gt;add_header&lt;/code&gt; in the &lt;code&gt;server&lt;/code&gt; level and then also inside the &lt;code&gt;location&lt;/code&gt;, only the headers set in the &lt;code&gt;location&lt;/code&gt; will be sent in the final, but those set one level higher in the &lt;code&gt;server&lt;/code&gt; will be ignored. For that reason, it is better to use the &lt;a href="https://github.com/openresty/headers-more-nginx-module" rel="noopener noreferrer"&gt;more-headers&lt;/a&gt; module and its functions that behave inherited (&lt;code&gt;more_set_headers&lt;/code&gt;, &lt;code&gt;more _clear_headers&lt;/code&gt;, &lt;code&gt;more_set_input_headers&lt;/code&gt;, &lt;code&gt;more_clear_input_headers&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;If you use Debian, I recommend using the repo from &lt;a href="https://deb.sury.org/" rel="noopener noreferrer"&gt;Ondřej Surý&lt;/a&gt; (&lt;a href="https://packages.sury.org/" rel="noopener noreferrer"&gt;packages.sury.org&lt;/a&gt;) (thanks to Ondřej thank you for maintaining this repo), which, in addition to the latest versions of Nginx, also contains compatible versions of the more-headers module.&lt;/li&gt;
&lt;li&gt;The standard of reliability for us has long been &lt;a href="http://www.haproxy.org/" rel="noopener noreferrer"&gt;HAProxy&lt;/a&gt;, which we have been using for many years for load balancing and various auto-failover scenarios. In addition, since version 2, it has completely redesigned and improved handling of HTTP requests and more robust HTTP/2 support. We first tried to use HAProxy instead of Nginx, but unfortunately it only has very limited caching capabilities, which is critical for a CDN. However, we would certainly use HAProxy as a load-balancer in the event that we have multiple servers behind one PoP.&lt;/li&gt;
&lt;li&gt;If you want maximum performance, we recommend trying &lt;strong&gt;H2O&lt;/strong&gt; instead of Nginx - &lt;a href="https://h2o.examp1e.net/" rel="noopener noreferrer"&gt;https://h2o.examp1e.net/&lt;/a&gt;. We have many years of experience with Nginx, so even more complex scenarios are already fully automated in Ansible. Transcription into H2O would definitely be interesting, but also quite time-consuming. In addition, the ratio of 500 open and 650 closed tickets on GitHub is a sign that it is not yet completely production ready.&lt;/li&gt;
&lt;li&gt;If you have even greater demands on the functioning of the cache, we recommend &lt;a href="https://varnish-cache.org/" rel="noopener noreferrer"&gt;Varnish&lt;/a&gt; instead of Nginx. Nginx is great and according to our measurements a bit more powerful, but with Varnish you can get, for example, &lt;strong&gt;cache tagging support&lt;/strong&gt; through HTTP headers, when you can then selectively invalidate the cache of all URLs with the desired tag. This can be very useful, e.g. in combination with caching of POST requests (e.g. on the GraphQL API), where after detecting a change in some entity on the BE, you could invalidate all relevant caches on the API layer. This is how we cache and invalidate it at the application layer, and our future goal is to cache it at the data level in the CDN as well. For future web projects, we want to stick to the &lt;a href="https://jamstack.org/" rel="noopener noreferrer"&gt;JAMStack&lt;/a&gt; philosophy, where such a CDN with smart options for selective cache invalidation plays a key role. Therefore, we will definitely be using Varnish for our CDN in the future, probably in combination with Nginx.&lt;/li&gt;
&lt;li&gt;If you want to support &lt;strong&gt;HTTP/3 (QUIC)&lt;/strong&gt; we recommend &lt;a href="https://github.com/cloudflare/quiche" rel="noopener noreferrer"&gt;quiche&lt;/a&gt; from Cloudflare, or &lt;a href="https://github.com/litespeedtech/%20lsquic" rel="noopener noreferrer"&gt;lsquic&lt;/a&gt; which is part of the &lt;a href="https://openlitespeed.org/" rel="noopener noreferrer"&gt;OpenLiteSpeed&lt;/a&gt; web server. For now, we are just experimenting with HTTP/3. It requires BoringSSL instead of OpenSSL and additionally Nginx older version 1.16.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UPDATE&lt;/strong&gt;: The point above was valid at the end of 2021. At the beginning of 2024, you already have HTTP/3 support directly in Nginx. We are currently cautious with the deployment of HTTP/3, especially due to the risks of DoS/DDoS attacks, for which we do not yet have sufficient protection mechanisms with UDP.&lt;/li&gt;
&lt;li&gt;If you use virtualized servers and have the supported HW, use &lt;strong&gt;SR-IOV&lt;/strong&gt; and driver &lt;strong&gt;ixgbevf&lt;/strong&gt; with setting &lt;code&gt;InterruptThrottleRate=1&lt;/code&gt;. The queue of incoming requests will be processed more efficiently and the CPU load will also be reduced.&lt;/li&gt;
&lt;li&gt;If you have a lot of CPU cores and optimize for hundreds of thousands of requests per second, also focus on &lt;strong&gt;RPS (Receive Packet Steering)&lt;/strong&gt;, because usually only one CPU core processes the incoming queue.&lt;/li&gt;
&lt;li&gt;For those who are also interested in various network details regarding browser requests, HTTP/2 streams or DNS resolving, we recommend studying the tools around Google Chrome. Specifically &lt;a href="https://dev.tochrome://net-internals/"&gt;&lt;strong&gt;chrome://net-internals/&lt;/strong&gt;&lt;/a&gt;, &lt;a href="https://dev.tochrome://net-export/"&gt;&lt;strong&gt;chrome://net-export/&lt;/strong&gt;&lt;/a&gt; and related tool &lt;a href="https://netlog-viewer.appspot.com/" rel="noopener noreferrer"&gt;https://netlog-viewer.appspot.com/&lt;/a&gt;. It helps us to understand the influence of the behavior of HTTPs requests on the rendering of the page itself, also to reveal blind spots where something is waiting, etc.&lt;/li&gt;
&lt;li&gt;If you really want to understand HTTP/2 and optimize the loading speed of your pages, install &lt;a href="https://nghttp2.org/" rel="noopener noreferrer"&gt;&lt;strong&gt;nghttp2&lt;/strong&gt;&lt;/a&gt; and understand how HTTP/2 communicates directly with your website. You can try, for example, the command &lt;code&gt;nghttp -nv https://dev.to/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The performance of the server and its connectivity can be easily tested, e.g. using the one-line &lt;a href="https://github.com/n-st/nench" rel="noopener noreferrer"&gt;nench benchmark&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;In the case of hosting large files, it is necessary to realize that even if the client makes a byte-range request, your CDN must first load the entire file from the origin (or cache it) and only then return the required chunk from it. That's why it can be better if you have the option to push these videos and other large files to the CDN before visitors start accessing them. But you can also help yourself by using the &lt;a href="http://nginx.org/en/docs/http/ngx_http_slice_module.html" rel="noopener noreferrer"&gt;slice module&lt;/a&gt; of Nginx, which can download and cache only configurable "chunks" from Origin.&lt;/li&gt;
&lt;li&gt;Beware of the popular and sometimes slightly treacherous recursive cache DNS servers of Google (8.8.8.8, 8.8.4.4) or Cloudflare (1.1.1.1). It is not uncommon for Czech visitors to occasionally translate requests to foreign IP addresses. But it only happens once every few days or hours and it usually only lasts a few minutes.&lt;/li&gt;
&lt;li&gt;Although CDN PoPs as such are functionally fully autonomous and independent, you will still need a connection to some central location for their management, monitoring or e.g.: distribution of cache-purge requests. Therefore, set up IPsec tunnels using &lt;strong&gt;strongSwan&lt;/strong&gt; or &lt;strong&gt;WireGuard&lt;/strong&gt;, the configuration of which can be very nicely automated.&lt;/li&gt;
&lt;li&gt;When implementing cache deletion, you can use the script &lt;a href="https://github.com/perusio/nginx-cache-purge/blob/master/nginx-cache-purge" rel="noopener noreferrer"&gt;nginx-cache-purge&lt;/a&gt;, which shows how cache files can be effectively found by URL or mask. I also recommend the articles &lt;a href="https://scene-si.org/2016/11/02/purging-cached-items-from-nginx-with-lua/" rel="noopener noreferrer"&gt;Purging cached items from Nginx with Lua&lt;/a&gt; and &lt;a href="https://scene-si.org/2017/01/08/improving-nginx-lua-cache-purge/" rel="noopener noreferrer"&gt;Improving NGINX LUA cache purges&lt;/a&gt;. We decided to base it on &lt;a href="https://gist.github.com/nosun/0cfb58d3164f829e2f027fd37b338ede" rel="noopener noreferrer"&gt;this Lua script&lt;/a&gt;, we just added a few of our modifications. If you script it in Lua, we recommend making a vhost listening on a non-standard port, which you will only have available through an IPsec tunnel. If you also implement static brotli/gzip compression, don't forget to delete your &lt;code&gt;.br&lt;/code&gt;/&lt;code&gt;.gz&lt;/code&gt; files or &lt;code&gt;.webp&lt;/code&gt;/&lt;code&gt;.avif&lt;/code&gt; files as well.&lt;/li&gt;
&lt;li&gt;If you are deploying your own or commercial CDN in front of your entire domain, be aware of one potential vulnerability that you can quickly overlook. Respect the client IP address from the &lt;code&gt;X-Forwarded-For&lt;/code&gt; header only if the request comes to you via the network only from specific known public IP addresses of CDN servers. In Nginx, trusted sources are defined via the &lt;a href="http://nginx.org/en/docs/http/ngx_http_realip_module.html" rel="noopener noreferrer"&gt;realip module&lt;/a&gt; and the &lt;code&gt;set_real_ip_from&lt;/code&gt; directive. Never use something like &lt;code&gt;set_real_ip_from 0.0.0.0/0&lt;/code&gt;. If you have some part of the domain or application functionality limited only to the IP whitelist, then the attacker could obtain another IP address with the HTTP header.&lt;/li&gt;
&lt;li&gt;In case you decide to use a commercial CDN, we recommend the domestic &lt;a href="https://www.cdn77.com/" rel="noopener noreferrer"&gt;CDN77&lt;/a&gt; because their support can ensure that all requests to your source Origin domain will only come from a few fixed IP addresses in the Czech Republic (their CDN proxy servers), and you can set them as trusted. Usually, CDN providers do not tell you the entire list of possible IP addresses of their PoPs, and you cannot rely on the fact that they send a header e.g. &lt;code&gt;Via: cdn-provider&lt;/code&gt; in requests. This is simply not safe and can be easily thrown away, while the support of CDN providers will often recommend such a dangerous solution.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion and practical experience
&lt;/h2&gt;

&lt;p&gt;We hope that the series of these 3 articles helped you and showed you how you can build a CDN yourself. We have described to you what it consists of, and how you can lay out specific components and set them up yourself. But carefully consider whether it is really worth it for your needs. Also keep in mind that you will have several servers running around the world that need to be paid for and also taken care of and patched.&lt;/p&gt;

&lt;p&gt;Our CDN, built as described in this article, works great. We have it under close scrutiny and carefully actively and passively monitor traffic on all servers. Its performance and speed in browsers is even higher than commercial CDNs (thanks to static compression and also the fact that most of the content is in RAM, since we don't have thousands of clients). We gradually deploy it in the projects we develop for our clients. Thanks to this, we cover Europe in particular very well and under our own power. In order to cover remote corners of the world just as well, we use another commercial CDN in these secondary locations. We know that we provide our clients with a quality service at a good price. And technically, we have another interesting project in our portfolio that is "living" and brings real value.&lt;/p&gt;

&lt;p&gt;In addition, since the production deployment in 09/2019, not a single problem has appeared - all components work flawlessly. We tried not to underestimate anything - the production deployment was preceded by stress and penetration tests. We looked for post-mortems of various successful commercial CDN attacks and tried to debug our configurations according to them. We first tested the functionality on various non-production environments of our client projects. Search engines detect the use of our CDN correctly - despite the fact that the images are loaded from the CDN domain, they are indexed correctly under the domain of origin.&lt;/p&gt;

&lt;p&gt;In the future, we will consider splitting the CDN into two parts - one optimized especially for many small, frequently loaded files (eg JS/CSS/icons/fonts) and the other for larger files (eg audio/video or large images). Such a solution can have 2 advantages - the browser will allow even more parallelism when rendering the page (assets will be loaded from several different CDN domains/IP addresses depending on the type) and it will also allow fine-tuning even more precisely to the level of traffic, more efficient use of cache, or HW selection.&lt;/p&gt;

&lt;p&gt;In our heads, we still have the option of using our CDN as a reverse proxy in front of the entire client domain, i.e. for all requests, including POST/PUT/DELETE. This would give us the benefit of another level of DDoS protection against Origin servers, but we would deprive ourselves of other benefits – especially targeted optimization for static content and also the use of higher parallelism in browsers, thanks to loading content from several different domains, or IP address. At the same time, it would be very tempting for each PoP to use multiple servers for different types of content with load balancing between these servers, e.g. according to the suffix in the URL. But we have a lot of such possible improvements in the drawer, and maybe they will give us meaning and return in the coming years.&lt;/p&gt;

&lt;h2&gt;
  
  
  I'm asking everyone - let's report bugs
&lt;/h2&gt;

&lt;p&gt;CDN implementation and debugging also showed us that all technologies have flaws. The more super-features someone brings, the more bugs they make. And that regardless of whether it is developed and tested by one or thousands of people. That's why I have one personal &lt;strong&gt;off-topic request&lt;/strong&gt;: please don't be lax and when we encounter a problem, &lt;strong&gt;report it to the authors and don't expect someone to do it for us&lt;/strong&gt;. This way we will solve the community problem, but also our problem, and at the same time we will learn a lot more, because we often have to go in depth. It also teaches us to communicate things to the other party in an understandable form.&lt;/p&gt;

&lt;p&gt;I used to not do it myself, and I thought to myself that "&lt;em&gt;they will surely quickly find out and fix it themselves&lt;/em&gt;". A mistake and a faulty reasoning, which I admitted to myself over time...&lt;/p&gt;

&lt;p&gt;However, in recent years I have already reported or participated in the correction of various errors myself and problems. For example in Firefox (bugs in behavior and headers around AVIF), Google Chrome (problems with CORS vs. cache vs. prefetching), web server Nginx (HTTP/2), PHP (OPcache), ELK Stack (UI/UX errors in Kibana and Grok in Logstash), in Mikrotik RouterOS or GlusterFS. I also have 13 tickets for MariaDB and MaxScale proxy. Although I could not help with these technologies as a developer, I at least provided enough comprehensible information so that developers could quickly understand the problems, simulate and fix it. &lt;strong&gt;If you happen to be making some resolutions to 2024, the willingness to open well-described tickets or send PR could be one of them&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you are interested in any other CDN-related details, ask in the comments or ask on X/Twitter &lt;a href="https://twitter.com/janreges" rel="noopener noreferrer"&gt;@janreges&lt;/a&gt;. I will be happy to answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test your websites with my analyzer
&lt;/h2&gt;

&lt;p&gt;In conclusion, I would like to recommend one of my personal open-source projects, which I would like to help improve the quality of websites around the world. The tool is available as a &lt;a href="https://github.com/janreges/siteone-crawler-gui" rel="noopener noreferrer"&gt;desktop application&lt;/a&gt;, but also a &lt;a href="https://github.com/janreges/siteone-crawler" rel="noopener noreferrer"&gt;command-line tool&lt;/a&gt; usable in CI/CD pipelines. For Windows, macOS and Linux.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftz3qaz6upoowg5tptmfk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftz3qaz6upoowg5tptmfk.png" alt="SiteOne Crawler - Free Website Analyzer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I launched it at the end of 2023 and I believe that it will help a lot of people to increase security, performance, SEO, accessibility or other important aspects of a quality web presentation or application. It's called &lt;a href="https://crawler.siteone.io/?utm_source=dev.to&amp;amp;utm_campaign=cdn-part-3"&gt;SiteOne Crawler - Free Website Analyzer&lt;/a&gt; and I also wrote an &lt;a href="https://dev.to/janreges/siteone-crawler-useful-tool-you-will-oe1"&gt;article&lt;/a&gt; about it. Below you will find 3 descriptive videos - the last one also shows what report it will generate for your website.&lt;/p&gt;

&lt;p&gt;In addition to various analyses, it also offers, for example, the export of the entire website into an offline form, where you can view the entire website from a local disk without the internet, or the generation of sitemaps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sharing this project&lt;/strong&gt; with your colleagues and friends will be the &lt;strong&gt;greatest reward for me&lt;/strong&gt; for writing these articles. &lt;strong&gt;Thank you and I wish you all the best in 2024&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Desktop Application&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/rFW8LNEVNdw"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Command-line tool&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/25T_yx13naA"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;HTML report - analysis results&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/PHIFSOmk0gk"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
    </item>
    <item>
      <title>SiteOne Crawler — website analyzer you will ♥</title>
      <dc:creator>Ján Regeš</dc:creator>
      <pubDate>Sat, 09 Dec 2023 22:12:29 +0000</pubDate>
      <link>https://forem.com/janreges/siteone-crawler-useful-tool-you-will-oe1</link>
      <guid>https://forem.com/janreges/siteone-crawler-useful-tool-you-will-oe1</guid>
      <description>&lt;p&gt;Greetings to all web developers, QA engineers, DevOps, website owners, IT students or consultants in the online environment.&lt;/p&gt;

&lt;p&gt;I would like to introduce you all to a &lt;strong&gt;very useful&lt;/strong&gt; and &lt;strong&gt;open-source&lt;/strong&gt; tool that I believe you will quickly come to love and will be a useful tool for you in the long run. The goal of the tool is to &lt;strong&gt;help improve the quality of websites worldwide&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It &lt;strong&gt;analyzes&lt;/strong&gt; your &lt;strong&gt;entire website&lt;/strong&gt;, every single file found, provides you with a &lt;strong&gt;clear report&lt;/strong&gt; and has additional features, such as complete &lt;strong&gt;export&lt;/strong&gt; of your website to &lt;strong&gt;offline version&lt;/strong&gt;, where you can view your website from a local disk or USB stick.&lt;/p&gt;

&lt;p&gt;This tool can be used as a &lt;strong&gt;desktop application&lt;/strong&gt; (for &lt;strong&gt;Win&lt;/strong&gt;/&lt;strong&gt;macOS&lt;/strong&gt;/&lt;strong&gt;Linux&lt;/strong&gt;) or just as a &lt;strong&gt;command-line&lt;/strong&gt; tool with clear and detailed output in the console, also usable in CI/CD pipelines. &lt;em&gt;Note:&lt;/em&gt; In the next few days we will set up an Apple and Microsoft developer account so that we can &lt;strong&gt;properly sign&lt;/strong&gt; the desktop apps and the installation will be trusted. At the same time, to get the applications into the official App Store or Microsoft Store.&lt;/p&gt;

&lt;p&gt;If you don't like reading, scroll to the end of the article with &lt;strong&gt;videos&lt;/strong&gt; where there are practical examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;SiteOne Crawler - &lt;a href="https://crawler.siteone.io/?utm_source=dev.to&amp;amp;utm_campaing=siteone-crawler-useful-tool-you-will-love"&gt;&lt;strong&gt;https://crawler.siteone.io/&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Desktop Application - &lt;a href="https://github.com/janreges/siteone-crawler-gui"&gt;https://github.com/janreges/siteone-crawler-gui&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Command-line Interface - &lt;a href="https://github.com/janreges/siteone-crawler"&gt;https://github.com/janreges/siteone-crawler&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Main features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For developers and QA engineers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No one is perfect&lt;/strong&gt; and I don't know of a single developer or company that, even across different levels of testing and checklists, runs a really perfect website. Websites are usually not about the optimized homepage, but a bunch of different pages. This makes it difficult to really check the entire website for SEO, security, performance, accessibility, semantics, content quality, etc. This tool &lt;strong&gt;will crawl every single page&lt;/strong&gt;, every URL contained anywhere in the content, &lt;strong&gt;including JS, CSS, images, fonts&lt;/strong&gt; or &lt;strong&gt;documents&lt;/strong&gt;. Depending on the type of content, it performs various analyses and reports imperfections.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Works well for development versions of websites on &lt;strong&gt;localhost&lt;/strong&gt; and &lt;strong&gt;specific ports&lt;/strong&gt;, or with &lt;strong&gt;HTTP proxy&lt;/strong&gt; or &lt;strong&gt;HTTP authentication&lt;/strong&gt; required.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It can also &lt;strong&gt;generate&lt;/strong&gt; a fully &lt;strong&gt;functional&lt;/strong&gt; (usually) and &lt;strong&gt;viewable offline static version of the website&lt;/strong&gt;, even when dynamic query parameters are used in the URL. However, the problem are some modern JS frameworks that use JS modules, and unfortunately these are disabled by CORS with local &lt;code&gt;file://&lt;/code&gt; protocol.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Can &lt;strong&gt;generate sitemap.xml&lt;/strong&gt; and &lt;strong&gt;sitemap.txt&lt;/strong&gt; with lists of all URLs of existing pages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It can also serve as a &lt;strong&gt;stress-test tool&lt;/strong&gt;, as it allows you to set the max number of parallel requests and the max number of requests per second. But please &lt;strong&gt;do not abuse the tool for DoS attacks&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It's really consistent in searching and crawling URLs - it pulls and downloads e.g. all images listed in &lt;code&gt;srcset&lt;/code&gt; attributes, in &lt;code&gt;CSS url()&lt;/code&gt;, even e.g. for &lt;strong&gt;NextJS&lt;/strong&gt; websites it detects &lt;code&gt;build-manifest&lt;/code&gt; and creates from it URLs to all JS-chunks, which it then downloads.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A list of analyses that the crawler performs and reports imperfections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;for each URL &lt;strong&gt;HTTP status code&lt;/strong&gt;, &lt;strong&gt;content type&lt;/strong&gt;, response &lt;strong&gt;time &lt;/strong&gt;and &lt;strong&gt;size&lt;/strong&gt;, &lt;strong&gt;title&lt;/strong&gt;, &lt;strong&gt;description&lt;/strong&gt;, &lt;strong&gt;DOM &lt;/strong&gt;elements count, etc.;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;checks inline SVGs and warns when there are &lt;strong&gt;large inline SVGs&lt;/strong&gt; in the HTML, or a lot of duplication, and it would be better to insert them as an extra &lt;code&gt;*.svg&lt;/code&gt; file that may be cached;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;checks the &lt;strong&gt;validity of the SVG&lt;/strong&gt; from an XML perspective (very often manual editing of SVGs will break the syntax and not all browsers can fix this with their autocorrect);&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;checks for &lt;strong&gt;missing quotes&lt;/strong&gt; in HTML text attributes (can cause problems if values are not escaped correctly);&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;checks the &lt;strong&gt;max depth of DOM elements&lt;/strong&gt; and warns if the depth exceeds threshold;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;checks the &lt;strong&gt;semantic structure of headings&lt;/strong&gt;, the existence of &lt;strong&gt;just one &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;&lt;/strong&gt;, warns about details;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;checks that &lt;strong&gt;phone numbers&lt;/strong&gt; contained in the HTML are correctly wrapped in a link with &lt;code&gt;href="tel:"&lt;/code&gt;, so that they can be clicked on to make a phone call;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;checks the &lt;strong&gt;uniqueness of titles&lt;/strong&gt; and &lt;strong&gt;meta descriptions&lt;/strong&gt; - it will alert you very quickly if you don't add the page number to the title, or the name of a filtered category, etc.;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;checks the use of modern &lt;strong&gt;Brotli compression&lt;/strong&gt; for the most efficient data transfer;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;checks the use of modern &lt;strong&gt;WebP&lt;/strong&gt; and &lt;strong&gt;AVIF&lt;/strong&gt; image formats;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;checks for &lt;strong&gt;accessibility&lt;/strong&gt; and that important HTML elements have &lt;strong&gt;aria&lt;/strong&gt; attributes, images have &lt;strong&gt;alt&lt;/strong&gt; attributes, etc.;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;checks &lt;strong&gt;HTTP headers&lt;/strong&gt; of all responses and warns about the absence of important &lt;strong&gt;security headers&lt;/strong&gt; and generates statistics of all HTTP headers and their unique values;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;checks &lt;strong&gt;cookie settings&lt;/strong&gt; and warns about missing &lt;code&gt;Secure&lt;/code&gt; flags on HTTPS, &lt;code&gt;HttpOnly&lt;/code&gt; or &lt;code&gt;SameSite&lt;/code&gt;;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;checks &lt;strong&gt;OpenGraph metadata&lt;/strong&gt; on all pages and displays their values in the report;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;checks and reports on all &lt;strong&gt;404 pages&lt;/strong&gt; including URLs where non-existent URLs are located (also monitors links to external domains);&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;checks and reports all &lt;strong&gt;301/302 redirects&lt;/strong&gt; including the URL where the redirected URL is located;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;checks and reports &lt;strong&gt;DNS settings&lt;/strong&gt; (IP address(es) to which the domain is resolved, including visualization of possible CNAME chain);&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;checks and reports &lt;strong&gt;SSL/TLS settings&lt;/strong&gt; - reports the validity of the certificate from-to, warns about support of unsafe SSL/TLS protocols, or recommends the use of newer ones;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;if enabled, &lt;strong&gt;downloads all linked assets from other domains&lt;/strong&gt; (JS, CSS, images, fonts, documents, etc.);&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;downloads &lt;strong&gt;robots.txt&lt;/strong&gt; on every domain it browses and respects the prohibition of crawling on pages forbidden in robots.txt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;shows &lt;strong&gt;all unique images&lt;/strong&gt; found on the website in the &lt;strong&gt;Image Gallery&lt;/strong&gt; report;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;shows statistics of the &lt;strong&gt;fastest&lt;/strong&gt; and &lt;strong&gt;slowest pages&lt;/strong&gt;, which are best to optimize, add cache, etc.;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;shows statistics on the number, size and speed of downloads of each &lt;strong&gt;content type &lt;/strong&gt;and then a larger &lt;strong&gt;breakdown by mime-type&lt;/strong&gt; (&lt;code&gt;Content-Type&lt;/code&gt; header);&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;shows statistics from which &lt;strong&gt;different and foreign domains&lt;/strong&gt; you are retrieving which type of content;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;shows a &lt;strong&gt;summary of all findings&lt;/strong&gt;, sorted &lt;strong&gt;by severity&lt;/strong&gt;;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;allows you to also set response HTTP headers to be included in the URL listing (in the console and HTML report) via the &lt;code&gt;--extra-columns&lt;/code&gt; setting - typically e.g. &lt;code&gt;X-Cache&lt;/code&gt;;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;has &lt;strong&gt;dozens of useful settings&lt;/strong&gt; that can be used to influence the behavior of crawling, parsing, caching, reporting, output, etc.;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;in the &lt;strong&gt;future &lt;/strong&gt;we want to implement a &lt;strong&gt;lot of other controls and analyses&lt;/strong&gt; that will make sense within the user community - &lt;strong&gt;the goal is to create a free tool that will be very useful and versatile.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For DevOps
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Especially for Linux users, the &lt;strong&gt;command-line&lt;/strong&gt; part of SiteOne crawler is very easy to use, &lt;strong&gt;without&lt;/strong&gt; having to install &lt;strong&gt;any dependencies&lt;/strong&gt;. Included is the runtime binary for x64/arm64 and the crawler source code. Just &lt;em&gt;git clone&lt;/em&gt;, or use the crawler in &lt;em&gt;tar.gz&lt;/em&gt; to where you need it. By default, crawler saves files in its 'tmp' folder, but any paths for caching or reports/exports can be set with a CLI switch. In the coming weeks we will also prepare public &lt;strong&gt;Docker images&lt;/strong&gt; for the possibility to use Crawler in &lt;strong&gt;CI/CD environments&lt;/strong&gt; with Docker or Kubernetes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Very useful is the possibility to have the whole website rebuilt during some pre-release phase in CI/CD. Using CLI switches you can have the resulting &lt;strong&gt;HTML report sent to one or more emails&lt;/strong&gt; via your SMTP server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Crawler allows you to configure the use of your &lt;strong&gt;HTTP proxy&lt;/strong&gt;, set up &lt;strong&gt;HTTP authentication&lt;/strong&gt; or crawl the website on a &lt;strong&gt;special port&lt;/strong&gt;, e.g. &lt;code&gt;http://localhost:3000&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;By setting the number of &lt;strong&gt;parallel workers&lt;/strong&gt; or &lt;strong&gt;max requests per second&lt;/strong&gt;, you can &lt;strong&gt;test your DoS protections&lt;/strong&gt;, or perform a &lt;strong&gt;stress test&lt;/strong&gt; to see how much load the target server(s) are producing what traffic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can use CLI switches to &lt;strong&gt;turn off support&lt;/strong&gt; for JS, CSS, images, fonts or documents, and you can use the crawler to immediately &lt;strong&gt;warm up the cache&lt;/strong&gt; after a new release, which usually includes flushing the cache of the previous version.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In addition to the HTML and TXT report (output as in the console), the crawler also generates &lt;strong&gt;output to a JSON&lt;/strong&gt; file, which then contains all the findings and data, in a structured and programmable form. So you can &lt;strong&gt;integrate the output&lt;/strong&gt; from the crawler further, according to your needs.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For website owners and consultants
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;General &lt;strong&gt;quality audit&lt;/strong&gt; of website processing - website owners should be aware of what reserves their website has and where improvements could be made. Some improvements are not trivial and can be quite costly to implement. Some, however, take tens of minutes to implement and their &lt;strong&gt;impact on output quality&lt;/strong&gt; can be high.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Audit &lt;strong&gt;on-page SEO factors&lt;/strong&gt; - checks all titles and descriptions as well as headings on all pages pointing out lack of &lt;strong&gt;uniqueness&lt;/strong&gt;, or &lt;strong&gt;missing &lt;h1&gt;&lt;/h1&gt;&lt;/strong&gt; headings or &lt;strong&gt;incorrect semantic structure&lt;/strong&gt;. Most of the findings can usually be corrected by the website owner themselves through the CMS.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Link Functionality Audit&lt;/strong&gt; - goes through every single link in the content on all pages and alerts you to broken links or unnecessary redirects (typically due to missing slashes at the end of the URL).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Audit &lt;strong&gt;various UX details&lt;/strong&gt;, such as whether all &lt;strong&gt;phone numbers&lt;/strong&gt; found in the HTML are wrapped in an active link with &lt;code&gt;href="tel:"&lt;/code&gt; so that a visitor can click on them and dial the call without having to rewrite or copy the number.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Overview of &lt;strong&gt;all images on the website&lt;/strong&gt; - the HTML output report contains a viewable gallery of absolutely all images found on the website. You may notice, for example, low-quality or unwanted images.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Overview of &lt;strong&gt;page generation speed&lt;/strong&gt; - the website owner should strive to have all pages on their website generate ideally in tens, max hundreds of milliseconds, as slow sites discourage visitors and are statistically proven to have lower conversions on slow sites. In fact, often only the homepage is measured, which is often optimized by the developers, but the other pages may be neglected from the perspective. If the website is slow and optimization would be expensive, it is often possible to move the website to a more powerful hosting with a slightly higher price. SiteOne Crawler &lt;strong&gt;stores all reports on your hard drive&lt;/strong&gt;, so you can then use it to measure and compare the website before/after optimizations or moving to faster hosting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can tell the Crawler what &lt;strong&gt;other domains it can also fully crawl&lt;/strong&gt; - typically subdomains or domain extensions with other language mutations, such as &lt;code&gt;*.mysite.tld&lt;/code&gt; or &lt;code&gt;*.mysite.*&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Crawler offers the possibility to have the entire website, including all images or documents, &lt;strong&gt;exported to the offline form&lt;/strong&gt;. The site is then fully, or almost fully functional in terms of browsing and crawling even from a &lt;strong&gt;local disk, without the need for the Internet&lt;/strong&gt;. Great functionality for easy &lt;strong&gt;archiving&lt;/strong&gt; of web content at any given time. It can also help in a situation where some institution requires you to keep an archive of your website on different days, for &lt;strong&gt;legal purposes&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Feedback is welcome&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We would be very happy if you &lt;strong&gt;try our tool&lt;/strong&gt; and &lt;strong&gt;give us your feedback&lt;/strong&gt;. Any &lt;strong&gt;ideas for improvement&lt;/strong&gt; are also very welcome. The tool is certainly &lt;strong&gt;not perfect today&lt;/strong&gt;, but our goal is to make it perfect in the coming months.&lt;/p&gt;

&lt;p&gt;And, of course, we will also be happy to &lt;strong&gt;share this article&lt;/strong&gt; or the website &lt;a href="https://crawler.siteone.io/?utm_source=dev.to&amp;amp;utm_campaign=article1-footer"&gt;&lt;strong&gt;crawler.siteone.io&lt;/strong&gt;&lt;/a&gt; with your colleagues or friends who the tool could help. On the homepage you will find sharing buttons.&lt;/p&gt;

&lt;p&gt;Thank you for your attention and we believe that our tool will &lt;strong&gt;help you in improving the quality of your website(s)&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Videos
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Desktop Application
&lt;/h3&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/rFW8LNEVNdw"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Command-line Interface
&lt;/h3&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/25T_yx13naA"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  HTML report
&lt;/h3&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/PHIFSOmk0gk"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>performance</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to build a CDN (2/3): server and reverse proxy configuration</title>
      <dc:creator>Ján Regeš</dc:creator>
      <pubDate>Sat, 08 Jan 2022 17:57:33 +0000</pubDate>
      <link>https://forem.com/janreges/how-to-build-a-cdn-23-server-and-reverse-proxy-configuration-16md</link>
      <guid>https://forem.com/janreges/how-to-build-a-cdn-23-server-and-reverse-proxy-configuration-16md</guid>
      <description>&lt;p&gt;In the previous article about &lt;a href="https://dev.to/janreges/how-to-build-a-cdn-1-3-introduction-and-basic-components-345o"&gt;basic CDN components&lt;/a&gt; we described what components you need to build a CDN, and today we will focus on the software configuration of the servers and the reverse proxy itself, which will cache the content to ensure that the data is always as close as possible to the end visitors.&lt;/p&gt;

&lt;p&gt;The primary goal of this article is not to give you specific values for each setting (although we will recommend some), but to tell you what to look for and what to watch out for. In fact, we also tune and optimize the specific values ourselves over time according to the traffic and the collected monitoring indications. It is therefore essential to understand the individual settings and adjust them with respect to your HW and expected traffic.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Operating system&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;At &lt;a href="https://www.siteone.cz/" rel="noopener noreferrer"&gt;SiteOne&lt;/a&gt; we have the vast majority of servers running on Linux — specifically Gentoo and Debian distributions. In the case of CDN, however, all our servers are running on Debian, so any detailed tips will include Debian paths/settings.&lt;/p&gt;

&lt;p&gt;In the area of OS and kernel, we recommend focusing on the following parameters, which will significantly affect how much traffic each server can handle without rejecting TCP connections or hitting other limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configure &lt;em&gt;/etc/security/limits.conf&lt;/em&gt; — set significantly higher soft and hard limits especially for &lt;strong&gt;nproc&lt;/strong&gt; and &lt;strong&gt;nofile&lt;/strong&gt; for the &lt;strong&gt;nginx&lt;/strong&gt; process (tens to hundreds of thousands).&lt;/li&gt;
&lt;li&gt;Ideally, configure the &lt;strong&gt;kernel&lt;/strong&gt; via &lt;strong&gt;sysctl.conf&lt;/strong&gt; and focus on the parameters you see in the recommended configuration below. It’s a good idea to study each parameter, understand how it affects your operation, and set it accordingly.&lt;/li&gt;
&lt;li&gt;If you have kernel 4.9+ you can enable the &lt;a href="https://atoonk.medium.com/tcp-bbr-exploring-tcp-congestion-control-84c9c11dc3a9" rel="noopener noreferrer"&gt;TCP BBR algorithm&lt;/a&gt; to reduce RTT and increase the speed of content delivery. Parameters: &lt;em&gt;net.ipv4.tcp_congestion_control=bbr, net.core.default_qdisc=fq&lt;/em&gt; (more info in the article at &lt;a href="https://blog.cloudflare.com/http-2-prioritization-with-nginx/" rel="noopener noreferrer"&gt;Cloudflare&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Check the &lt;strong&gt;RX-DRP&lt;/strong&gt; value with netstat -i, and if the value is already in the millions after a couple of days and still increasing, increase the RX/TX buffers on the netstat. To find the current setting and max value, use &lt;em&gt;ethtool -g YOUR-IFACE&lt;/em&gt; and set the new value with &lt;em&gt;ethtool -G&lt;/em&gt;, so for example &lt;em&gt;ethtool -G ens192 rx 2048 tx 2048&lt;/em&gt;. To make the setting survive a reboot, call the command in post-up scripts in &lt;em&gt;/etc/network/interfaces&lt;/em&gt; or &lt;em&gt;/etc/rc.local&lt;/em&gt;. If you are modifying the network interface that connects you to the server, be careful, because the change will reboot the interface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Txqueuelen&lt;/strong&gt; on network cards is recommended to be raised from the default 1000, depending on your connectivity and network card.&lt;/li&gt;
&lt;li&gt;Set the &lt;strong&gt;IO scheduler&lt;/strong&gt; on each disk/array depending on what storage you are using — &lt;em&gt;/sys/block/*/queue/scheduler&lt;/em&gt;. If you are using SSD or NVME, we recommend &lt;em&gt;none&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iptables&lt;/strong&gt; or &lt;strong&gt;router&lt;/strong&gt; — it is recommended to set some hard limits on the number of simultaneous connections from one IP address and the number of connections per certain time. In case of a DoS attack, you can filter out a large part of the traffic effectively already at the network level. However, you should also set limits with respect to possible visitors behind NAT (multiple legitimate visitors behind one IP address is a typical situation e.g. with mobile operators or smaller local ISPs).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When setting individual parameters, consider what the typical traffic of a visitor who retrieves content from the CDN looks like. HTTP/2 is essential, as it usually only takes one TCP connection for a visitor to download all the content on the page. You can afford shorter TCP connection timeouts, keepalives, smaller buffers. The metrics you collect, such as: the number of TCP connections in each state, will tell you a lot in real traffic. If you want to handle tens of thousands of visitors in seconds or minutes, forget about the default values of various timeouts in minutes and test values in units to tens of seconds.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Recommended kernel configuration&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;The values of each setting should be taken &lt;strong&gt;only as our recommendation&lt;/strong&gt;, which has been proven to work well for a server with 4–8 GB RAM, 4–8 vCPUs and Intel X540-AT2 or Intel I350 network cards. Some directives have values an order of magnitude higher or lower than the distributions default. These are usually modifications to increase the ability to handle heavy traffic efficiently and minimize the impact of a DoS or DDoS attack. It is also important to note that the configuration is for a server with IPv6 support disabled. If your situation allows it, use IPv6 too.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

fs.aio-max-nr = 524288  
fs.file-max = 611160  
kernel.msgmax = 131072  
kernel.msgmnb = 131072  
kernel. panic = 15  
kernel.pid_max = 65536  
kernel.printk = 4 4 1 7  
net.core.default_qdisc = fq  
net.core.netdev_max_backlog = 262144  
net.core.optmem_max = 16777216  
net.core.rmem_max = 16777216  
net.core.somaxconn = 65535  
net.core.wmem_max = 16777216  
net.ipv4.conf.all.accept_redirects = 0  
net.ipv4.conf.all.log_martians = 1  
net.ipv4.conf.all.rp_filter = 1  
net.ipv4.conf.all.secure_redirects = 0  
net.ipv4.conf.all.send_redirects = 0  
net.ipv4.conf.default.accept_redirects = 0  
net.ipv4.conf.default.accept_source_route = 0  
net.ipv4.conf.default.rp_filter = 1  
net.ipv4.conf.default.secure_redirects = 0  
net.ipv4.conf.default.send_redirects = 0  
net.ipv4.ip_forward = 0  
net.ipv4.ip_local_port_range = 1024 65535  
net.ipv4.tcp_congestion_control = bbr  
net.ipv4.tcp_fin_timeout = 10  
net.ipv4.tcp_keepalive_intvl = 10  
net.ipv4.tcp_keepalive_probes = 5  
net.ipv4.tcp_keepalive_time = 60  
net.ipv4.tcp_low_latency = 1  
net.ipv4.tcp_max_orphans = 10000  
net.ipv4.tcp_max_syn_backlog = 65000  
net.ipv4.tcp_max_tw_buckets = 1440000  
net.ipv4.tcp_moderate_rcvbuf = 1  
net.ipv4.tcp_no_metrics_save = 1  
net.ipv4.tcp_notsent_lowat = 16384  
net.ipv4.tcp_rfc1337 = 1  
net.ipv4.tcp_rmem = 4096 87380 16777216  
net.ipv4.tcp_sack = 0  
net.ipv4.tcp_slow_start_after_idle = 0  
net.ipv4.tcp_synack_retries = 2  
net.ipv4.tcp_syncookies = 1  
net.ipv4.tcp_syn_retries = 2  
net.ipv4.tcp_timestamps = 0  
net.ipv4.tcp_tw_reuse = 1  
net.ipv4.tcp_window_scaling = 0  
net.ipv4.tcp_wmem = 4096 65536 16777216  
net.ipv6.conf.all.disable_ipv6 = 1  
net.ipv6.conf.default.disable_ipv6 = 1  
net.ipv6.conf.lo.disable_ipv6 = 1  
vm.dirty_background_ratio = 2  
vm.dirty_ratio = 60  
vm.max_map_count = 262144  
vm.overcommit_memory = 1  
vm.swappiness = 1


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h1&gt;
  
  
  &lt;strong&gt;Reverse proxy and cache&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;On all PoP servers, you need a critical CDN component — a reverse proxy with robust caching support. Most popular are &lt;strong&gt;Varnish&lt;/strong&gt;, &lt;strong&gt;Squid&lt;/strong&gt;, &lt;strong&gt;Nginx&lt;/strong&gt;, &lt;strong&gt;Traefik&lt;/strong&gt;, &lt;strong&gt;H2O&lt;/strong&gt; and with limited functionality e.g. &lt;strong&gt;HAProxy&lt;/strong&gt;. &lt;strong&gt;Tengine&lt;/strong&gt; is also worth considering, built on Nginx and adding a lot of interesting functionality.&lt;/p&gt;

&lt;p&gt;In the context of a CDN, the functionality of the reverse proxy is quite clear — based on the URL and request headers, find the content in the cache and if it is not there, or has expired, download it from the Origin server and store it in the cache so that the next visitor’s request is processed faster, from the cache on the PoP.&lt;/p&gt;

&lt;p&gt;We finally chose &lt;a href="https://www.nginx.org/" rel="noopener noreferrer"&gt;Nginx web server&lt;/a&gt; because we have been using it successfully on most of our servers for many years. We have all the configurations and different vhost variants as well as optimal functional, performance and security settings in Ansible. As for the specific version, we recommend the latest &lt;strong&gt;1.19.x&lt;/strong&gt;, which already includes the improved HTTP/2 implementation, along with OpenSSL 1.1.1 due to TLSv1.3.&lt;/p&gt;

&lt;p&gt;Compared to our normal default values for application servers, we have significantly reduced various buffers, timeouts, and thresholds for CDNs, as well as for the kernel. Our CDN is optimized for static content and for handling only GET/HEAD/OPTIONS requests. Since we don’t have to support POST or uploads anymore, we could tighten the parameters significantly, both on the client side and on the backend (requests to source origin servers).&lt;/p&gt;

&lt;p&gt;The following text assumes that you already have at least basic experience with Nginx — that’s why there are no specific configuration snippets, but rather various recommendations beyond basic usage that you won’t usually find in Nginx tutorials and have a significant impact on CDN operation.&lt;/p&gt;

&lt;p&gt;Cache is a key functionality of a CDN, so we recommend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check out &lt;a href="https://www.nginx.com/blog/nginx-high-performance-caching/" rel="noopener noreferrer"&gt;the High-Performance Caching guide&lt;/a&gt;. For proxy cache, carefully study and understand all &lt;em&gt;proxy_cache*_&lt;/em&gt; directives and their parameters. Start with &lt;strong&gt;proxy_cache_path&lt;/strong&gt; and the &lt;em&gt;levels&lt;/em&gt;, &lt;em&gt;key_zone&lt;/em&gt;, _inactive_or &lt;em&gt;max_size&lt;/em&gt; attributes. For remote secondary PoPs, you can have &lt;em&gt;inactive for&lt;/em&gt; weeks or months, for example — the cache manager will also keep content that hasn’t been accessed for longer, thus increasing the accelerating effect of CDN and cache hit-ratio even for PoPs from which the content of specific URLs is not downloaded as often.&lt;/li&gt;
&lt;li&gt;Optimally set the &lt;strong&gt;proxy_cache_valid&lt;/strong&gt; directive, which affects how long the HTTP codes are cached. If you decide to cache error codes, e.g. &lt;em&gt;400 Bad Request&lt;/em&gt;, then only cache them for a very short period of time to minimize the effects of possible “cache poisoning”.&lt;/li&gt;
&lt;li&gt;If you don’t want an original to consider its “cache control” through response headers when caching, you can use &lt;strong&gt;proxy_ignore_headers&lt;/strong&gt; and ignore typically &lt;em&gt;Cache-Control&lt;/em&gt;, _Expires_ or &lt;em&gt;Vary&lt;/em&gt; headers.&lt;/li&gt;
&lt;li&gt;Also pay attention to the &lt;strong&gt;proxy_cache_use_stale,&lt;/strong&gt; which affects how the cache behaves if the origin is unavailable. We decided that if by chance the original is down and the cache has expired, we will return the original content to the visitor anyway. This will encourage high availability. Also set up &lt;em&gt;updating&lt;/em&gt; to load the visitor’s content immediately from the cache after expiration (without waiting for the original), but update the content immediately from the original in the background for future visitors. This eliminates the effect of occasional slowdowns, where once in a while a visitor “gets carried away” by the need to update the expired content of a given URL in the CDN.&lt;/li&gt;
&lt;li&gt;Decide what to set in the &lt;strong&gt;proxy_cache_key&lt;/strong&gt;. For example, do you want to include a possible query string in the cache key, which is often used to “version” files and suppress the cache of the original version of the file?&lt;/li&gt;
&lt;li&gt;Activate &lt;strong&gt;proxy_cache_lock&lt;/strong&gt; to keep the cache filling/keeping optimal even with high parallelization and decide how to set &lt;strong&gt;proxy_cache_min_uses&lt;/strong&gt; &lt;em&gt;.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In addition, consider the following tips and settings that affect Nginx performance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your platform allows it, set up &lt;strong&gt;use epool&lt;/strong&gt;. If you have kernel 4.5+, it will use &lt;a href="https://sudonull.com/post/14030-The-whole-truth-about-linux-epoll#epollexclusive" rel="noopener noreferrer"&gt;EPOLLEXCLUSIVE&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;listen&lt;/strong&gt; directivity of the main node of your CDN (&lt;em&gt;cdn.company.com&lt;/em&gt;) use &lt;strong&gt;reuseport&lt;/strong&gt;, so that requests to individual Nginx workers are distributed by the kernel, it is &lt;a href="https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/#Benchmarking-Performance-with-%3Ccode%3Ereuseport" rel="noopener noreferrer"&gt;many times more efficient&lt;/a&gt;. For the listen directive, study also the &lt;strong&gt;backlog&lt;/strong&gt; and &lt;strong&gt;fastopen&lt;/strong&gt; parameters. You can also activate &lt;strong&gt;deferred&lt;/strong&gt;, so that the request reaches Nginx only when the client actually receives the first data, which can better address some types of DDoS attacks.&lt;/li&gt;
&lt;li&gt;Activate &lt;strong&gt;http2&lt;/strong&gt; on the listen directive and always keep a secure set of &lt;strong&gt;ssl_ciphers&lt;/strong&gt; (with respect to the browser versions you want to support).&lt;/li&gt;
&lt;li&gt;If you can afford to do so given the browsers supported, only support &lt;strong&gt;TLSv1.2&lt;/strong&gt; and &lt;strong&gt;TLSv1.3&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The CDN server processor will be mostly loaded by gzip/brotli compression and SSL/TLS communication. Set &lt;strong&gt;ssl_session_cache&lt;/strong&gt; to minimize SSL/TLS handshakes. We recommend &lt;em&gt;shared&lt;/em&gt; so that the cache is shared between all workers. For example, a cache size of 50 MB, which will fit about 200,000 sessions in the cache. To minimize the number of SSL/TLS handshakes, you can increase the &lt;strong&gt;ssl_session_timeout&lt;/strong&gt;. If you don’t want to use SSL cache on the server, enable &lt;strong&gt;ssl_session_tickets&lt;/strong&gt; to keep the session cache active at least in the browser.&lt;/li&gt;
&lt;li&gt;For SSL settings, activate &lt;a href="https://blog.cloudflare.com/introducing-0-rtt/" rel="noopener noreferrer"&gt;0-RTT on TLSv1.3&lt;/a&gt; (&lt;em&gt;ssl_early_data on&lt;/em&gt;) to substantially reduce latency, but understand and consider &lt;a href="https://tools.ietf.org/html/rfc8470" rel="noopener noreferrer"&gt;Replay attack&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;If you want to achieve minimal TTBF (at the expense of higher load when transferring large files), study and set reasonably low &lt;strong&gt;ssl_buffer_size&lt;/strong&gt; and &lt;strong&gt;http2_chunk_size&lt;/strong&gt;. Alternatively, deploy the Cloudflare patch to Nginx, which supports dynamic settings — just google the &lt;strong&gt;ssl_dyn_rec_size_lo&lt;/strong&gt; directive.&lt;/li&gt;
&lt;li&gt;Also focus on understanding and setting up &lt;strong&gt;KeepAlive&lt;/strong&gt; both on the client side and in the upstreams — this will help streamline communication with the origin servers. KeepAlive HTTP/2 is governed by the &lt;strong&gt;http2_idle_timeout&lt;/strong&gt; directive (default: 3min), also look at &lt;strong&gt;http2_recv_timeout&lt;/strong&gt;. Keeping connections open unnecessarily long significantly reduces the number of visitors you are then able to serve. It also affects how large a DDoS attack you are then able to withstand. It’s good to have an understanding of how connection-tracking works (both on Linux and possibly on routers when the server is behind NAT), how it relates to the limit_conn setting, and how it behaves as a whole if you have hundreds of thousands of clients accessing your servers or are under a DDoS attack on L7.&lt;/li&gt;
&lt;li&gt;If you need to detect a change in the IP address of the original and you don’t have a paid &lt;em&gt;Nginx Plus&lt;/em&gt; with the &lt;em&gt;resolve&lt;/em&gt; attribute on the upstream server, you can just use &lt;code&gt;proxy_pass: https://www.myorigin.com;&lt;/code&gt; instead of defining an upstream. In this mode, &lt;em&gt;proxy_pass monitors&lt;/em&gt; the TTL in the domain DNS and updates the IP address(es) if necessary.&lt;/li&gt;
&lt;li&gt;Also study the &lt;strong&gt;lingering_close&lt;/strong&gt;, &lt;strong&gt;lingering_time&lt;/strong&gt;, and &lt;strong&gt;lingering_timeout&lt;/strong&gt; directives, which determine how quickly inactive connections should be closed. For better resistance to attacks, it makes sense to reduce the default times. For HTTP/2 connections, however, lingering_* directives have only been applied since Nginx 1.19.1.&lt;/li&gt;
&lt;li&gt;Increase &lt;strong&gt;ULIMIT&lt;/strong&gt; in &lt;em&gt;/etc/default/nginx&lt;/em&gt; and also set a higher &lt;strong&gt;LimitNOFILE&lt;/strong&gt; in &lt;em&gt;/etc/systemd/system/nginx.service.d/nginx.conf&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The sendfile&lt;/strong&gt;, &lt;strong&gt;tcp_nopush&lt;/strong&gt; and &lt;strong&gt;tcp_nodelay&lt;/strong&gt; also help to handle files and requests quickly. To prevent clients with fast connections downloading large files from using up the entire worker process, set &lt;strong&gt;sendfile_max_chunk&lt;/strong&gt; sensibly as well.&lt;/li&gt;
&lt;li&gt;If you are handling very large files and are seeing slowdowns in other requests, consider using &lt;strong&gt;aio&lt;/strong&gt;. Be sure to set the &lt;strong&gt;directio&lt;/strong&gt; directive appropriately, which defines the max size of the file that will still be sent via sendfile and larger ones via aio. We find 4MB to be the optimal value, so all JS/CSS/fonts and most images are handled through the sendfile and usually from the FS cache, so no IO does this either.&lt;/li&gt;
&lt;li&gt;Also look at the directives around &lt;strong&gt;open_file_cache&lt;/strong&gt;. With optimal settings and enough RAM you will have almost zero IOPS, even if you are clearing hundreds of Mbps.&lt;/li&gt;
&lt;li&gt;To handle high numbers of concurrent visitors and protect yourself from attacks, reduce &lt;strong&gt;client_max_body_size&lt;/strong&gt;, &lt;strong&gt;client_header_timeout&lt;/strong&gt;, &lt;strong&gt;client_body_timeout&lt;/strong&gt;, and &lt;strong&gt;send_timeout&lt;/strong&gt; as a matter of principle.&lt;/li&gt;
&lt;li&gt;For access log settings, study the &lt;strong&gt;buffer&lt;/strong&gt; and &lt;strong&gt;flush&lt;/strong&gt; parameters to minimize the IOPS associated with writing logs. Beware that this will also cause the logs to not be written 100% chronologically. Access logs should ideally be stored on a different disk than the cached data.&lt;/li&gt;
&lt;li&gt;For upstreams, you can play with load balancing (if the original can be accessed via multiple IP addresses) and &lt;strong&gt;backup weighting&lt;/strong&gt; attributes. In the current version, the useful &lt;strong&gt;max_conns&lt;/strong&gt; attribute, which was for a long time only in the paid version, is now freely available.&lt;/li&gt;
&lt;li&gt;If you also want to have some form of &lt;strong&gt;auto-retry&lt;/strong&gt; logic (for case of short unavailability of the origin), you can solve it for example by using multiple upstream-servers to the same original, but in between them put a vhost with short Lua code that will provide sleep between retry requests.&lt;/li&gt;
&lt;li&gt;Use a custom &lt;strong&gt;resolver&lt;/strong&gt; setup and consider using the local &lt;em&gt;dnsmasq&lt;/em&gt; as the primary resolver.&lt;/li&gt;
&lt;li&gt;Learn how the Cache Manager works in Nginx, which starts working especially when the cache gets full.&lt;/li&gt;
&lt;li&gt;Not everything can be mentioned here, but other attributes have an impact on proxy and cache behavior, which we recommend to study and set as well: &lt;em&gt;proxy_buffering, proxy_buffer_size, proxy_buffers, proxy_read_timeout, output_buffers, reset_timedout_connection&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;If you will be using dynamic modules with Nginx (in our case for brotli compression and WAF), with every Nginx upgrade you have to &lt;strong&gt;recompile all modules&lt;/strong&gt; against the new Nginx version. If you don’t do this, Nginx won’t boot after the upgrade due to &lt;em&gt;signature&lt;/em&gt; conflicts with *.so modules. It is therefore better to automate the whole process of upgrading Nginx, because you will end up with a broken Nginx when you upgrade e.g. apt. Part of this automation should include using the option to do &lt;a href="https://www.nginx.com/resources/wiki/start/topics/tutorials/commandline/#upgrading-to-a-new-binary-on-the-fly" rel="noopener noreferrer"&gt;Nginx upgrade on-the-fly&lt;/a&gt; where Nginx continues to run the old instance (from memory) and at the same time runs (or at least tries to) the new instance from the current binary and modules. This will ensure that you don’t lose a single request during the upgrade, even if the new Nginx doesn’t run after the upgrade for some reason. This whole process is in most distributions in init scripts under the &lt;em&gt;upgrade&lt;/em&gt; action, i.e. &lt;em&gt;service nginx upgrade&lt;/em&gt;. To prevent unwanted Nginx upgrades when upgrading packages globally, use &lt;em&gt;apt-mark hold/unhold nginx&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Depending on what content and behavior of the originals you want to support, you will need to study and possibly debug the behavior of the CDN cache with respect to the &lt;em&gt;Cache-Control&lt;/em&gt; header or, perhaps quite fundamentally, the &lt;em&gt;Vary&lt;/em&gt; header. For example, if the origin says in the response &lt;em&gt;Vary: User-Agent&lt;/em&gt;, the cache key should include the user-agent of the client, otherwise it can easily happen that you return cached HTML for the mobile version to someone on the desktop. But that depends on what scenarios and content types you want/do not want to support. Supporting these scenarios often means a lot of work, and it also reduces the efficiency of the cache. Usually you won’t be able to get by with native Nginx directives and will have to handle some scenarios with Lua scripts.&lt;/p&gt;

&lt;p&gt;Finally, I’ll mention that in the case of Nginx you also have a paid version &lt;a href="https://www.nginx.com/products/nginx/" rel="noopener noreferrer"&gt;Nginx Plus&lt;/a&gt; which offers various useful functionalities, a live dashboard and extra modules. Important is for example the &lt;em&gt;resolve&lt;/em&gt; directive of the upstream server, which in conjunction with the &lt;em&gt;resolver&lt;/em&gt; directive can detect a change in the IP address of the origin. However, the cost per instance is in the thousands of dollars per year, so its use would only make sense for a large commercial solution. If you don’t have thousands of dollars and would still like to have a realtime view of Nginx traffic, we recommend buying the $49 &lt;a href="https://luameter.com/" rel="noopener noreferrer"&gt;Luameter&lt;/a&gt; (&lt;a href="https://luameter.com/demo" rel="noopener noreferrer"&gt;demo&lt;/a&gt;). It works well, but if you’ll be handling hundreds of requests per second and a lot of unique URLs, expect increased load and RAM requirements. We have it disabled by default and only activate it when debugging.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Sample Nginx configuration&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Below we have prepared a sample average basic configuration of Nginx, which in this model example does not do a reverse proxy in front of the whole domain, but provides a CDN endpoint &lt;code&gt;https://cdn.company.com/myorigin.com/*.(css|js|jpg|jpeg|png|gif|ico)&lt;/code&gt; that retrieves content from the origin &lt;code&gt;https://www.myorigin.com/*&lt;/code&gt;. Averaged because we further modify some directives due to the HW of individual PoP servers, and it also doesn’t include some additional security mechanisms that we don’t want to expose. On the servers this configuration is of course split into separate configuration files, which in our case we generate via Ansible.&lt;/p&gt;

&lt;p&gt;The settings are especially different at the definition level for individual locations/origins, because you may want differently composed cache-keys, cache validity, limits, ignore cookies, have/not WebP or AVIF support, referer validation, active CORS-related settings, or maybe use a slice module, where you have to cache the 206 code and the cache key must also contain &lt;em&gt;$slice_range&lt;/em&gt;. Similarly, for some origins you may want to ignore &lt;em&gt;Cache-Control&lt;/em&gt; headers entirely and cache everything at a fixed time, or other per-origin specialties.&lt;/p&gt;

&lt;p&gt;The configuration also contains various per-origin directories or files — these must of course be set up by your automation, which you are using to introduce the new origin into your CDN. &lt;strong&gt;So really just take this as a guide on how to grab and set up the various functionalities.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

worker_processes 4;
worker_rlimit_nofile 100000;
pcre_jit on;

events {
  use epoll;
  worker_connections 16000;
  multi_accept on;
}

http {

  # IP whitelist to which no conn/rate restrictions should be applied
  geo $ip_whitelist {
    default        0;
    127.0.0.1      1;
    10.225.1.0/24  1;
  }
  map $ip_whitelist $limited_ip {
    0  $binary_remote_addr;
    1  "";
  }

  limit_conn_zone $limited_ip zone=connsPerIP:20m;
  limit_conn connsPerIP 30;
  limit_conn_status 429;

  limit_req_zone $limited_ip zone=reqsPerMinutePerIP:50m rate=500r/m;
  limit_req zone=reqsPerMinutePerIP burst=700 nodelay;
  limit_req_status 429;

  client_max_body_size 64k;
  client_header_timeout 10s;
  client_body_timeout 10s;
  client_body_buffer_size 16k;
  client_header_buffer_size 4k;

  send_timeout 10s;
  connection_pool_size 512;
  large_client_header_buffers 8 16k;
  request_pool_size 4k;

  http2_idle_timeout 60s;
  http2_recv_timeout 10s;
  http2_chunk_size 16k;

  server_tokens off;
  more_set_headers "Server: My-CDN";

  include /etc/nginx/mime.types;
  variables_hash_bucket_size 128;
  map_hash_bucket_size 256;

  gzip on;
  gzip_static on; # searches for the *.gz file and returns it directly from disk (compression is provided by our extra process in the background)
  gzip_disable "msie6";
  gzip_min_length 4096;
  gzip_buffers 16 64k;
  gzip_vary on;
  gzip_proxied any;
  gzip_types image/svg+xml text/plain text/css application/json application/x-javascript application/javascript text/xml application/xml application/xml+rss text/javascript text/x-component font/truetype font/opentype image/x-icon;
  gzip_comp_level 4;

  brotli on;
  brotli_static on; # searches for the *.br file and returns it directly from the disk (compression is provided by our extra process in the background)
  brotli_types text/plain text/css application/javascript application/json image/svg+xml application/xml+rss;
  brotli_comp_level 6;

  output_buffers 1 32k;
  postpone_output 1460;

  sendfile on;
  sendfile_max_chunk 1m;
  tcp_nopush on;
  tcp_nodelay on;

  keepalive_timeout 10 10;
  ignore_invalid_headers on;
  reset_timedout_connection on;

  open_file_cache          max=50000 inactive=30s;
  open_file_cache_valid    10s;
  open_file_cache_min_uses 2;
  open_file_cache_errors   on;

  proxy_buffering           on;
  proxy_buffer_size         16k;
  proxy_buffers             64 16k;
  proxy_temp_path           /var/lib/nginx/proxy;
  proxy_cache_min_uses      2;

  proxy_ignore_client_abort on;
  proxy_intercept_errors    on;
  proxy_next_upstream       error timeout invalid_header http_500 http_502 http_503 http_504;
  proxy_redirect            off;
  proxy_connect_timeout     60;
  proxy_send_timeout        180;
  proxy_cache_lock          on;
  proxy_read_timeout        10s;

  # setting up trusted IP subnets to respect X-Forwarded-For header (for multi-level proxy setup)
  set_real_ip_from          127.0.0.1/32;
  set_real_ip_from          10.1.2.0/24;
  real_ip_header            X-Forwarded-For;
  real_ip_recursive         on;

  ############################################################################
  ## Example configuration for:                                             ##
  ## https://cdn.mycompany.com/myorigin.com/* -&amp;gt; https://www.myorigin.com/* ##
  ############################################################################

  upstream up_www_myorigin_com {
    server www.myorigin.com:443 max_conns=50;

    keepalive 20;
    keepalive_requests 50;
    keepalive_timeout 5s;
  }

  proxy_cache_path /var/lib/nginx/tmp/proxy/www.myorigin.com levels=1:2 keys_zone=cache_www_myorigin_com:20m inactive=720h max_size=10g;

  server {

    server_name cdn.company.com;

    listen lan-ip:443 ssl default_server http2 reuseport deferred backlog=32768;
    ssl_prefer_server_ciphers on;
    ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
    ssl_certificate /etc/nginx/ssl/cdn.company.com.nginx-bundle.crt;
    ssl_certificate_key /etc/nginx/ssl/cdn.company.com.key;
    ssl_session_cache shared:SSL_cdn_company_com:50m;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_dhparam /etc/ssl/webserver_dhparams.pem;
    ssl_early_data on;

    lingering_close on;
    lingering_time 10s;
    lingering_timeout 5s;

    resolver 127.0.0.1; # dnsmasq with logging to get an idea of the DNS traffic that Nginx is doing

    ...

    location ~* ^/myorigin\.com/(.+\.(css|js|jpg|jpeg|png|gif|ico))$ {
      set $origin_uri "/$1$is_args$args";
      root /var/www/myorigin.com;
      access_log  /var/log/nginx/www.myorigin.com/ssl.access.log main buffer=4k flush=5m;
      error_log   /var/log/nginx/www.myorigin.com/ssl.error.log notice;

      if ($request_method !~ ^(GET|HEAD|OPTIONS)$ ) {
        more_set_headers "Content-Type: application/json";
        return 405 '{"code": 405, "message": "Method Not Allowed"}';
      }

      more_clear_headers "Strict-Transport-Security";
      more_set_headers "Strict-Transport-Security: max-age=31536000";
      more_set_headers "X-Content-Type-Options: nosniff";
      more_set_headers 'Link: &amp;lt;https://www.myorigin.com$origin_uri&amp;gt;; rel="canonical"';

      expires 1y; # enforce caching in browsers for 1 year (use only consciously, if you are sure that when you change the content of the file on the original, the URL will also change)

      modsecurity on;
      modsecurity_rules_file /etc/nginx/modsecurity/myorigin.com.conf;

      # for requests that fall under CORS (e.g. fonts) we allow to load content only from selected domains
      set $headerCorsAllowOrigin "";
      if ($http_origin ~ '^https?://(localhost|cdn\.company\.com|www\.myorigin\.com)') {
          set $headerCorsAllowOrigin "$http_origin";
      }
      if ($request_method = 'OPTIONS') {
          more_set_headers "Access-Control-Allow-Origin: $headerCorsAllowOrigin";
          more_set_headers "Access-Control-Allow-Methods: GET, HEAD, OPTIONS";
          more_set_headers "Access-Control-Max-Age: 3600";
          more_set_headers "Content-Length: 0";
          return 204;
      }

      # we allow to load content only from the original domain (e.g. it prevents displaying our images on foreign domains)
      valid_referers none blocked server_names *.myorigin.com;
      if ($invalid_referer) {
          more_set_headers "Content-Type: application/json";
          return 403 '{"code": 403, "message": "Forbidden Resource - invalid referer"}';
      }

      set $webp "";
      set $file_for_webp "";
      if ($http_accept ~* webp) {
          set $webp "A";
      }
      if ($request_filename ~ (.+\.(png|jpe?g))$) {
          set $file_for_webp $1;
      }
      if (-f $file_for_webp.webp) {
          set $webp "${webp}E";
      }
      if ($webp = AE) {
          rewrite ^/(.+)$ /webp/$1 last;
      }

      proxy_cache cache_www_myorigin_com;
      proxy_cache_key "$request_uri"; # we don't need a schema or a host, because we store in per-origin cache and support only HTTPS
      proxy_cache_use_stale error timeout invalid_header updating http_429 http_500 http_502 http_503 http_504;
      proxy_read_timeout 20s;
      proxy_cache_valid 200              720h;
      proxy_cache_valid 301              4h;
      proxy_cache_valid 302              1h;
      proxy_cache_valid 400 401 403 404  30s;
      proxy_cache_valid 500 501 502 503  30s;
      proxy_cache_valid 429              10s;


      # due to keep-alive on origins
      proxy_http_version 1.1;
      proxy_set_header Connection "";

      proxy_set_header "Via" "My-CDN";
      proxy_set_header "Early-Data" $ssl_early_data; # for the ability to detect Replay attack on the application level
      proxy_set_header Accept-Encoding ""; # we always want to receive and cache RAW content from the origin, because we have a process for preparing static *.gz and *.br versions

      proxy_set_header        Host                    www.myorigin.com;
      proxy_set_header        X-Forwarded-For         $remote_addr;
      proxy_set_header        X-Forwarded-Host        $host:$server_port;
      proxy_set_header        X-Forwarded-Server      $host;
      proxy_set_header        X-Forwarded-Proto       $scheme;

      if (-f $request_filename) {
          more_set_headers "X-Cache: HIT";
      }

      if (!-f $request_filename) {
          proxy_pass https://up_www_myorigin_com$origin_uri;
      }

    }

    # internal location for webp
    location ~* ^/webp(/myorigin\.com/(.*))$ {
      internal;
      root /var/www/myorigin.com;
      set $origin_uri "/$1$is_args$args";
      access_log /var/log/nginx/www.myorigin.com/ssl.access.webp.log main buffer=4k flush=5m;
      expires 366d;
      more_set_headers 'Link: &amp;lt;https://www.myorigin.com$origin_uri&amp;gt;; rel="canonical"';
      more_clear_headers 'Vary';
      more_set_headers "Vary: Accept";
      more_set_headers "X-Cache: HIT";
      try_files $1.webp $1 =404;
    }

  }

}


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h1&gt;
  
  
  &lt;strong&gt;Static compression as an essential helper&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;We did a random test of two commercial CDNs that have servers in Prague and neither provider is obviously using this great functionality/option. The commercial CDNs have to compress content using brotli or gzip on every request, which drastically drains their CPU and increases the response time several times, but the visitor pays for it.&lt;/p&gt;

&lt;p&gt;We tested how long it takes our CDN and a commercial CDN to transfer eight javascript files (from 1 to 500 kB) in HTTP/2 stream — our CDN did it in 45 ms, commercial CDN in 170 to 200 ms. Moreover, even when using brotli compression, the files were 14% larger because we use the maximum compression level. We tested normally in Chrome and we got 1 ms latency to both CDNs because we and their PoPs are in Prague.&lt;/p&gt;

&lt;p&gt;So how to solve the compression? In Nginx, you can enable static compression for both gzip and brotli (&lt;strong&gt;gzip_static on&lt;/strong&gt;; &lt;strong&gt;brotli_static on;&lt;/strong&gt; ). This, if understood and implemented correctly, can &lt;strong&gt;reduce the CPU load&lt;/strong&gt; quite substantially and at the same time &lt;strong&gt;speed up the visitor’s loading time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The way it works is that when static compression is active and the browser requests e.g. /js/file.js, Nginx looks at the disk to see if there is already a pre-compressed file /js/file.js.gz or /js/file.js. br. If such a file exists, it will send it straight away (without bothering the CPU with compression). The type of compression the browser supports is sent in the &lt;em&gt;Accept-Encoding&lt;/em&gt; header (&lt;em&gt;br&lt;/em&gt; takes precedence over &lt;em&gt;gzip&lt;/em&gt; if the browser supports it).&lt;/p&gt;

&lt;p&gt;Nginx &lt;strong&gt;does not create&lt;/strong&gt; &lt;em&gt;.br&lt;/em&gt; or &lt;em&gt;.gz&lt;/em&gt; files for you. Nor does it try to download these files from the originals. Frontend builds often create these &lt;em&gt;*.br&lt;/em&gt; or &lt;em&gt;*.gz&lt;/em&gt; files for their JS/CSS as part of the build, but they are simply not used here. You have to provide this yourself with your CDN. We’ve made a background process that continuously parses access logs and extracts “200 OK” requests for text files that don’t have their &lt;em&gt;*.br&lt;/em&gt; or &lt;em&gt;*. gz&lt;/em&gt; yet.&lt;/p&gt;

&lt;p&gt;Because this is a background process, you can afford to choose the &lt;strong&gt;highest&lt;/strong&gt;, &lt;strong&gt;most efficient&lt;/strong&gt;, but therefore &lt;strong&gt;slowest compression level&lt;/strong&gt; for compression. You’ll put a bit of strain on the CPU for once, but the reward will be an additional 5–15% lower transfer rate. In addition, the decompression speed in browsers is minimally affected (you can find benchmarks for this). Don’t forget to figure out how you will clean up the already expired &lt;em&gt;*.br&lt;/em&gt; or &lt;em&gt;*.gz after they&lt;/em&gt; expire. Also, how and if at all you will handle the situation when the query string contains e.g. &lt;em&gt;?v=1.0.5&lt;/em&gt; to force the download of a new version of the file.&lt;/p&gt;

&lt;p&gt;However you implement static compression, ensure that your files behave atomically during compression. In other words, store the final &lt;em&gt;*.br&lt;/em&gt; or &lt;em&gt;*.gz&lt;/em&gt; file next to it first, and only when the file is finally done, rename it to the destination location where Nginx expects it. You won’t have someone download a non-valid (only partial) file if a visitor hits the moment you compress.&lt;/p&gt;

&lt;p&gt;Since we usually cache content in the browser for months, such a visitor would have downloaded e.g. broken JS/CSS until the cache is cleared, which is very annoying. We all know how unprofessional it is when developers tell a client to clear their browser cache.&lt;/p&gt;

&lt;p&gt;Hint: If you don’t have a background process that will handle static compression for you, you should leave static compression disabled. This is because you will unnecessarily increase your IOPS when Nginx will look for &lt;em&gt;*.gz&lt;/em&gt; or &lt;em&gt;*.br&lt;/em&gt; variants.&lt;/p&gt;
&lt;h1&gt;
  
  
  &lt;strong&gt;JPG/PNG to WebP/AVIF conversion&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;If you want to reduce image bitrates by 30% to 90% (depending on how much the source images are already optimized), you can arrange for smart image conversion to modern WebP or AVIF format.&lt;/p&gt;

&lt;p&gt;Be careful about the AVIF format though — while it is fully supported and well-functioning in Google Chrome, support in Firefox is still experimental and there it still exhibits various bugs described in &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1443863" rel="noopener noreferrer"&gt;this ticket&lt;/a&gt;, which will manifest themselves e.g. in not displaying some images. However, this experimental support is disabled by default, so Firefox does not send the &lt;em&gt;image/avif&lt;/em&gt; for the &lt;em&gt;Accept&lt;/em&gt; request header.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For inspiration, this is how we implemented WebP/AVIF support:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The background process analyzes the access logs and searches for the most frequently retrieved images with a defined minimum data size.&lt;/li&gt;
&lt;li&gt;Using converters &lt;a href="https://developers.google.com/speed/webp/docs/cwebp" rel="noopener noreferrer"&gt;&lt;strong&gt;cwebp&lt;/strong&gt;&lt;/a&gt; a &lt;a href="https://github.com/kornelski/cavif-rs" rel="noopener noreferrer"&gt;&lt;strong&gt;cavif&lt;/strong&gt;&lt;/a&gt; convert the source image, e.g. /images/source.jpg, to /images/source.jpg.webp (atomically, as in static compression).&lt;/li&gt;
&lt;li&gt;In Nginx we have logic that when &lt;em&gt;image/avif&lt;/em&gt; or &lt;em&gt;image/webp&lt;/em&gt; occurs in the &lt;em&gt;Accept&lt;/em&gt; header of the request, it tries to send the requested file with the extension &lt;em&gt;. avif&lt;/em&gt; or &lt;em&gt;. webp&lt;/em&gt;, if it exists on the disk. The solution can be based on a combination of &lt;strong&gt;maps&lt;/strong&gt; and &lt;strong&gt;try_files&lt;/strong&gt; or composing the contents of a variable and IFs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If we have a real need for this, we may eventually centralise the process. That is, this process will not be done by each server separately, but will be managed by some central system that can select suitable images for optimization from the central logs, keeping statistics of real data savings by transfers, etc. This brings a certain degree of flexibility and the possibility to perform some operations in bulk. However, on the other hand, we like that the decentralization of these processes and the maximum autonomy of the individual PoPs minimizes the risk that some bug will reach the whole CDN. Another advantage is that each PoP optimizes its most loaded content according to the visitors there.&lt;/p&gt;
&lt;h1&gt;
  
  
  &lt;strong&gt;Search engines&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;It’s important to note that if you deploy a CDN and suddenly HTML images are loaded from another domain (unless you happen to use the CDN as a proxy for the entire site/domain), search engines will not index them as belonging to your domain, but to the CDN domain. Of course, you don’t want that.&lt;/p&gt;

&lt;p&gt;The solution is to provide canonicalization in Nginx using the HTTP &lt;strong&gt;Link&lt;/strong&gt; header, which tells the search engine where the actual source (origin) is. This way it will not index the image under the CDN domain, but under the source domain specified in the Link header. For optimal image indexing, we recommend that you also generate &lt;a href="https://support.google.com/webmasters/answer/178636" rel="noopener noreferrer"&gt;sitemap for images&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Example: the URL &lt;code&gt;https://cdn.company.com/myorigin.com/image.jpg&lt;/code&gt; should return the HTTP header:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

Link: https://www.myorigin.com/image.jpg; rel="canonical"


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h1&gt;
  
  
  &lt;strong&gt;Using CDN in projects&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;The primary and preferred way of using our CDN is very simple and is also evident from the sample Nginx configuration.&lt;/p&gt;

&lt;p&gt;If we want to deploy a CDN for content e.g. on &lt;code&gt;www.myorigin.com&lt;/code&gt; the web developers just need to ensure that instead of &lt;code&gt;/js/script.js&lt;/code&gt;, for example, this file is addressed as &lt;code&gt;https://cdn.company.com/myorigin.com/js/scripts.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The base URL is our GeoCDN domain, followed by the domain of the original (without the “www”) and ending with the path to the file on the original.&lt;/p&gt;

&lt;p&gt;The CDN administrators control which origin domains our CDN supports through Ansible. In Ansible, administrators can also set some specific behavior for each origin. In addition, for each origin it is possible to specify what type of content is supported, restrict URL shapes, define custom WAF rules, etc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip&lt;/strong&gt;: if you want to deploy a CDN to your site without requiring a single intervention in the application code and you are using Nginx, you can very easily help yourself with the native Nginx &lt;a href="https://nginx.org/en/docs/http/ngx_http_sub_module.html" rel="noopener noreferrer"&gt;sub module&lt;/a&gt;. This allows you to easily replace the paths to selected files so that they are addressed from the CDN (typically in HTML or CSS).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

sub_filter '&amp;lt;link href='/' '&amp;lt;link href='https://cdn.company.com/myorigin.com/';
sub_filter '&amp;lt;script src="/' '&amp;lt;script src="https://cdn.company.com/myorigin.com/";
sub_filter '&amp;lt;img src="/' '&amp;lt;img src="https://cdn.company.com/myorigin.com/";

sub_filter_types 'text/css' 'application/json' 'application/javascript'; # text/html is included automatically, but we also want to replace content in JSON API or CSS styles and JavaScripts
sub_filter_once off; # we want to replace all found occurrences


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The example shows that it requires href/src as the first attribute of the HTML tag. Unfortunately, regular expressions are not supported by sub_filter. If this is not sufficient for you, you can solve this substitution in the application code. You’re probably using a templating system that usually forces you to use some form of base-path variable, so this should be a piece of cake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note 1&lt;/strong&gt;: for content substitution to work, you must also set &lt;em&gt;proxy_set_header Accept-Encoding “”;&lt;/em&gt; , so that the original text content is uncompressed and strings can be substituted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note 2&lt;/strong&gt;: since the CDN is not deployed as a reverse proxy for the entire origin domain, the content loads faster in the browser. This is because the browser allows for more parallelization (HTML and assets are loaded from different IP addresses), so the resulting page build and render time is shorter. In reverse proxy mode, HTTP/2 multiplexing and prioritization helps a lot before full origin, but when the browser can load content from multiple different IP addresses, it is still a bit more efficient.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Security, protection against DoS/DDoS attacks and monitoring&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;With the help of the previous article on &lt;a href="https://dev.to/janreges/how-to-build-a-cdn-1-3-introduction-and-basic-components-345o"&gt;CDN components&lt;/a&gt; and this article, you should be able to get your CDN up and running with all the basic functionality.&lt;/p&gt;

&lt;p&gt;I hope that this article has helped you and that someone may have found some ideas or settings that will help them to improve their web or application server.&lt;/p&gt;

&lt;p&gt;If anyone has additional tips when looking at the proposed settings, or if they see any threats in our configuration, &lt;strong&gt;we would be happy to share them in the discussion&lt;/strong&gt;. We’ve been tweaking the settings ourselves for years, reflecting the different needs and attacks we’ve had on our projects, so it’s an ongoing and never-ending process. Additionally, simulating real traffic to verify the effect of some settings is very difficult, so every lived experience is welcomed and we will be grateful for sharing.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/janreges/how-to-build-a-cdn-33-security-monitoring-and-practical-tips-11e0"&gt;next and last article&lt;/a&gt; of the &lt;em&gt;How to build a CDN&lt;/em&gt; series, we will focus on various operational aspects of CDN operation — &lt;a href="https://dev.to/janreges/how-to-build-a-cdn-33-security-monitoring-and-practical-tips-11e0"&gt;how to protect the origins, how to defend against DoS/DDoS attacks&lt;/a&gt; and how to have the whole CDN operation under control.&lt;/p&gt;

&lt;p&gt;Thanks for reading, and if you like the article, I will be happy if you share it or leave a comment.&lt;/p&gt;

&lt;p&gt;If you are interested in any other CDN-related details, ask in the comments or ask on X/Twitter &lt;a href="https://twitter.com/janreges" rel="noopener noreferrer"&gt;@janreges&lt;/a&gt;. I will be happy to answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test your websites with my analyzer
&lt;/h2&gt;

&lt;p&gt;In conclusion, I would like to recommend one of my personal open-source projects, which I would like to help improve the quality of websites around the world. The tool is available as a &lt;a href="https://github.com/janreges/siteone-crawler-gui" rel="noopener noreferrer"&gt;desktop application&lt;/a&gt;, but also a &lt;a href="https://github.com/janreges/siteone-crawler" rel="noopener noreferrer"&gt;command-line tool&lt;/a&gt; usable in CI/CD pipelines. For Windows, macOS and Linux.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftz3qaz6upoowg5tptmfk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftz3qaz6upoowg5tptmfk.png" alt="SiteOne Crawler - Free Website Analyzer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I launched it at the end of 2023 and I believe that it will help a lot of people to increase security, performance, SEO, accessibility or other important aspects of a quality web presentation or application. It's called &lt;a href="https://crawler.siteone.io/?utm_source=dev.to&amp;amp;utm_campaign=cdn-part-2"&gt;SiteOne Crawler - Free Website Analyzer&lt;/a&gt; and I also wrote an &lt;a href="https://dev.to/janreges/siteone-crawler-useful-tool-you-will-oe1"&gt;article&lt;/a&gt; about it. Below you will find 3 descriptive videos - the last one also shows what report it will generate for your website.&lt;/p&gt;

&lt;p&gt;In addition to various analyses, it also offers, for example, the export of the entire website into an offline form, where you can view the entire website from a local disk without the internet, or the generation of sitemaps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sharing this project&lt;/strong&gt; with your colleagues and friends will be the &lt;strong&gt;greatest reward for me&lt;/strong&gt; for writing these articles. &lt;strong&gt;Thank you and I wish you all the best in 2024&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Desktop Application&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/rFW8LNEVNdw"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Command-line tool&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/25T_yx13naA"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;HTML report - analysis results&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/PHIFSOmk0gk"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>devops</category>
      <category>webdev</category>
      <category>performance</category>
      <category>nginx</category>
    </item>
    <item>
      <title>How to build a CDN (1/3): introduction and basic components</title>
      <dc:creator>Ján Regeš</dc:creator>
      <pubDate>Fri, 09 Apr 2021 09:37:07 +0000</pubDate>
      <link>https://forem.com/janreges/how-to-build-a-cdn-1-3-introduction-and-basic-components-345o</link>
      <guid>https://forem.com/janreges/how-to-build-a-cdn-1-3-introduction-and-basic-components-345o</guid>
      <description>&lt;p&gt;If your projects have high traffic and you need to deliver a lot of static files, there is nothing easier than getting a commercial CDN. But if you’re a technology enthusiast like us, you can build a real CDN yourself.&lt;/p&gt;

&lt;p&gt;This is the first in a series of three articles and aims to introduce you to the issue and describe the basic components that make up the CDN (&lt;em&gt;Content Delivery Network&lt;/em&gt;). The next two articles will describe the technologies used and their configurations, as well as various other tips regarding the operation, functionality and monitoring of the CDN.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fip0t4e6ff3ebgjl1nmvs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fip0t4e6ff3ebgjl1nmvs.png" alt="SiteOne CDN - latency in Europe"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Motivation to use CDN
&lt;/h1&gt;

&lt;p&gt;The main motivation for using the CDN is clear — to ensure fast and reliable loading of the website and its content for all visitors over the world. But if you care about the operation of projects with a monthly traffic of millions of users and the traffic just from JS/CSS files is tens of TB, then sooner or later you will get to a state where your 1 Gbps internet connection simply stops to be sufficient.&lt;/p&gt;

&lt;p&gt;Websites are usually composed of dynamic and static content. Dynamic content usually includes generated HTML code and data from various APIs (typically REST or GraphQL). Static content is made up of files such as javascripts, styles, images, fonts, or audio/video. A typical ratio for our projects is that dynamic content makes up 10% and static 90% of the total data transfer.&lt;/p&gt;

&lt;p&gt;If you have a really high number of visitors, the introduction of the rule that static files are cached in a browser valid for one year will not help you much. Changing the contents of a file then requires a file with a new name or some “version” in the query parameter to force the browser to download the new file. If you do a release every few days, even if you use JS/CSS chunks, at least some part of JS/CSS will be recompiled and every visitor must download it.&lt;/p&gt;

&lt;p&gt;Then, when you reach gigabit at the peak of traffic, you start to deal with what to do next and thus look for a CDN.&lt;/p&gt;

&lt;h1&gt;
  
  
  The main benefits of CDN from our point of view
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Speed ​​for global visitors&lt;/strong&gt; — if you have a project hosted on servers in only one specific country, this increases the loading time of the pages in proportion to the distance. So the further around the world, the slower on the screen. The reason is high latency and low baud rate. But be careful here — if you have the vast majority of visitors from the local country (for us it is Czech Republic), make sure that your CDN provider has servers (PoPs) in the Czech Republic as well. Otherwise, the load speed for your primary users may slow down after deploying CDN. The Czech Republic is a small country, but has TOP-quality data centers and connectivity providers. Loading content only from foreign countries PoPs would be disadvantageous to Czech visitors.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Speed ​​for local visitors&lt;/strong&gt; — all browsers have a limit on the maximum number of concurrent requests per server IP address. If the browser can load content from multiple different domains and IP addresses (&lt;em&gt;domain sharding&lt;/em&gt;), it will allow more parallelization and the content will load into the browser faster. This is especially important for JS/CSS/images and fonts that are part of the initial rendering of the page. HTTP/2 with multiplexing helps to solve this problem very well, but only to a certain extent. Based on real requirements/rendering tests, we conclude that even with HTTP/2 streams, where there are dozens of files in one stream, the resulting page display is slower than with the involvement of a CDN on another domain than the site itself.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Reduce the load on primary servers&lt;/strong&gt; — if you don’t have a CDN, your primary servers and their connectivity must handle both dynamic content requests and relatively trivial static data requests. This is inefficient because the optimal server configuration for dynamic content is little different than for handling static files.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Content Optimization&lt;/strong&gt; — A good CDN also provides tools for data/binary optimization of static content. As a result, less data is transferred and pages load faster (brotli compression, WebP or AVIF images).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Cost savings&lt;/strong&gt; — even though it has long been possible to get an almost unlimited “thick” line to your primary servers, the jumps are quite drastic — why pay 10 Gbit, when 1 Gbit is enough for us 90% of the time?&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Simplifying the life of DevOps&lt;/strong&gt; — if the configurations of file/web and application servers for maximum performance and security are fine-tuned, then it is necessary to have all possible metrics from real operation. If the traffic for dynamic and static content is strictly separated, then the statistics are cleaner. It is therefore possible to make better decisions and optimize performance and security parameters exactly tailored to the specific workload.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Why we decided to build our own CDN
&lt;/h1&gt;

&lt;p&gt;There are many commercial CDNs on the market, for example see at &lt;a href="https://www.cdnperf.com/" rel="noopener noreferrer"&gt;CDNPerf&lt;/a&gt; . The best known include CloudFlare, Amazon CloudFront, Google, Fastly, Akamai, KeyCDN or our favorite and recommended &lt;a href="https://bunnycdn.com/?ref=ubyhm76592" rel="noopener noreferrer"&gt;&lt;strong&gt;BunnyCDN&lt;/strong&gt;&lt;/a&gt; or &lt;a href="https://www.cdn77.com/?utm_source=dev.to&amp;amp;utm_medium=article&amp;amp;utm_campaign=jan-reges-siteone-cdn-1"&gt;&lt;strong&gt;CDN77&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our projects are most often visited by clients from the Czech and Slovak Republics. Unrivaled in such a case, both in terms of function, price and immediate professional support, is &lt;a href="https://www.cdn77.com/?utm_source=dev.to&amp;amp;utm_medium=article&amp;amp;utm_campaign=jan-reges-siteone-cdn-1"&gt;&lt;strong&gt;CDN77&lt;/strong&gt;&lt;/a&gt; and their &lt;a href="https://www.cdn77.com/network?utm_source=dev.to&amp;amp;utm_medium=article&amp;amp;utm_campaign=jan-reges-siteone-cdn-1"&gt;awesome network&lt;/a&gt;. It is one of the best CDN to cover traffic from around the world. Their very strong ability is also video streaming for the world’s largest high-traffic projects.&lt;/p&gt;

&lt;p&gt;Because we don’t want to invent a wheel in &lt;a href="https://www.siteone.cz/?utm_source=dev.to&amp;amp;utm_medium=article&amp;amp;utm_campaign=cdn-1a"&gt;&lt;strong&gt;SiteOne&lt;/strong&gt;&lt;/a&gt;, we first looked to see if any of the above-mentioned providers would suit us. Our requirements were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;100 TB&lt;/strong&gt; data transfer per month (majority from Europe).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Low latency&lt;/strong&gt; and fast transfers in the Czech Republic/Slovakia.&lt;/li&gt;
&lt;li&gt;  Very good &lt;strong&gt;coverage&lt;/strong&gt; of the whole of Europe, good coverage of North America and sufficient coverage of other continents.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;HTTP/2&lt;/strong&gt; (and fast deployment of HTTP/3 after it is more standardized).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Brotli&lt;/strong&gt; compression, which is even 15% — 30% more efficient on text files than gzip (LZ77 + dictionary).&lt;/li&gt;
&lt;li&gt;  Automatic &lt;strong&gt;JPG/PNG conversion&lt;/strong&gt; → &lt;strong&gt;WebP&lt;/strong&gt;/&lt;strong&gt;AVIF&lt;/strong&gt;, if supported by the browser (reduces data transfer without noticeable loss of quality by 30% to 1,000% depending on how much the source JPG/PNG has already been optimized).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;TLSv1.3 with 0-RTT&lt;/strong&gt; (zero round-trip) significantly reduces the hand-shake communication time of browsers with servers.&lt;/li&gt;
&lt;li&gt;  API for selective &lt;strong&gt;cache invalidation using regular expressions&lt;/strong&gt;. Ideally with support for cache tagging by response HTTP header like &lt;em&gt;X-Cache-Tags&lt;/em&gt; or &lt;em&gt;X-Key&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;DDoS protection&lt;/strong&gt; &amp;amp; Web Application Firewall (&lt;strong&gt;WAF&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Access to logs&lt;/strong&gt; and statistics.&lt;/li&gt;
&lt;li&gt;  100 GB of storage (typically for videos and large image libraries).&lt;/li&gt;
&lt;li&gt;  Custom HTTPs certificates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finding a provider that meets most requirements was not a problem. The problem was the price. For progressive players (such as &lt;a href="https://bunnycdn.com/?ref=ubyhm76592" rel="noopener noreferrer"&gt;&lt;strong&gt;BunnyCDN&lt;/strong&gt;&lt;/a&gt; or &lt;a href="https://www.cdn77.com/?utm_source=dev.to&amp;amp;utm_medium=article&amp;amp;utm_campaign=jan-reges-siteone-cdn-1"&gt;&lt;strong&gt;CDN77&lt;/strong&gt;&lt;/a&gt;) you can buy a service for about 1 000 EUR/month, for other leaders in the CDN market, the costs start at 3–4 000 EUR/month and increase in multiples. If you start working with such amounts as a budget for building your own CDN, the return on investment (ROI) will become more than interesting. Of course, there are other price-friendly providers on the global market, but usually their coverage in the Czech Republic/Slovakia is very weak, so they cannot be recommended for primarily local projects.&lt;/p&gt;

&lt;p&gt;Combining the above requirements with our enthusiasm for IT challenges, we have come to the conclusion that we will build our own CDN. The resulting (but our own) CDN is not nearly robust as that of commercial providers, yet it meets all our requirements. A big advantage is the fact that we can scale very quickly according to our real needs, at low cost.&lt;/p&gt;

&lt;p&gt;Another of our motivations for our own CDN is that we use GraphQL for all web projects in recent years. Unlike REST, this cannot simply be cached on a reverse proxy or CDN, because everything is POST requests to one single URL endpoint. Of course, there are already attempts in the world, however, no commercial CDN offers a sophisticated cache of POST requests. We have types of projects where clever selective caching of POST requests at the CDN level (probably written in &lt;em&gt;Lua&lt;/em&gt;) could greatly ease application servers. For us, this is another useful benefit that commercial CDNs will not offer for a long time.&lt;/p&gt;

&lt;p&gt;At the end of this chapter, it should be noted that our CDN is designed primarily for handling static files and its deployment on the web does not require any changes in the DNS origin of the domain. Therefore, our CDN do not serve as a proxy for absolutely all requests to the website (which is the usual way of deploying commercial CDNs), only to static files. To deploy our CDN, it is necessary to prefix file paths with our main CDN domain, which can be solved also very easily without the need to intervene in the application itself, eg. using the output filter in &lt;em&gt;Nginx&lt;/em&gt; (&lt;em&gt;sub_filter&lt;/em&gt;).&lt;/p&gt;

&lt;h1&gt;
  
  
  CDN components
&lt;/h1&gt;

&lt;p&gt;In order for our CDN to meet all the required parameters, we first had to provide all the components and processes that are needed to operate a quality CDN. And of course learn some new areas. Because we manage more than 120 servers for our other projects, we had everything we needed to handle it technically and procedurally.&lt;/p&gt;

&lt;p&gt;The following chapters describe in more detail the individual components of the CDN that you will need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Domain&lt;/strong&gt; — used mainly for configuring GeoDNS rules and possible referencing of other domains via CNAME.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;GeoDNS&lt;/strong&gt; — a network service that will direct visitors to the nearest servers according to your settings and requirements.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Servers&lt;/strong&gt; — strategically located around the world, in order to minimize latency for visitors and maximize transfer speed.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Technologies&lt;/strong&gt; and their &lt;strong&gt;configurations&lt;/strong&gt; — fine-tuned operating system and reverse proxy with caching and content optimization (brotli compression, WebP, AVIF).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Operational tools&lt;/strong&gt; — you will have many servers and need to solve orchestration, backup, monitoring, metrics, logs and much more.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Auxiliary applications&lt;/strong&gt; — background processes that provide, for example, static brotli compression or conversion of images to WebP/AVIF.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Domain
&lt;/h1&gt;

&lt;p&gt;First, choose and buy the second-level domain on which you will run the CDN. It is ideal to choose a domain that you achieve “cookie-less” requests. During heavy traffic, every byte saved is counted. In the examples of the article we will use “company.com” and its subdomain “cdn.company.com”.&lt;/p&gt;

&lt;p&gt;You will manage the DNS zone file for this domain with the GeoDNS provider(s) of your choice.&lt;/p&gt;

&lt;p&gt;Get an SSL/TLS certificate for the domain, whether from Let’s Encrypt or a commercial Certification Authority (CA). Consider a wildcard certificate, which will make your life easier if you use more than one subdomain. You can get trusted wildcard certificates from as little as 40 USD/year. I recommend, for example, &lt;a href="https://www.ssl2buy.com/" rel="noopener noreferrer"&gt;ssl2buy.com&lt;/a&gt; and give a few seconds google the discount code. You will often get an identical certificate from the same CA for 30–40% of the price than elsewhere.&lt;/p&gt;

&lt;p&gt;To prevent attackers from spoofing other IP addresses for your domain, setup DNSSEC for your domain. Check the correct DNS configuration yourself with the &lt;a href="https://zonemaster.labs.nic.cz/" rel="noopener noreferrer"&gt;Zonemaster&lt;/a&gt; tool from CZ.NIC. We had to temporarily deactivate DNSSEC on our CDN because we use two DNSs in primary-primary mode (for each of them, GeoDNS rules and failovers are defined differently). In this mode, setting up DNSSEC on both providers is difficult because they would both have to share the same private key, or some other solution. So far, this manual intervention is complicated for providers, but they have promised to allow it in the future.&lt;/p&gt;

&lt;p&gt;Whether you use this domain directly in URLs or just as a hostname so that you can route to the CDNs of other domains via CNAME is up to you.&lt;/p&gt;

&lt;h1&gt;
  
  
  GeoDNS with failover support
&lt;/h1&gt;

&lt;h2&gt;
  
  
  What you need GeoDNS for
&lt;/h2&gt;

&lt;p&gt;A critical component of a real CDN is an area of ​​interest, let’s call it: &lt;em&gt;GeoDNS&lt;/em&gt; . You can also find it under the names &lt;em&gt;IP intelligence&lt;/em&gt; , &lt;em&gt;GeoIP&lt;/em&gt; , &lt;em&gt;Geo-based routing, Latency-based routing&lt;/em&gt; , etc.&lt;/p&gt;

&lt;p&gt;GeoDNS is a network service that translates a domain name into an IP address(es), taking into account the location/country from which the visitor comes. If someone is interested in details, they can study them in &lt;a href="https://tools.ietf.org/html/rfc7871" rel="noopener noreferrer"&gt;RFC 7871 (Client Subnet in DNS Queries)&lt;/a&gt; .&lt;/p&gt;

&lt;p&gt;We, as the administrator of the GeoDNS settings of our CDN domain, can define various rules from which continents/states the traffic should be directed to which IP addresses (PoPs in specific states). To be precise — PoP (Point of Presence) can technically mean only one server or more servers, in front of which is a load balancer (typically eg HAProxy).&lt;/p&gt;

&lt;p&gt;Because we needed to rent servers abroad and from various providers, in addition, we do not have many years of experience with some, so we needed to solve the guarantee of high availability. Therefore, the critical functionality of GeoDNS is also &lt;strong&gt;automatic failover&lt;/strong&gt; — the ability to monitor the availability of individual PoPs and the immediate elimination or replacement of unavailable or non-functional PoPs in the CDN topology.&lt;/p&gt;

&lt;p&gt;In practice, it looks like our URL status is monitored every minute on each PoP. When it starts to fail from more than one place at once, the set failover scenario is automatically activated, which, according to our per-PoP consideration, has 2 main forms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Deactivation of the DNS record&lt;/strong&gt; — in such a case it will direct traffic only to the second secondary PoP in the given locality (if any), or visitors will start directing to the default PoPs (in our case all in the Czech Republic).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Replacing the IP address with another&lt;/strong&gt; — with this setting you can say “&lt;em&gt;If the PoP in Paris goes out in France, let the traffic go to the nearby PoP in the Netherlands instead, and if it doesn’t happen by accident, to the PoP in Germany&lt;/em&gt;“.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Due to the minute TTL, a really non-functional PoP is deactivated or replaced by another, no later than 2–3 minutes to all end visitors. However, if you have at least two PoPs defined for each location (DNS resolves to 2 IP addresses), then the browsers will be able to cope with such an outage, and visitors may not even know the critical 2–3 minute moment, which we describe in the next chapter. If you have only one PoP defined for a site and you do not have a backup PoP defined for it, then visitors from this site are in case of failure to route to the default PoPs, which are set as default for “the rest of the world”.&lt;/p&gt;

&lt;p&gt;Even given the minute TTL, it is necessary to think about the speed of DNS translation, this also has a significant effect on the page load speed. We therefore recommend that you choose a DNS provider that has anycast NS (Name Servers) worldwide. Cloudflare leads in the speed ladder, see benchmarks on &lt;a href="https://www.dnsperf.com/" rel="noopener noreferrer"&gt;DNSPerf.com&lt;/a&gt; . With a global DNS provider, you can be sure that your domain will be translated into units of up to tens of milliseconds around the world.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browsers also help with high availability
&lt;/h2&gt;

&lt;p&gt;Because high availability is essential for us, we use the native functionality of browsers, which can work with the fact that our CDN domain will be translated in all major locations to multiple IP addresses from different providers. The real behavior of browsers is then such that the browser randomly selects one of the IP addresses and tries to make requests to it. If the IP address is unavailable, the browser will try another IP address after a few seconds.&lt;/p&gt;

&lt;p&gt;Failure of one of the IP addresses / servers / providers will not cause the required content to malfunction. It will only take a little longer to load the page. Today’s browsers are already really smart and very helpful in terms of outage detection, connection recovery and auto-retry logic. The driving force of this area are mainly mobile/portable devices, where there are frequent mini outages due to switching connectivity between BTS in mobile networks, their alternation with WiFi networks, etc.&lt;/p&gt;

&lt;p&gt;Unfortunately, we have not yet found any publicly available information/specifications that would specify exactly how these auxiliary functionalities are implemented in individual browsers. We therefore rely only on our own tests and analyzes of behavior from current versions of individual browsers.&lt;/p&gt;

&lt;p&gt;If you have studied this unique issue, share the information in the discussion with us :-)&lt;/p&gt;

&lt;h2&gt;
  
  
  Which GeoDNS provider to choose?
&lt;/h2&gt;

&lt;p&gt;There are many GeoDNS providers to choose from — it is worth mentioning Amazon Route53, &lt;a href="https://www.cloudns.net/aff/id/295942/" rel="noopener noreferrer"&gt;&lt;strong&gt;ClouDNS&lt;/strong&gt;&lt;/a&gt;, NS1, &lt;a href="https://constellix.com/products/geodns?utm_source=dev.to&amp;amp;utm_medium=article&amp;amp;utm_campaign=jan-reges-cdn-1"&gt;&lt;strong&gt;Constellix GeoDNS&lt;/strong&gt;&lt;/a&gt;, FastDNS from Akamai, EasyDNS, UltraDNS from Neustar or DNS Made Easy.&lt;/p&gt;

&lt;p&gt;Due to high availability, we do not recommend relying on only one DNS provider, even if it has NS servers worldwide, with anycast IP addresses. Likewise, the distribution of changes is usually solved by one “central brain” and once every few years there are defects that eventually affect more or all NS servers at once (real experience from 2019). Therefore, we decided to go the route of redundant primary-primary settings, where we run all GeoDNS settings at two completely independent providers.&lt;/p&gt;

&lt;p&gt;This is a bit annoying, because the AXFR protocol for DNS synchronization of GeoDNS zones does not support the problem, so we have to manage everything manually with two independent providers. We tested six GeoDNS providers and due to their grasp of “GeoDNS rule modeling” and monitoring, we cannot imagine that someone would propose a uniform specification for GeoDNS issues in order to synchronize DNS zones.&lt;/p&gt;

&lt;p&gt;We at &lt;a href="https://www.siteone.cz/?utm_source=dev.to&amp;amp;utm_medium=article&amp;amp;utm_campaign=cdn-1b"&gt;&lt;strong&gt;SiteOne&lt;/strong&gt;&lt;/a&gt; have chosen for GeoDNS as the first &lt;a href="https://www.cloudns.net/aff/id/295942/" rel="noopener noreferrer"&gt;&lt;strong&gt;ClouDNS&lt;/strong&gt;&lt;/a&gt; provider to offer excellent options for setting up the “geo rules” themselves and an automatic failover with multiple behavior options. The provider has DDoS protection, has anycast IP addresses and low latency from the Czech Republic/Slovakia. It also provides traffic statistics and has very decent limits and pricing due to the number of DNS requests (in the basic GeoDNS package there are 100M queries per month).&lt;/p&gt;

&lt;p&gt;The big advantage is non-stop chat support 24/7, which can answer technical questions in a matter of minutes, or tailor the price program, even if you do not fit into any of the pre-prepared packages.&lt;/p&gt;

&lt;p&gt;As the second DNS provider, we chose the company &lt;a href="https://constellix.com/products/geodns?utm_source=dev.to&amp;amp;utm_medium=article&amp;amp;utm_campaign=jan-reges-cdn-1"&gt;Constellix&lt;/a&gt; (sister of DNS Made Easy), which offers similar options for setting up GeoDNS issues, monitoring and failover as &lt;a href="https://www.cloudns.net/aff/id/295942/" rel="noopener noreferrer"&gt;ClouDNS&lt;/a&gt;. The strength of Constellix is ​​the definition of weights (traffic distribution) in some situations.&lt;/p&gt;

&lt;p&gt;At first, we also liked Microsoft Azure and its Traffic Manager, but in the end we gave it up because it didn’t give us the ability to manage traffic in some countries the way we wanted. However, Azure pleasantly surprised us with its pricing policy in the area of ​​DNS compared to other global cloud providers, such as Amazon or Google.&lt;/p&gt;

&lt;p&gt;Route53 from Amazon is also worth considering, which is more cost-effective if DNS resolves to IP addresses in AWS. However, if you send tens of TB or more from AWS per month, then expect monthly costs in the thousands of USD/EUR. But you already have the same or more expensive as if you conveniently rent a commercial CDN.&lt;/p&gt;

&lt;p&gt;For all GeoDNS providers, however, the price depends mainly on the number of DNS requests and the number and frequency of health checks. In other words, from the number of PoPs you have in the CDN, or from how many places around the world you have them monitored to eliminate false positives and, of course, the monitoring frequency, which can usually be set from 30 seconds to tens of minutes — our default is one minute. You can also reduce the price for DNS requests many times by increasing the TTL for individual DNS records. However, and of course at the expense of the speed of a possible auto-failover, because the recursive NS cache will keep the translations longer in their cache.&lt;/p&gt;

&lt;p&gt;For the biggest pioneers, there is also a variant to build your own GeoDNS service with your own name servers. But for this to make sense and real value, anycast IP addresses would be needed. Also a number of other reliable servers around the world with DDoS protection and then understand, select and adapt eg &lt;a href="https://github.com/jedisct1/edgedns" rel="noopener noreferrer"&gt;EdgeDNS&lt;/a&gt; or Czech &lt;a href="https://www.knot-dns.cz/" rel="noopener noreferrer"&gt;Knot DNS&lt;/a&gt; (which also uses Cloudflare). However, commercial GeoDNS services are relatively cheap and reliable, so we can’t imagine an ROI that would make sense with our own small, non-commercial DNS solution.&lt;/p&gt;

&lt;h1&gt;
  
  
  Servers
&lt;/h1&gt;

&lt;h2&gt;
  
  
  GEO server layout and provider selection
&lt;/h2&gt;

&lt;p&gt;If you are going to build your own CDN, then take into account that if it is to be a real CDN, you will need 8–10 servers around the world in even the smallest setup. We currently have twenty production and three test ones. We also have two development PoPs, available only on the internal network, that developers can use to deploy CDNs to internal development domains as well.&lt;/p&gt;

&lt;p&gt;The main goal of CDN is to provide visitors around the world with the lowest possible latency and the highest transfer rate to the data that CDN caches locally.&lt;/p&gt;

&lt;p&gt;The ideal situation is if you have the opportunity to analyze visitors to projects for which you use the CDN. If you know from which continents/countries what traffic and which data transfers you handle, then you can &lt;strong&gt;strategically decide&lt;/strong&gt; on which continents and in which countries you will place your PoPs.&lt;/p&gt;

&lt;p&gt;In the beginning, you won’t have servers in every state, and probably not on every continent, so consider “catchment areas.” However, based on real latency and traceroute measurements, you will often be surprised that the latency between ISPs in each state does not correspond to geographical proximity. Peering between states and individual ISPs is different, very often “neighbor is not neighbor”. E.g. from Finland, you may have significantly lower latency to the Czech Republic than to Poland for some providers. If you do not yet have any servers abroad through which you could perform measurements, the &lt;a href="https://wondernetwork.com/pings/" rel="noopener noreferrer"&gt;WonderNetwork.com&lt;/a&gt; tool can also help you . This tool shows the latency between different cities of the world, vice versa. Of course, this is a fee for the ISP used in this tool, but it is sufficient for orientation.&lt;/p&gt;

&lt;p&gt;Do a good market research when choosing a server provider and connectivity. Of course, price is not the only or last factor, but it must not be the first. We focused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Provider quality and reputation&lt;/strong&gt; — In each state, 2–3 robust providers usually stand out, who should be the most reliable. Their robust infrastructure should be better able to withstand potential DDoS attacks. We do not recommend small and unverified providers.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Local and global connectivity of the provider&lt;/strong&gt; — it is necessary to take into account that the servers will handle large traffic. Partly in their own country, some are catchment areas for other states as well. Therefore, focus on studying and comparing their connectivity abroad. A quality provider describes its connectivity on the web because it is usually proud of it. &lt;a href="https://www.sh.cz/sit-a-telehouse" rel="noopener noreferrer"&gt;SuperHosting&lt;/a&gt; , which we have part of our infrastructure for 15 years, does great for us .&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Quality support&lt;/strong&gt; — sooner or later some problems will definitely occur and it is necessary to react quickly. As a first test, you can choose to communicate with support about what line the server will actually have available (usually 100 or 1,000 Mbps), what aggregation it has, and what they mean by “Unlimited traffic.” If this includes your estimated XY terabytes per month that the server will need to handle. You can ask the second question to the possibilities and functioning of their DDoS protection.&lt;/li&gt;
&lt;li&gt;  The expected &lt;strong&gt;data traffic&lt;/strong&gt; on a given server should &lt;strong&gt;ideally be included in the price&lt;/strong&gt;, or there should be a clear pricing policy in advance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our CDN currently counts 20 PoPs and each is from a different provider. So far, our primary Czech/Slovak visitors are covered by six PoPs (4 × Prague, Brno and Bratislava). Then Germany (two PoPs) and Poland (two PoPs) for part of Eastern and Northern Europe. We also have one PoP in France, Italy, England and Hungary. The two PoPs also cover North America. South America is covered by only one PoP in Sao Paulo. Africa is covered by one PoP in Cairo, Australia by one PoP in Sydney, the Russian Federation by one PoP in Moscow and Asia by one PoP in Mumbai. These PoPs also include selected neighboring states, where it made sense to us according to the measured latencies.&lt;/p&gt;

&lt;p&gt;In the next chapter, you will also find information on how you can cover various secondary sites very effectively with the help of a commercial CDN, if it makes functional and economic sense. For our CDNs, this makes sense to us, so we have covered most of the non-redundant sites described above with commercial CDNs, and we only have some our PoPs as a backup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recommendation&lt;/strong&gt; : select at least two independent providers in each important location — ideally with different foreign connectivity. Try to ensure that at least two independent PoPs (IP addresses) are resolved in each DNS site. In the event of a failure of one of the PoPs, visitors will not have to wait 2–3 minutes for DNS failover, because browsers detect this and immediately switch traffic to the other IP address. In current browser versions, you will only see “ &lt;em&gt;Connecting…”&lt;/em&gt; for 2–3 seconds and the content will then be read immediately from the second IP address.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; You can test the quality of your CDN topology (especially with regard to latencies from different parts of the world) using the online tool &lt;a href="https://maplatency.com/" rel="noopener noreferrer"&gt;MapLatency.com&lt;/a&gt; . This is great in that it measures latency from endpoints at different ISPs, which means that it measures more realistic latency of visitors to your CDN, not just from servers/datacenters. For us, the coverage of Europe is key and we have it very good for our needs (see screenshot). The &lt;a href="https://www.cdnperf.com/tools/cdn-latency-benchmark" rel="noopener noreferrer"&gt;CDN Latency Test&lt;/a&gt; from CDNPerf fulfills the same purpose — but it measures latencies from data centers, not from end devices.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5mvc3hy99xir5v14e5yd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5mvc3hy99xir5v14e5yd.png" alt="SiteOne CDN - latency over the world"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Use of commercial CDN for better coverage
&lt;/h2&gt;

&lt;p&gt;At some point, you will be very sorry (as well as we) that you will not give visitors in remote corners of the world (for us it is mainly Africa, Asia, Australia and South America) such comfort (latency and transfer speed) as in Europe. But even that has its own effective and simple solution.&lt;/p&gt;

&lt;p&gt;You can cover remote corners of the world with a commercial CDN provider that has a robust infrastructure and strong coverage in these locations as well. Because these are low-traffic secondary sites (hundreds of GB to TB units per month), you can take advantage of a pay-as-you-go CDN provider and cost you a few tens or hundreds of dollars a month. On the one hand, this may seem like parasitism, but on the other hand, when we examined the IP addresses of commercial CDNs in different countries, we found that some providers shared their own infrastructure in different locations. So it’s not unusual. We all want to deliver maximum value to our clients, but at the same time we have to think about the economy and operating costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to set it up?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;The commercial CDN&lt;/strong&gt; will &lt;strong&gt;provide&lt;/strong&gt; you &lt;strong&gt;with a hostname&lt;/strong&gt; , usually under their 3rd order domain managed in their GeoDNS (eg “&lt;em&gt;mycompany.cdn-provider.com&lt;/em&gt;”), to which you can point your CDN domain through CNAME.&lt;/li&gt;
&lt;li&gt;  For a &lt;strong&gt;commercial CDN, set&lt;/strong&gt; it to “listen” &lt;strong&gt;to your&lt;/strong&gt; “cdn.company.com” &lt;strong&gt;domain in&lt;/strong&gt; addition to the hostname mentioned above . You will also need to set up an SSL/TLS certificate. The provider will probably offer you the opportunity to use Let’s Encrypt, but we recommend using your own SSL certificate purchased from a public CA, uniform for all PoPs. If you have different certificates in different locations and, moreover, with a short validity, it will not be possible to use SSL pinning, which you may need in some situations.&lt;/li&gt;
&lt;li&gt;  For your GeoDNS provider, &lt;strong&gt;route the CNAME&lt;/strong&gt; of your domain in all secondary locations to the hostname of the commercial CDN. Technically illustrated: set it to “&lt;em&gt;(Africa) cdn.company.com → CNAME mycompany.cdn-provider.com”.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;You must avoid loops&lt;/strong&gt; . You must not tell the commercial CDN to listen to “&lt;em&gt;cdn.company.com&lt;/em&gt;” and at the same time set it as the original domain. The African PoP would have resolved the DNS origin to itself. To prevent such looping, you must ensure that a few major PoPs will listen on the domain, eg “&lt;em&gt;cdn-src.company.com&lt;/em&gt;” (it directs A records to eg the three main PoPs in the EU). You then set “&lt;em&gt;cdn-src.company.com&lt;/em&gt;” as the origin, so if the PoP commercial CDN does not have the file in its cache, it will download it from one of the main PoPs in the EU through “&lt;em&gt;cdn-src.company.com&lt;/em&gt;”.&lt;/li&gt;
&lt;li&gt;  If, over time, you find out from &lt;strong&gt;statistics and billing&lt;/strong&gt; that it will be more advantageous for you to cover a location with your PoPs due to increased traffic, then you always have the option and you can deploy it without an outage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The disadvantage of secondary sites is that they are very far from the origin servers, and it is likely that most first visitors will wait quite a long time before the cache heats up. Therefore, it is advisable to prepare a background process that will regularly push these most queried files into the commercial CDN storage from the TOP requests statistics. There will be a good chance that remote visitors will be able to retrieve content from the local PoP immediately, even though it was called for the first time at that PoP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardware
&lt;/h2&gt;

&lt;p&gt;If you already have selected providers, you still have to choose a specific physical or virtual server from their menu. This of course depends on your budget. But also decide how important the site is to you and your visitors.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few of our verified recommendations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Virtual vs. physical server&lt;/strong&gt; — this is a rather controversial topic and it is not appropriate to generalize it. If the economy allows, choose physical servers for critical servers, even if only those from the basic menu. Redundant disks are a must, ideally with redundant power supply. With a physical server, you usually get a 1 Gbps uplink and a direct physical connection directly to the ToR switch. There is a much lower chance that you will struggle with sharing CPU and IO or connectivity on a physical hypervisor running hundreds, or dozens of virtual servers at best. If you’re lucky, they have a shared “tube” of * × 10 Gbit, or worse, they have 1 Gbit. With authenticated providers you don’t even have to worry about virtual servers, just watch the aggregation and performance (eg benchmark &lt;a href="https://github.com/n-st/nench" rel="noopener noreferrer"&gt;nench&lt;/a&gt;). Over time, the collected metrics will also tell you a lot, especially for redundant PoPs that will handle ± the same traffic (DNS round-robin). As a result, we have very quickly detected very aggressive CPU throttling or volatile IO performance at some providers.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;CPU&lt;/strong&gt; — if you do it smartly and tighten the static gzip and brotli compression correctly, you will be able to handle hundreds of Mbps even with 1–2 CPU cores. However, if you do not provide static compression and ad-hoc compress each request, you need at least 4–8 cores. It is good to choose a modern CPU with a high clock speed (turbo at 3 GHz+). By the way, the absence of static compression is something that, according to our benchmarks, commercial CDNs are often missing, and as a result, they send textual content much more slowly than with it.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;RAM&lt;/strong&gt; — the minimum is 1 GB, but the more, the better. This is because the cache filesystem (PageCache) is stored in RAM. Usually, this cache will contain most of the small but most downloaded files (typically JS/CSS/fonts). The more of them fit in the RAM, the lower the IOPS requirements, so you can more safely afford a larger rotating HDD instead of an SSD. When you have enough RAM, even with hundreds of Mbps, you can have almost zero IOPS on storage.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;SSD/NVME vs. HDD&lt;/strong&gt; — of course we recommend SSD/NVME for handling high IOPS. But the real need depends on the actual operation. We have preferred SSDs over high capacity everywhere. 100–200 GB per-server is enough for us. But it is also necessary to take into account the fact that you need to log in. It is optimal to rotate the logs continuously, send them to a collection point for further processing and clean them.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Connectivity&lt;/strong&gt; — it is advisable to have a realistic idea of ​​how much traffic and especially its peaks you will handle. As for the less important PoP, 100 Mbps will suffice. However, when it comes to PoP in an important location, prefer 1 Gbps and distribute the load among multiple PoPs (round-robin DNS, when more A records are returned). You will achieve overall higher throughput and lower load on specific ISPs, in addition to higher availability of the CDN as a whole. Whoever has the budget and the real need for this, of course, can choose a 10 Gbps port, but it is necessary to count on a high price.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Orchestration
&lt;/h2&gt;

&lt;p&gt;Because you will manage several servers around the world with 99% identical configuration, you need to ensure automated installation, configuration, and mass orchestration.&lt;/p&gt;

&lt;p&gt;We use and recommend &lt;a href="https://www.ansible.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Ansible&lt;/strong&gt;&lt;/a&gt;. Historically, we’ve also used Puppet, Chef and SaltStack for a while, but only Ansible meets what we need for many years. Over the years of use, we have over 80 own roles in it, so when preparing each additional server, the most time-consuming is order and waiting for an activation e-mail. If we have 10 or 50 servers, it doesn’t matter from the orchestration point of view.&lt;/p&gt;

&lt;p&gt;Whether you manage the servers with any orchestration tool, we recommend a few things to help you eliminate the “global outage”:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  When &lt;strong&gt;deploying changes&lt;/strong&gt; to all servers &lt;strong&gt;in bulk&lt;/strong&gt; , be careful — deploy to individual servers should run in series rather than in parallel. Possibly also in parallel, but for example after three servers at once simultaneously (in the Ansible playbook this is controlled by the “&lt;em&gt;serial&lt;/em&gt;” directive). If the deploy on one of the servers fails, force the deploy to abort (in the Ansible directive, “&lt;em&gt;max_fail_percentage&lt;/em&gt;”).&lt;/li&gt;
&lt;li&gt;  Before &lt;strong&gt;restarting/reloading components,&lt;/strong&gt; first &lt;strong&gt;check the validity of the configuration&lt;/strong&gt; (&lt;em&gt;configtest&lt;/em&gt;). Eliminate outages associated with invalid configuration. Some distributions and their init scripts do not do this automatically. Ideally, &lt;em&gt;configtest&lt;/em&gt; should be performed before restarting the service to prevent the service from stopping and starting.&lt;/li&gt;
&lt;li&gt;  At the &lt;strong&gt;end of deployment&lt;/strong&gt; to an individual server, perform a &lt;strong&gt;set of CDN functionality tests&lt;/strong&gt; on that particular server. E.g. by calling the status URL and ideally also by calling some functional URL from one of the originals, which will be returned from the cache and also one URL, which, on the contrary, will not be in the cache and will have to be downloaded from the original. We also have one “service” origin domain for these purposes. In conjunction with serial deployment, you can be sure that you will not cause outages on more than 1 PoP at a time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Server configuration and reverse proxy (cache)
&lt;/h1&gt;

&lt;p&gt;If you already have prepared servers, the really interesting and &lt;br&gt;
creative part awaits you — the preparation of configurations of individual SW components, of which the CDN is composed.&lt;/p&gt;

&lt;p&gt;In the next article (in 2–3 weeks), we will focus on operating system settings (with real settings for &lt;em&gt;Debian&lt;/em&gt; Linux), reverse proxy (&lt;em&gt;Nginx&lt;/em&gt; as cache) and other aspects related to CDN traffic — content optimization, security, attack protections or settings that affect search engines behavior. And maybe also cache tagging and its invalidation based on &lt;em&gt;Varnish&lt;/em&gt; (we are working on it these weeks). This is a very useful feature that very few CDN providers offer and only in their most expensive plans.&lt;/p&gt;

&lt;p&gt;Thanks for reading, and if you like the article, I will be happy if you share it or leave a comment. Have a nice day :-)&lt;/p&gt;

&lt;p&gt;If you are interested in any other CDN-related details, ask in the comments or ask on X/Twitter &lt;a href="https://twitter.com/janreges" rel="noopener noreferrer"&gt;@janreges&lt;/a&gt;. I will be happy to answer.&lt;/p&gt;
&lt;h2&gt;
  
  
  Test your websites with my analyzer
&lt;/h2&gt;

&lt;p&gt;In conclusion, I would like to recommend one of my personal open-source projects, which I would like to help improve the quality of websites around the world. The tool is available as a &lt;a href="https://github.com/janreges/siteone-crawler-gui" rel="noopener noreferrer"&gt;desktop application&lt;/a&gt;, but also a &lt;a href="https://github.com/janreges/siteone-crawler" rel="noopener noreferrer"&gt;command-line tool&lt;/a&gt; usable in CI/CD pipelines. For Windows, macOS and Linux.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftz3qaz6upoowg5tptmfk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftz3qaz6upoowg5tptmfk.png" alt="SiteOne Crawler - Free Website Analyzer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I launched it at the end of 2023 and I believe that it will help a lot of people to increase security, performance, SEO, accessibility or other important aspects of a quality web presentation or application. It's called &lt;a href="https://crawler.siteone.io/?utm_source=dev.to&amp;amp;utm_campaign=cdn-part-1"&gt;SiteOne Crawler - Free Website Analyzer&lt;/a&gt; and I also wrote an &lt;a href="https://dev.to/janreges/siteone-crawler-useful-tool-you-will-oe1"&gt;article&lt;/a&gt; about it. Below you will find 3 descriptive videos - the last one also shows what report it will generate for your website.&lt;/p&gt;

&lt;p&gt;In addition to various analyses, it also offers, for example, the export of the entire website into an offline form, where you can view the entire website from a local disk without the internet, or the generation of sitemaps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sharing this project&lt;/strong&gt; with your colleagues and friends will be the &lt;strong&gt;greatest reward for me&lt;/strong&gt; for writing these articles. &lt;strong&gt;Thank you and I wish you all the best in 2024&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Desktop Application&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/rFW8LNEVNdw"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Command-line tool&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/25T_yx13naA"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;HTML report - analysis results&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/PHIFSOmk0gk"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: this article was written with the best intentions and without advertising purposes. However, it contains a few partner links in the text to specific providers with whom we have many years of excellent experience.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>devops</category>
      <category>webdev</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
