I’ll be honest — the first time I set up SSL on Apache, I spent an embarrassing amount of time confused about why my site was still loading over plain HTTP even though I had a certificate installed. The missing piece was that SSL configuration lives inside its own virtual host block, separate from your regular HTTP one. Once that clicked, everything else fell into place. In this post I’ll walk through exactly how I configure SSL virtual hosts in Apache from scratch.
What SSL Virtual Hosts Are and Why They Matter
An SSL (or more accurately, TLS) virtual host is a block bound to port 443 instead of port 80, with directives that tell Apache which certificate and private key to use for encrypting traffic. Every domain you want to serve over HTTPS needs its own SSL-enabled virtual host block, even if it also has a plain HTTP block on port 80.
I set this up for practically every production site I manage, because:
- HTTPS is now a baseline expectation for visitors and a ranking factor for SEO
- Browsers actively warn users away from non-HTTPS sites
- Payment processors and many APIs require encrypted connections
- It’s required for HTTP/2 in most modern browsers
Prerequisites
- Apache 2.4+ installed
mod_sslenabled- A registered domain pointing to your server’s IP
- An SSL certificate — either from Let’s Encrypt or a commercial CA
- Root or sudo access
Step 1: Enable mod_ssl
sudo apt update
sudo apt install -y apache2
sudo a2enmod ssl
sudo systemctl restart apache2
On CentOS/RHEL:
sudo yum install -y mod_ssl
sudo systemctl restart httpd
Step 2: Obtain an SSL Certificate
My go-to method is Certbot with Let’s Encrypt, since it’s free and automates renewal:
sudo apt install -y certbot python3-certbot-apache
sudo certbot --apache -d example.com -d www.example.com
Certbot will ask if I want to redirect all HTTP traffic to HTTPS automatically — I almost always say yes.
If I already have a certificate from a commercial CA, I just make sure I have three files ready: the certificate, the private key, and the intermediate/chain certificate.
Step 3: Create the SSL Virtual Host Block
If I’m doing this manually instead of letting Certbot handle it, I create a config like this:
sudo nano /etc/apache2/sites-available/example.com-ssl.conf
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example.com/public_html
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
ErrorLog ${APACHE_LOG_DIR}/example.com_ssl_error.log
CustomLog ${APACHE_LOG_DIR}/example.com_ssl_access.log combined
Step 4: Redirect HTTP to HTTPS
I keep the plain HTTP virtual host, but I use it purely to redirect visitors to the secure version of the site:
ServerName example.com
ServerAlias www.example.com
Redirect permanent / https://example.com/
Step 5: Enable the Site and Test the Configuration
sudo a2ensite example.com-ssl.conf
sudo apache2ctl configtest
sudo systemctl reload apache2
If configtest reports “Syntax OK,” I know the config itself is valid, though that doesn’t guarantee the certificate paths are correct — I still test in a browser.
Step 6: Verify HTTPS Is Working
I open https://example.com in a browser and check the padlock icon, then click through to view certificate details. I also like using command-line verification:
curl -vI https://example.com
And for a deeper check on certificate chain validity and supported protocols, I run the site through SSL Labs’ test:
https://www.ssllabs.com/ssltest/
Real-World Example: Enforcing HTTPS Sitewide with HSTS
Once HTTPS is working reliably, I add HTTP Strict Transport Security to tell browsers to always use HTTPS for this domain, even if a user types http:// manually:
...
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
This requires mod_headers to be enabled:
sudo a2enmod headers
sudo systemctl restart apache2
Step 7: Automating Certificate Renewal
Let’s Encrypt certificates expire every 90 days, so I never rely on manually remembering to renew them. Certbot installs a systemd timer or cron job automatically during setup, but I always verify it’s actually there:
sudo systemctl list-timers | grep certbot
And I test the renewal process without actually renewing anything, just to confirm it would succeed:
sudo certbot renew --dry-run
If that dry run fails, I know I have a problem waiting to happen in a few weeks, rather than finding out the hard way when a certificate has already expired and visitors start seeing browser warnings.
Step 8: Supporting Multiple SSL Virtual Hosts Cleanly
When I’m running several HTTPS sites on one server, I keep every SSL vhost in its own dedicated file rather than combining them, following the exact same organizational pattern I use for plain HTTP virtual hosts. This makes it trivial to add, remove, or temporarily disable HTTPS for a single site without risking a typo affecting every other domain on the server.
ls /etc/apache2/sites-available/ | grep ssl
I like seeing a clean, predictable list here — site1.com-ssl.conf, site2.com-ssl.conf, and so on — rather than one giant file trying to do everything at once.
Troubleshooting Common Issues
“Unable to start Apache” after adding SSL vhost Almost always a missing or incorrectly pathed certificate/key file. Double-check with:
sudo apache2ctl configtest
Certificate mismatch warning in browser This usually means the ServerName in the vhost doesn’t match the domain the certificate was actually issued for.
Mixed content warnings Happens when HTTPS pages load HTTP resources (images, scripts, stylesheets). I search my codebase for hardcoded http:// references and switch them to protocol-relative or HTTPS URLs.
Certificate expired If I’m using Let’s Encrypt, this usually means automatic renewal silently failed. I check:
sudo certbot renew --dry-run
Security Best Practices
- I disable outdated protocols like SSLv3 and TLS 1.0/1.1, keeping only TLS 1.2 and 1.3:
SSLProtocol -all +TLSv1.2 +TLSv1.3
- I set strong cipher suites and disable weak ones.
- I set
SSLCertificateKeyFilepermissions to600, readable only by root. - I schedule automatic certificate renewal via cron or a systemd timer so certificates never silently expire.
Performance Optimization Tips
- I enable OCSP stapling to speed up certificate validation for visitors:
SSLUseStapling on
SSLStaplingCache "shmcb:/var/run/ocsp(128000)"
- I enable HTTP/2, which requires SSL and gives a real performance boost on modern browsers:
Protocols h2 http/1.1
- I reuse SSL sessions with
SSLSessionCacheto reduce handshake overhead on repeat visits.
Step 9: Understanding the Certificate Chain
Early on, I misunderstood the difference between a certificate and a certificate chain, and it cost me an afternoon of confusion when a site worked fine in Chrome but showed warnings in some other tools. A proper SSL setup needs the full chain — your domain’s certificate plus any intermediate certificates from the certificate authority — not just the single leaf certificate. Let’s Encrypt’s fullchain.pem already includes this, which is why I always reference that file rather than cert.pem alone in my SSLCertificateFile directive. If you’re using a commercial CA instead, you typically combine your certificate with their provided intermediate bundle manually before referencing it in Apache.
Step 10: Monitoring Certificate Expiry Proactively
Even with automated renewal in place, I like having a second layer of monitoring in case a renewal silently fails for some unrelated reason — a firewall change, a DNS misconfiguration, or a Certbot update that breaks a plugin. I use a simple monitoring check that alerts me if any certificate is within 14 days of expiry:
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -noout -enddate
Running this periodically, or wiring it into an external uptime monitoring service, means I find out about a renewal failure from an automated alert rather than from a customer telling me their browser is showing a security warning.
Frequently Asked Questions
Can I run HTTP and HTTPS virtual hosts for the same domain? Yes — this is actually the standard pattern: an HTTP vhost that redirects to HTTPS, and a separate HTTPS vhost that serves the actual content.
Do I need a separate SSL virtual host for every subdomain? Each subdomain needs to be covered by a certificate (or a wildcard certificate), but subdomains can share a single vhost block if configured with ServerAlias or wildcard ServerName.
How often do Let’s Encrypt certificates need renewal? Every 90 days. Certbot handles this automatically if you set up the renewal cron job or systemd timer.
What’s the difference between mod_ssl and Let’s Encrypt? mod_ssl is the Apache module that handles SSL/TLS itself; Let’s Encrypt is a certificate authority that issues the actual certificates you feed into that module.
Summary and Key Takeaways
Configuring SSL virtual hosts in Apache boils down to a repeatable pattern: enable mod_ssl, get a certificate, create a dedicated block pointing to that certificate, redirect HTTP traffic to HTTPS, and verify everything with configtest and a real browser test. Once you’ve done it a couple of times, it becomes second nature.