Vucense

How to Install and Configure Apache Web Server on Ubuntu 24.04 LTS (2026)

🟢Beginner

Install and configure Apache 2.4 on Ubuntu 24.04 LTS in 2026. Covers virtual hosts, SSL with Let's Encrypt, .htaccess, security hardening, mod_rewrite, reverse proxy, and performance tuning.

Noah Choi

Author

Noah Choi

Linux & Cloud Native Infrastructure Engineer

Published

Duration

Reading

18 min

Build

20 min

How to Install and Configure Apache Web Server on Ubuntu 24.04 LTS (2026)
Article Roadmap

Key Takeaways

  • a2ensite / a2dissite: Ubuntu’s Apache helpers enable and disable virtual hosts by creating symlinks in sites-enabled/. Always use a2ensite mysite rather than manually creating symlinks.
  • systemctl reload apache2: Reload applies config changes without dropping active connections. Use restart only when changing modules or after major configuration changes.
  • .htaccess is Apache’s superpower: Per-directory overrides without touching server config. WordPress, Laravel, Symfony all need AllowOverride All in the virtual host for .htaccess to work.
  • mod_proxy for reverse proxying: ProxyPass / http://127.0.0.1:3000/ routes all requests to a backend application. Apache handles SSL, static files, and rate limiting; the backend only handles application logic.

Introduction

Direct Answer: How do I install and configure Apache on Ubuntu 24.04 LTS in 2026?

Install Apache with sudo apt-get install -y apache2. It starts automatically and is enabled at boot. Test with curl -I http://localhost — you should see 200 OK with Server: Apache/2.4.x. Create a virtual host by writing a config file to /etc/apache2/sites-available/mysite.conf with a <VirtualHost *:80> block containing ServerName mysite.example.com, DocumentRoot /var/www/mysite, and <Directory /var/www/mysite> AllowOverride All Require all granted </Directory>. Enable with sudo a2ensite mysite.conf, disable the default with sudo a2dissite 000-default.conf, test config with sudo apache2ctl configtest, and reload with sudo systemctl reload apache2. For SSL, install Certbot: sudo apt-get install certbot python3-certbot-apache && sudo certbot --apache -d mysite.example.com.


Part 1: Installation

sudo apt-get update
sudo apt-get install -y apache2

# Verify installation
apache2 -v
sudo systemctl status apache2 --no-pager | grep "Active:"

Expected output:

Server version: Apache/2.4.58 (Ubuntu)
Server built:   2024-01-13T12:00:00
     Active: active (running)
# Test default page
curl -sI http://localhost | head -5

Expected output:

HTTP/1.1 200 OK
Date: Tue, 22 Apr 2026 12:00:00 GMT
Server: Apache/2.4.58 (Ubuntu)
Last-Modified: Tue, 22 Apr 2026 09:00:00 GMT
Content-Type: text/html
# Enable essential modules
sudo a2enmod ssl          # SSL/TLS
sudo a2enmod rewrite      # URL rewriting (required by most PHP frameworks)
sudo a2enmod headers      # Security headers
sudo a2enmod proxy        # Reverse proxy
sudo a2enmod proxy_http   # HTTP proxy
sudo a2enmod deflate      # Gzip compression
sudo a2enmod expires      # Browser caching headers
sudo systemctl reload apache2

# List enabled modules
apache2ctl -M | grep -E "ssl|rewrite|proxy|headers" | sort

Expected output:

 headers_module (shared)
 proxy_http_module (shared)
 proxy_module (shared)
 rewrite_module (shared)
 ssl_module (shared)

Part 2: Virtual Hosts

# Create a document root
sudo mkdir -p /var/www/mysite
echo "<h1>My Site — Sovereign</h1>" | sudo tee /var/www/mysite/index.html
sudo chown -R www-data:www-data /var/www/mysite
sudo chmod -R 755 /var/www/mysite

# Create the virtual host configuration
sudo tee /etc/apache2/sites-available/mysite.conf << 'EOF'
<VirtualHost *:80>
    ServerName  mysite.example.com
    ServerAlias www.mysite.example.com
    DocumentRoot /var/www/mysite

    # Logging
    ErrorLog  ${APACHE_LOG_DIR}/mysite-error.log
    CustomLog ${APACHE_LOG_DIR}/mysite-access.log combined

    # Directory permissions and .htaccess support
    <Directory /var/www/mysite>
        Options -Indexes +FollowSymLinks
        AllowOverride All          # Allows .htaccess to override settings
        Require all granted
    </Directory>

    # Deny access to hidden files (.git, .env, etc.)
    <FilesMatch "^\.">
        Require all denied
    </FilesMatch>
