AboutBlogContact
DevOpsApril 12, 2016 5 min read 20

How to Migrate an Existing Site to HTTPS with Let's Encrypt and Zero Downtime (2016)

AunimedaAunimeda
📋 Table of Contents

How to Migrate an Existing Site to HTTPS with Let's Encrypt and Zero Downtime (2016)

Short answer: Install Certbot, get certificate with certbot --nginx -d example.com, add 301 redirect from HTTP, fix mixed content (HTTP resources on HTTPS page), update canonical URLs and sitemaps, notify Google Search Console of the protocol change. In 2016, Certbot was still young — use the --nginx plugin, but verify its edits before reloading.


Install Certbot (2016 method)

# Ubuntu 14.04/16.04 — Certbot added to apt in 2016
sudo apt-get install software-properties-common
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install python-certbot-nginx

# Alternatively (original method from 2015):
sudo wget https://dl.eff.org/certbot-auto
chmod +x certbot-auto

Get Certificate

# Nginx plugin — automatically edits nginx config
sudo certbot --nginx -d example.com -d www.example.com

# Webroot method (safer — no nginx editing):
sudo certbot certonly \
    --webroot \
    --webroot-path=/var/www/html \
    -d example.com \
    -d www.example.com \
    --email admin@example.com \
    --agree-tos \
    --no-eff-email

# Certificates stored in:
# /etc/letsencrypt/live/example.com/fullchain.pem
# /etc/letsencrypt/live/example.com/privkey.pem

Nginx Configuration After Migration

# /etc/nginx/sites-available/example.com

# Step 1: Keep HTTP temporarily for ACME challenges
server {
    listen 80;
    server_name example.com www.example.com;

    # Let's Encrypt renewal
    location /.well-known/acme-challenge/ {
        root /var/www/html;
        try_files $uri =404;
    }

    # Everything else → HTTPS
    location / {
        return 301 https://example.com$request_uri;
    }
}

# Step 2: Redirect www → non-www
server {
    listen 443 ssl http2;
    server_name www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    return 301 https://example.com$request_uri;
}

# Step 3: Main HTTPS server
server {
    listen 443 ssl http2;  # http2 available in nginx 1.9.5+, 2016 standard
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Modern SSL config (2016 Mozilla Intermediate)
    ssl_protocols             TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers               ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:kEDH+AESGCM:!aNULL:!eNULL:!EXPORT;
    ssl_prefer_server_ciphers on;
    ssl_session_cache         shared:SSL:10m;
    ssl_session_timeout       10m;
    ssl_stapling              on;
    ssl_stapling_verify       on;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options SAMEORIGIN;

    root /var/www/html;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php7.0-fpm.sock;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param HTTPS on;
    }
}

Fix Mixed Content

Mixed content (HTTP resources loaded on HTTPS page) causes browser warnings and blocks:

# Find mixed content in codebase
grep -r "http://" /var/www/html/templates/
grep -r 'src="http://' /var/www/html/templates/
grep -r "url('http://" /var/www/html/assets/css/
<?php
// PHP: replace hardcoded http:// URLs in database content
// This handles CMS content stored with http:// URLs

function forceHttpsInContent(string $content): string {
    return str_replace('http://example.com', 'https://example.com', $content);
}

// Or: use protocol-relative URLs (works for external resources)
// http://cdn.example.com/file.js → //cdn.example.com/file.js
<!-- HTML: protocol-relative for external scripts/styles -->
<!-- OLD: -->
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>

<!-- NEW: -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>

<!-- Or simply HTTPS: -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
<?php
// WordPress: update all URLs in database after HTTPS migration
// Via WP-CLI:
// wp search-replace 'http://example.com' 'https://example.com' --all-tables

// Manual SQL:
UPDATE wp_options  SET option_value = REPLACE(option_value, 'http://example.com', 'https://example.com');
UPDATE wp_posts    SET post_content  = REPLACE(post_content,  'http://example.com', 'https://example.com');
UPDATE wp_postmeta SET meta_value    = REPLACE(meta_value,    'http://example.com', 'https://example.com');

