0%
Setting Up a Linux Web Server: Nginx and Apache

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.

I

InSkillCoach

· min

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.

CriterionNginxApache
ArchitectureEvent-driven, asynchronousProcess/thread per connection (modern event MPM)
Static contentExcellent, very low memoryGood
Massive numbers of simultaneous connectionsHistorical strengthDecent with mpm_event
Reverse proxy / load balancingNative and widely usedPossible (mod_proxy)
.htaccess filesNot supportedSupported (per-directory configuration)
Code execution (PHP…)Always delegated (FastCGI/PHP-FPM)Built-in modules possible (mod_php)
Configuration syntaxConcise blocksXML-like directives, more verbose
Typical use caseReverse proxy, static content, modern applicationsShared 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 -t or apachectl configtest before every reload, ideally in the same command line.
  • Prefer reload over restart: zero downtime for your visitors.
  • HTTPS everywhere, HTTP-to-HTTPS redirect, and a certbot renew --dry-run after 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, and X-Forwarded-Proto to 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.

InSkillCoach

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+
546
215

Comments

Comments are powered by GitHub Discussions

Sign in with GitHub to join the discussion

Link copied!