</VirtualHost>
EOF

# Enable the site, disable default
sudo a2ensite mysite.conf
sudo a2dissite 000-default.conf

# Test configuration
sudo apache2ctl configtest

Expected output:

Syntax OK
sudo systemctl reload apache2
curl -sI http://localhost | grep "200"

Expected output:

HTTP/1.1 200 OK

Part 3: SSL with Let’s Encrypt

sudo apt-get install -y certbot python3-certbot-apache

# Obtain certificate and auto-configure Apache (requires DNS pointing to server)
sudo certbot --apache -d mysite.example.com --non-interactive \
  --agree-tos --email [email protected] --redirect

# Verify certificate
sudo certbot certificates

Expected output:

Found the following certs:
  Certificate Name: mysite.example.com
    Domains: mysite.example.com
    Expiry Date: 2026-07-22 (VALID: 89 days)
    Certificate Path: /etc/letsencrypt/live/mysite.example.com/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/mysite.example.com/privkey.pem

Manually configured HTTPS virtual host:

<VirtualHost *:80>
    ServerName mysite.example.com
    RewriteEngine On
    RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>

<VirtualHost *:443>
    ServerName mysite.example.com
    DocumentRoot /var/www/mysite

    SSLEngine on
    SSLCertificateFile    /etc/letsencrypt/live/mysite.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/mysite.example.com/privkey.pem

    # Modern TLS — TLS 1.2 and 1.3 only
    SSLProtocol           all -SSLv3 -TLSv1 -TLSv1.1
    SSLHonorCipherOrder   on
    SSLCipherSuite        ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384

    # Security headers
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set X-Content-Type-Options "nosniff"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"

    <Directory /var/www/mysite>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog  ${APACHE_LOG_DIR}/mysite-ssl-error.log
    CustomLog ${APACHE_LOG_DIR}/mysite-ssl-access.log combined
</VirtualHost>

Part 4: mod_rewrite and .htaccess

.htaccess files allow per-directory configuration without server reloads:

# Example .htaccess for a PHP application (WordPress/Laravel pattern)
cat > /var/www/mysite/.htaccess << 'EOF'
# Ensure RewriteEngine is on
Options -Indexes
RewriteEngine On

# Force HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

# Remove trailing slash (except for directories)
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [R=301,L]

# Laravel/Symfony: route all requests through index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]

# Block access to sensitive files
<FilesMatch "\.(env|log|sql|bak|ini|conf)$">
    Require all denied
</FilesMatch>

# Gzip compression
<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/plain text/css application/javascript application/json
</IfModule>

# Browser caching
<IfModule mod_expires.c>
    ExpiresActive On
    ExpiresByType image/jpeg "access plus 1 year"
    ExpiresByType text/css  "access plus 1 month"
    ExpiresByType application/javascript "access plus 1 month"
</IfModule>
EOF

# Test that .htaccess is applied
sudo apache2ctl configtest && sudo systemctl reload apache2
curl -sI http://localhost/nonexistent-path | head -3

Expected output:

HTTP/1.1 200 OK   ← Laravel-style: routes to index.php instead of 404

Part 5: Reverse Proxy to Node.js / Python Backend

sudo tee /etc/apache2/sites-available/proxy-app.conf << 'EOF'
<VirtualHost *:443>
    ServerName api.example.com

    SSLEngine on
    SSLCertificateFile    /etc/letsencrypt/live/api.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/api.example.com/privkey.pem

    # Security headers
    Header always set X-Frame-Options SAMEORIGIN
    Header always set X-Content-Type-Options nosniff

    # Proxy all requests to Node.js/Python backend on port 3000
    ProxyPreserveHost On
    ProxyPass        / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/

    # Pass real client IP to backend
    RequestHeader set X-Real-IP %{REMOTE_ADDR}s
    RequestHeader set X-Forwarded-For %{REMOTE_ADDR}s
    RequestHeader set X-Forwarded-Proto https

    # WebSocket support
    RewriteEngine On
    RewriteCond %{HTTP:Upgrade} websocket [NC]
    RewriteCond %{HTTP:Connection} upgrade [NC]
    RewriteRule ^/?(.*) "ws://127.0.0.1:3000/$1" [P,L]

    ErrorLog  ${APACHE_LOG_DIR}/api-error.log
    CustomLog ${APACHE_LOG_DIR}/api-access.log combined
</VirtualHost>
EOF

