Setting Up a Linux Web Server: Nginx and Apache
Complete guide to deploying a Linux web server: Nginx or Apache, virtual hosts, HTTPS with Let's Encrypt, reverse proxy, security, and optimizations.
InSkillCoach
Setting Up a Linux Web Server: Nginx and Apache
Hosting a website or web application on your own Linux server is one of the most instructive exercises in system administration: it draws on package management, systemd services, networking, TLS certificates, and security. In this guide, we will build a complete web server, from installation to HTTPS and reverse proxy, covering the two servers that dominate the market: Nginx and Apache. The examples use Ubuntu/Debian; differences with the Red Hat family are noted where they exist.
1. Choosing Between Nginx and Apache
Both servers are mature, high-performing, and perfectly capable of hosting any site. Their architectures differ, however, and some contexts favor one over the other.
| Criterion | Nginx | Apache |
|---|---|---|
| Architecture | Event-driven, asynchronous | Process/thread per connection (modern event MPM) |
| Static content | Excellent, very low memory | Good |
| Massive numbers of simultaneous connections | Historical strength | Decent with mpm_event |
| Reverse proxy / load balancing | Native and widely used | Possible (mod_proxy) |
| .htaccess files | Not supported | Supported (per-directory configuration) |
| Code execution (PHP…) | Always delegated (FastCGI/PHP-FPM) | Built-in modules possible (mod_php) |
| Configuration syntax | Concise blocks | XML-like directives, more verbose |
| Typical use case | Reverse proxy, static content, modern applications | Shared hosting, legacy, .htaccess required |
In 2026, the default choice for a new project is most often Nginx: native reverse proxy, compact configuration, stable memory consumption under heavy load. Apache remains essential when your users need to modify the configuration per directory (.htaccess) or for legacy applications that assume it. The rest of this guide favors Nginx and gives the Apache equivalents at key steps.
2. Installation
Nginx
# Debian / Ubuntu
sudo apt update
sudo apt install nginx
# RHEL / Rocky / Alma
sudo dnf install nginx
sudo systemctl enable --now nginx
# Check that the service is running
systemctl status nginx
curl -I http://localhost
Apache
# Debian / Ubuntu (the package and the service are named apache2)
sudo apt install apache2
# RHEL / Rocky / Alma (the package and the service are named httpd)
sudo dnf install httpd
sudo systemctl enable --now httpd
Don’t forget to open the firewall:
# Ubuntu with ufw
sudo ufw allow "Nginx Full"
# RHEL with firewalld
sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --reload
3. Configuration Directory Structure
Nginx
/etc/nginx/
├── nginx.conf # main configuration
├── conf.d/ # .conf files included automatically
├── sites-available/ # your vhosts (Debian/Ubuntu)
├── sites-enabled/ # symbolic links to active vhosts
└── snippets/ # reusable fragments (SSL, etc.)
On Debian/Ubuntu, you write each site in sites-available/ and then enable it with a symbolic link in sites-enabled/. On RHEL, this convention doesn’t exist: place your files directly in conf.d/.
# Enable a site (Debian/Ubuntu)
sudo ln -s /etc/nginx/sites-available/monsite.conf /etc/nginx/sites-enabled/
# Disable a site: remove the link, never the source file
sudo rm /etc/nginx/sites-enabled/monsite.conf
Apache
/etc/apache2/ # Debian/Ubuntu (RHEL: /etc/httpd/)
├── apache2.conf # main configuration
├── sites-available/ # available vhosts
├── sites-enabled/ # active vhosts
├── mods-available/ # available modules
└── mods-enabled/ # active modules
Apache provides dedicated commands for enabling and disabling:
sudo a2ensite monsite.conf # enable a vhost
sudo a2dissite monsite.conf # disable
sudo a2enmod rewrite ssl headers # enable modules
4. Virtual Hosts and Server Blocks
A single server can host multiple sites, distinguished by their domain names. Nginx calls them “server blocks,” while Apache calls them “virtual hosts.”
Complete Nginx server block
# /etc/nginx/sites-available/monsite.conf
server {
listen 80;
listen [::]:80;
server_name monsite.example.com;
root /var/www/monsite;
index index.html index.htm;
# Site-specific logs
access_log /var/log/nginx/monsite.access.log;
error_log /var/log/nginx/monsite.error.log;
location / {
# Serve the file, otherwise the directory, otherwise 404
try_files $uri $uri/ =404;
}
# Block access to hidden files (.git, .env...)
location ~ /\. {
deny all;
}
}
# Create the site root and a test page
sudo mkdir -p /var/www/monsite
echo "<h1>Bienvenue sur monsite</h1>" | sudo tee /var/www/monsite/index.html
sudo chown -R www-data:www-data /var/www/monsite
# Enable and reload
sudo ln -s /etc/nginx/sites-available/monsite.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Equivalent Apache virtual host
# /etc/apache2/sites-available/monsite.conf
<VirtualHost *:80>
ServerName monsite.example.com
DocumentRoot /var/www/monsite
ErrorLog ${APACHE_LOG_DIR}/monsite.error.log
CustomLog ${APACHE_LOG_DIR}/monsite.access.log combined
<Directory /var/www/monsite>
Options -Indexes
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
sudo a2ensite monsite.conf
sudo apachectl configtest && sudo systemctl reload apache2
5. HTTPS with Let’s Encrypt
A site without HTTPS is no longer acceptable: browsers and search engines penalize HTTP. Let’s Encrypt provides free certificates, and certbot automates everything, from the validation challenge to renewal.
# Install certbot with the Nginx plugin
sudo apt install certbot python3-certbot-nginx
# (Apache: python3-certbot-apache; RHEL: via EPEL)
# Obtain the certificate AND modify the configuration automatically
sudo certbot --nginx -d monsite.example.com
# Check automatic renewal (systemd timer)
systemctl list-timers | grep certbot
sudo certbot renew --dry-run
Prerequisites: the domain must point to the server’s public IP (A/AAAA DNS record), and port 80 must be reachable for the HTTP-01 challenge. Certbot adds the HTTP-to-HTTPS redirect and configures the certificates in your server block:
# Snippet added by certbot to the server block
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/monsite.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/monsite.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
Let’s Encrypt certificates are valid for 90 days; the systemd timer installed by the package renews them automatically as the expiration date approaches. Just run the --dry-run once after installation to verify.
6. Reverse Proxy to an Application
An extremely common case: your application (Node.js, Python, Go…) listens on a local port, and Nginx publishes it on port 443 while handling TLS, compression, and logs.
# /etc/nginx/sites-available/app.conf
# Backend grouping (allows load balancing later)
upstream mon_app {
server 127.0.0.1:3000;
keepalive 32;
}
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
location / {
proxy_pass http://mon_app;
# Pass client information to the application
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Reasonable timeouts
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
}
}
The X-Forwarded-* headers are essential: without them, your application sees every request coming from 127.0.0.1 and thinks it is on HTTP. On the Red Hat family, also remember the SELinux boolean that allows Nginx to open outbound connections:
sudo setsebool -P httpd_can_network_connect on
7. Security Headers
A few HTTP headers protect your visitors against clickjacking, MIME sniffing, and downgrades to HTTP. Place them in the server block (or in a snippet included by all your sites):
# /etc/nginx/snippets/security-headers.conf
# HSTS: forces HTTPS for 2 years (only enable it once HTTPS is stable)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
# Prevents the site from being embedded in a third-party iframe (clickjacking)
add_header X-Frame-Options "SAMEORIGIN" always;
# Prevents the browser from guessing MIME types
add_header X-Content-Type-Options "nosniff" always;
# Limits referrer information sent to third-party sites
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# In each HTTPS server block
include snippets/security-headers.conf;
Add a Content-Security-Policy tailored to your site once you know precisely where its scripts and styles come from: it is the most effective header, but a setting that is too strict will break the site. Test your configuration on securityheaders.com and the TLS part on ssllabs.com.
Also hide the server version, which is useless information to offer to scanners:
# In http {} of /etc/nginx/nginx.conf
server_tokens off;
8. Logs and Rotation
Reading the logs
# Follow traffic in real time
sudo tail -f /var/log/nginx/access.log
# Follow errors
sudo tail -f /var/log/nginx/error.log
# Top 10 most active IPs
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
# Count response codes
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
Rotation with logrotate
The Nginx and Apache packages already install a logrotate configuration; check it and adjust retention to your needs:
# /etc/logrotate.d/nginx (provided by the package, excerpt)
/var/log/nginx/*.log {
daily # daily rotation
rotate 14 # keep 14 archives
compress # compress archives
delaycompress # except the most recent
notifempty
sharedscripts
postrotate
invoke-rc.d nginx rotate >/dev/null 2>&1
endscript
}
# Test without waiting for the night
sudo logrotate -d /etc/logrotate.d/nginx
If you create per-site logs as in our examples, the *.log pattern covers them automatically.
9. Testing and Reloading Without Downtime
A reflex to ingrain permanently: never restart a web server without having validated the configuration, and always prefer a reload over a restart.
# 1. Validate the syntax and consistency of the configuration
sudo nginx -t
# Apache: sudo apachectl configtest
# 2. Reload: workers finish their in-flight requests,
# and new workers start with the new configuration.
# No connection is dropped.
sudo systemctl reload nginx
# In one line, reload conditioned on the test
sudo nginx -t && sudo systemctl reload nginx
systemctl restart abruptly cuts off ongoing connections; reserve it for cases where a reload is not enough (changing listening ports, updating the binary).
10. Basic Optimizations
gzip compression
# In http {} of /etc/nginx/nginx.conf
gzip on;
gzip_comp_level 5; # good CPU / size tradeoff
gzip_min_length 256; # pointless below 256 bytes
gzip_types text/plain text/css application/json application/javascript
application/xml image/svg+xml;
# JPEG/PNG/WebP images are already compressed: don't list them
Static file caching
Tell browsers to keep resources that don’t change for a long time:
# In the server block
location ~* \.(css|js|jpg|jpeg|png|webp|svg|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off; # also lightens the logs
}
This strategy assumes that your files change names with each version (hash in the file name, which all modern bundlers do). Otherwise, reduce the duration.
Worker processes
# In /etc/nginx/nginx.conf
worker_processes auto; # one worker per CPU core: the right setting
events {
worker_connections 1024; # simultaneous connections per worker
}
auto is the default value in recent packages and suits nearly all cases. Only increase worker_connections if your logs show the limit being reached, and then think about the file descriptor limit (worker_rlimit_nofile).
Best Practices
- One configuration file per site, enabled by symbolic link; don’t touch the files provided by the package.
- Validate with
nginx -torapachectl configtestbefore every reload, ideally in the same command line. - Prefer
reloadoverrestart: zero downtime for your visitors. - HTTPS everywhere, HTTP-to-HTTPS redirect, and a
certbot renew --dry-runafter installation to validate automatic renewal. - Add security headers in a shared snippet and test the result on securityheaders.com.
- Dedicated logs per site, verified rotation, and a regular eye on
error.log. - Behind a reverse proxy, always pass
Host,X-Forwarded-For, andX-Forwarded-Prototo the application. - Back up
/etc/nginx(or/etc/apache2) and/etc/letsencrypt: a web server configuration can be rebuilt in minutes when it is version-controlled.
Conclusion
You now have a complete web server: multiple sites as virtual hosts, automatic HTTPS with Let’s Encrypt, a reverse proxy to your applications, security headers, well-managed logs, and basic optimizations. Nginx and Apache are remarkably reliable software: properly configured, they run for years without intervention. The next steps in your progression: load balancing across multiple backends, caching with proxy_cache, rate limiting (limit_req) against abuse, and automating all of this configuration with Ansible or Terraform so you can rebuild your server with a single command.
About InSkillCoach
Training and technology expert
Coach specializing in advanced technologies and AI, backed by GNeurone Inc.
Certifications:
- AWS Certified Solutions Architect – Professional
- Google Cloud certifications
- Microsoft Certified: DevOps Engineer Expert
- Certified Kubernetes Administrator (CKA)
- CompTIA Security+
Comments
Comments are powered by GitHub Discussions
Sign in with GitHub to join the discussion