Fixing Infinite 301 Redirects: Ghost Docker with Cloudflare HTTPS

Fixing Infinite 301 Redirects: Ghost Docker with Cloudflare HTTPS

Introduction

Running Ghost on Docker with Cloudflare as your DNS and CDN provider can significantly improve performance and security. However, many users encounter a frustrating issue: an infinite 301 redirect loop when accessing their blog through HTTPS. If you're stuck in this redirect loop, you're not alone. This comprehensive guide will walk you through understanding the root cause and implementing the most effective solutions.

Understanding the Problem

What Causes the Infinite 301 Redirect Loop?

The infinite 301 redirect loop typically occurs due to a mismatch between how Cloudflare and Ghost handle HTTPS connections. Let's break down the two primary causes:

Cause 1: Cloudflare's "Flexible SSL" Mode

Cloudflare's default SSL/TLS encryption mode is "Flexible," which operates as follows:

  • Connections between your browser and Cloudflare are encrypted with HTTPS
  • Connections between Cloudflare and your origin server (where Ghost runs) are unencrypted HTTP

When Ghost is configured to use HTTPS (by setting url: https://your-domain.com), it automatically redirects all HTTP requests to HTTPS. This creates a problematic cycle:

  1. Browser requests https://your-domain.com
  2. Cloudflare receives the HTTPS request and forwards it to Ghost as HTTP
  3. Ghost sees an HTTP request and redirects to HTTPS (status 301)
  4. Cloudflare receives the redirect response
  5. The cycle repeats indefinitely

Cause 2: Missing Reverse Proxy Headers

When Ghost runs behind a reverse proxy (Nginx, Nginx Proxy Manager, etc.), it needs to receive specific HTTP headers to understand that the original request was secure (HTTPS). Without these headers, Ghost thinks the request is insecure and redirects accordingly.

This is the quickest and most effective solution for most users. Follow these steps:

Step-by-Step Instructions

  1. Log in to your Cloudflare dashboard
  2. Select your domain from the list
  3. Navigate to SSL/TLS in the left sidebar menu
  4. Find the "Encryption mode" section at the top
  5. Change from "Flexible" to "Full (Strict)"

Why This Works

The "Full (Strict)" mode ensures that:

  • Connections from browsers to Cloudflare are HTTPS encrypted
  • Connections from Cloudflare to your origin server are also HTTPS encrypted
  • Ghost receives the request as HTTPS and doesn't need to perform a redirect

This configuration respects your SSL certificate on the origin server and eliminates the protocol mismatch that causes the redirect loop.

Important Considerations

Before changing to "Full (Strict)" mode, ensure that:

  • Your Ghost instance has a valid SSL certificate (Let's Encrypt works great with Docker)
  • Your reverse proxy is properly configured with SSL certificates
  • Your DNS records point correctly to Cloudflare

Solution 2: Configure Nginx Proxy Manager Headers

If you're using Nginx Proxy Manager as your reverse proxy, this solution will resolve the issue while maintaining flexibility:

Step-by-Step Instructions

  1. Open your Nginx Proxy Manager dashboard
  2. Select your Ghost host from the list
  3. Click the "Advanced" tab
  4. Add the following configuration in the Custom Nginx Configuration field:
proxy_set_header Host $http_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 https;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Forwarded-Port 443;
  1. Make sure you have an SSL certificate configured in the SSL tab
  2. Enable "Force SSL" to ensure all traffic is encrypted
  3. Save your configuration

Why This Works

The key header is X-Forwarded-Proto https;. This tells Ghost that the original request was HTTPS, preventing it from issuing a redirect. The other headers ensure that Ghost receives correct information about the client's IP address and the original request details.

Solution 3: Configure Nginx Directly

If you're running Nginx directly (not through Nginx Proxy Manager), add these headers to your Nginx configuration:

server {
    listen 443 ssl http2;
    server_name your-domain.com;
    
    ssl_certificate /path/to/your/certificate.crt;
    ssl_certificate_key /path/to/your/private.key;
    
    # SSL configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    
    location / {
        proxy_pass http://127.0.0.1:2368;
        proxy_set_header Host $http_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;
        proxy_set_header X-Forwarded-Host $server_name;
        proxy_set_header X-Forwarded-Port $server_port;
    }
}

Don't forget the redirect from HTTP to HTTPS:

server {
    listen 80;
    server_name your-domain.com;
    return 301 https://$server_name$request_uri;
}

Solution 4: Verify Your Docker Compose Configuration

Ensure your Docker Compose file is correctly configured:

version: '3.8'

services:
  ghost:
    image: ghost:latest
    container_name: ghost_blog
    ports:
      - "2368:2368"
    environment:
      url: https://your-domain.com
      NODE_ENV: production
      database__client: mysql
      database__connection__host: mysql_service
      database__connection__user: ghost_user
      database__connection__password: your_secure_password
      database__connection__database: ghost_db
    depends_on:
      - mysql_service
    networks:
      - ghost_network
    restart: unless-stopped

  mysql_service:
    image: mysql:8
    container_name: ghost_mysql
    environment:
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_DATABASE: ghost_db
      MYSQL_USER: ghost_user
      MYSQL_PASSWORD: your_secure_password
    networks:
      - ghost_network
    volumes:
      - mysql_/var/lib/mysql
    restart: unless-stopped

networks:
  ghost_network:
    driver: bridge

volumes:
  mysql_

Critical Settings:

  • The url parameter must use HTTPS: https://your-domain.com
  • Ensure your Ghost container and reverse proxy are on the same Docker network
  • The port mapping should align with your reverse proxy configuration

Troubleshooting Guide

If you've applied one or more solutions and still experience redirect loops, follow these diagnostic steps:

Check Ghost Logs

docker logs ghost_container_name

Look for error messages related to redirects or protocol mismatches.

Test with cURL

curl -I https://your-domain.com

This shows the response headers. A 301 response indicates the redirect is still occurring.

curl -I -H "X-Forwarded-Proto: https" http://localhost:2368

This tests how Ghost responds when it receives the HTTPS header.

Verify Cloudflare Settings

  • Check that you've actually saved the SSL/TLS mode change
  • Wait a few minutes for changes to propagate (usually instant)
  • Clear your browser cache or use incognito mode when testing

Examine Reverse Proxy Configuration

  • Verify that your proxy_pass directive points to the correct Ghost port (default: 2368)
  • Ensure all X-Forwarded headers are present
  • Check that SSL certificates are valid and not expired

Check DNS Resolution

nslookup your-domain.com

Confirm that your domain resolves to Cloudflare's nameservers.

Best Practices for Ghost and Cloudflare Integration

Once you've resolved the redirect issue, follow these best practices:

1. Use Full (Strict) SSL Mode

"Full (Strict)" provides the best security posture, ensuring end-to-end encryption. This is recommended for production environments.

2. Enable Additional Cloudflare Security Features

  • Always Use HTTPS: Forces all HTTP traffic to HTTPS
  • Minimum TLS Version: Set to TLS 1.2 or higher
  • HSTS (HTTP Strict Transport Security): Adds an extra layer of security

3. Implement Proper Logging

Monitor your Ghost application logs regularly for any unusual redirect patterns or errors. Most issues surface in the logs before they become critical.

4. Regular Certificate Maintenance

If using Let's Encrypt with Docker, ensure your certificate renewal is automated. Most Docker images handle this automatically, but verify it's working.

5. Test After Updates

After updating Ghost or changing your infrastructure, test your HTTPS connection thoroughly to ensure nothing has regressed.

Conclusion

The infinite 301 redirect loop is a common issue when running Ghost on Docker with Cloudflare, but it's entirely preventable with proper configuration. The easiest solution is to change Cloudflare's SSL/TLS mode from "Flexible" to "Full (Strict)," which resolves the issue for most users instantly.

If that doesn't work for your setup, properly configuring your reverse proxy with the correct X-Forwarded headers will eliminate the problem. By understanding the root cause—the protocol mismatch between your CDN and origin server—you can troubleshoot similar issues in the future.

Your Ghost blog deserves a smooth, secure, and fast experience. With these solutions in place, your readers will enjoy uninterrupted access to your content without any redirect headaches.


Have you encountered this issue? Which solution worked for you? Share your experience in the comments below!