sudo a2ensite proxy-app.conf
sudo apache2ctl configtest && sudo systemctl reload apache2

Part 6: Security Hardening

sudo tee /etc/apache2/conf-available/security-hardening.conf << 'EOF'
# Hide Apache version from responses
ServerTokens Prod
ServerSignature Off

# Disable TRACE method (prevents XST attacks)
TraceEnable Off

# Limit request size (10MB)
LimitRequestBody 10485760

# Timeout settings
Timeout 60
KeepAliveTimeout 5
MaxKeepAliveRequests 100
EOF

sudo a2enconf security-hardening
sudo systemctl reload apache2

# Verify version is hidden
curl -sI http://localhost | grep Server

Expected output:

Server: Apache

Version number hidden — attackers can’t target specific CVEs by version.


Part 7: Performance and Monitoring

# Check virtual host routing (diagnostic command)
sudo apache2ctl -S

Expected output:

VirtualHost configuration:
*:443   mysite.example.com (/etc/apache2/sites-enabled/mysite-le-ssl.conf:2)
*:80    mysite.example.com (/etc/apache2/sites-enabled/mysite.conf:1)
ServerRoot: "/etc/apache2"
Main DocumentRoot: "/var/www/html"
# Live access log monitoring
sudo tail -f /var/log/apache2/access.log | awk '{print $1, $7, $9}'

# Most requested URLs today
sudo awk '{print $7}' /var/log/apache2/access.log | sort | uniq -c | sort -rn | head -10

# 5xx errors in last hour
sudo awk -v d="$(date '+%d/%b/%Y:%H')" '$0 ~ d && $9 ~ /^5/' /var/log/apache2/access.log | wc -l

# Test SSL configuration
curl -sI https://mysite.example.com | grep -E "HTTP|Strict|X-Frame"

Expected output:

HTTP/2 200
strict-transport-security: max-age=31536000; includeSubDomains
x-frame-options: SAMEORIGIN

Troubleshooting

403 Forbidden on valid files

Cause: Directory permissions wrong, or Require all granted missing from <Directory> block. Fix:

sudo chown -R www-data:www-data /var/www/mysite
sudo chmod -R 755 /var/www/mysite
# Ensure virtual host has: Require all granted

.htaccess not working

Cause: AllowOverride None in virtual host (Apache default). Fix: Add to the <Directory> block: AllowOverride All. Confirm mod_rewrite is enabled: apache2ctl -M | grep rewrite.

Port 80/443 already in use (conflict with Nginx)

Cause: Both Apache and Nginx cannot bind the same port. Fix: Stop one: sudo systemctl stop nginx or configure Apache to use a different port and put Nginx in front as a reverse proxy.


Conclusion

Apache 2.4 is installed, secured, and configured: virtual hosts route domains to correct document roots, SSL terminates at Apache with Let’s Encrypt, .htaccess enables per-directory config for PHP frameworks, and mod_proxy forwards API traffic to backend applications. The security hardening hides the version string and disables unnecessary methods.

Compare with Nginx Reverse Proxy Tutorial — both serve as web front-ends, but Apache’s .htaccess support makes it the right choice for PHP applications, while Nginx’s lower memory footprint suits high-concurrency API gateways.


People Also Ask

When should I choose Apache over Nginx?

Choose Apache when: hosting PHP applications that use .htaccess (WordPress, Drupal, Laravel, Symfony), running shared hosting where tenants need per-directory configuration, or when your team has deep Apache expertise. Nginx is better for: static file serving, reverse proxying, high-concurrency (Nginx uses event-driven architecture vs Apache’s process/thread model), and memory-constrained servers. For most sovereign self-hosted setups, both work — pick Nginx for new greenfield setups, Apache when the application documentation assumes it.

What does a2enmod, a2ensite, and a2enconf do?

These are Ubuntu-specific helper scripts for Apache configuration management. a2enmod rewrite enables the rewrite module by creating a symlink from /etc/apache2/mods-available/rewrite.load to /etc/apache2/mods-enabled/. a2ensite mysite enables a virtual host from sites-available/ to sites-enabled/. a2enconf myconf enables a config snippet. Their counterparts (a2dismod, a2dissite, a2disconf) remove the symlinks to disable. Always run sudo systemctl reload apache2 after any enable/disable operation.


Further Reading


Tested on: Ubuntu 24.04 LTS (Hetzner CX22). Apache 2.4.58, Certbot 2.11.0. Last verified: April 22, 2026.

Further Reading

All Dev Corner

Comments