Update Canonical URLs and Sitemap

<?php
// After migration: update canonical URLs in code

// Before:
$canonicalUrl = 'http://example.com' . $page['path'];

// After:
$canonicalUrl = 'https://example.com' . $page['path'];
// Or dynamically:
$protocol     = isset($_SERVER['HTTPS']) ? 'https' : 'http';
$canonicalUrl = $protocol . '://example.com' . $page['path'];
<!-- sitemap.xml: update all URLs to https:// -->
<!-- Before: -->
<loc>http://example.com/about/</loc>
<!-- After: -->
<loc>https://example.com/about/</loc>

Notify Google Search Console

1. Add HTTPS property to Google Search Console:
   - Add: https://example.com (new property)
   - Google treats http:// and https:// as different sites

2. Update preferred domain setting in old HTTP property:
   - Search Console → Settings → Preferred domain
   - With 301 redirects, Google will consolidate PageRank

3. Submit new sitemap:
   - Search Console → Crawl → Sitemaps → Submit
   - URL: https://example.com/sitemap.xml

4. Monitor for crawl errors in both properties for 4 weeks

Auto-Renewal Setup

# Certbot auto-renewal (test first):
sudo certbot renew --dry-run

# Cron job (runs twice daily):
crontab -e
# Add:
0 2,14 * * * /usr/bin/certbot renew --quiet && systemctl reload nginx

# Systemd timer (Ubuntu 16.04 default):
# Certbot package installs /etc/cron.d/certbot automatically
# Verify:
cat /etc/cron.d/certbot
# 0 */12 * * * root test -x /usr/bin/certbot -a \! -d /run/systemd/system && perl -e 'sleep int(rand(3600))' && certbot -q renew

Post-Migration Checklist

☑ Certificate installed, nginx serves HTTPS
☑ HTTP → HTTPS 301 redirects working
☑ www → non-www (or vice versa) 301 redirect
☑ No mixed content in browser console
☑ Canonical URLs updated to https://
☑ Sitemap updated to https://
☑ Google Search Console: new https property added
☑ New sitemap submitted
☑ Auto-renewal cron configured
☑ HSTS header present: Strict-Transport-Security
☑ Test on SSL Labs: A or A+ rating
☑ Monitor Search Console for 4 weeks (expect temporary ranking fluctuation)

Google confirmed in 2016 that HTTPS is a ranking signal. Sites that migrated saw small but measurable ranking improvements, especially in competitive niches. The SEO benefit wasn't the main driver — user trust and browser warnings were. But the ranking uplift was a welcome bonus.

Read Also

Docker and CI/CD for a Small Dev Team: What We Actually Ship in Productionaunimeda
DevOps

Docker and CI/CD for a Small Dev Team: What We Actually Ship in Production

Not every team needs Kubernetes. Here's the Docker-based CI/CD setup we run for 6 production projects with a team of 8 - GitHub Actions, Docker Compose, Nginx, and zero Kubernetes.

How to Use Docker for PHP Development: Replacing WAMP/MAMP in 2016aunimeda
DevOps

How to Use Docker for PHP Development: Replacing WAMP/MAMP in 2016

Docker 1.10 made consistent PHP development environments practical in 2016. No more 'works on my machine' — every developer ran identical nginx + PHP-FPM + MySQL containers. Here's the docker-compose.yml that replaced our team's MAMP setups, plus the gotchas with file permissions and volume mounts on Windows/Mac.

How to Deploy Node.js with Nginx + PM2 on Ubuntu (2015)aunimeda
DevOps

How to Deploy Node.js with Nginx + PM2 on Ubuntu (2015)

PM2 keeps Node.js alive after crashes and reboots. Nginx handles SSL, static files, and proxies API requests. Together they form the deployment stack that replaced Apache for Node.js apps in 2015. Full setup: PM2 config, Nginx virtual host, zero-downtime deploys.

Need IT development for your business?

We build websites, mobile apps and AI solutions. Free consultation.

Get Consultation All articles