Serving a website securely over HTTPS is a fundamental requirement for any modern web presence. This chapter guides you through deploying a static website using Nginx as the web server, securing it with HTTPS via Certbot and Let’s Encrypt, and orchestrating both services with Docker Compose. This isn’t just about getting a site online; it’s about doing it with modern containerization best practices, preparing you for robust production deployments.

By the end of this project, you will have a fully functional, secure static website accessible over HTTPS, demonstrating essential Docker concepts like multi-service orchestration, volume management, and network configuration. You’ll also learn how to automate SSL certificate provisioning and renewal, a critical aspect of web security that often trips up new developers.

This milestone is important because it lays the groundwork for understanding how to containerize and manage robust web services. A secure, performant Nginx server is often the first line of defense and traffic manager for more complex applications. You’ll gain practical, hands-on experience with tools widely used in production environments, setting a strong foundation for future projects.

Project Overview: Secure Static Site

Our objective is to host static content (HTML, CSS, JS) and ensure all traffic is encrypted with HTTPS. This setup requires two primary components: a web server (Nginx) to serve content and handle requests, and an SSL certificate management tool (Certbot) to automate certificate acquisition and renewal. Docker Compose will define and run these services together.

Problem Statement

We need to serve a static website, example.com, securely over HTTPS. The solution must be containerized, easy to deploy, and capable of handling automatic SSL certificate renewal to maintain continuous security without manual intervention. This approach prioritizes security, maintainability, and resource efficiency.

Tech Stack

This project leverages the following core technologies:

  • Docker Engine (v29.1.x, checked 2026-08-06): The underlying platform for building and running containers.
  • Docker Compose (v2.40.3, checked 2026-08-06): For defining and running multi-container Docker applications.
  • Nginx (v1.26.x, checked 2026-08-06): A high-performance HTTP and reverse proxy server, used here to serve static content and manage SSL/TLS.
  • Certbot (v2.10.0, checked 2026-08-06): A free, open-source tool that automates the process of obtaining and renewing SSL/TLS certificates from Let’s Encrypt.
  • Let’s Encrypt: A free, automated, and open certificate authority (CA) that issues SSL/TLS certificates.

Required Docker Concepts

This project will solidify your understanding of several core Docker concepts:

  • Dockerfiles: Creating custom images for Nginx, focusing on optimized builds for smaller, more secure production images.
  • Docker Compose: Defining and running multi-container applications as a single unit.
  • Volumes: Persisting Nginx configuration, website content, and Certbot certificates across container restarts and updates.
  • Networks: Enabling secure and isolated communication between Nginx and Certbot containers.
  • Environment Variables: Passing configuration data, like the domain name, to containers dynamically.
  • Image Building & Tagging: Creating and managing your custom images.

High-Level Architecture

Our setup will involve three main logical layers working together:

  1. Static Content: Your website files (HTML, CSS, JS) that Nginx will serve.
  2. Nginx Container: Serves the static content, acts as a reverse proxy, and handles HTTP-to-HTTPS redirection. It also temporarily exposes a path for Certbot’s domain verification challenge.
  3. Certbot Container: Obtains and renews SSL certificates from Let’s Encrypt. It uses the HTTP-01 challenge, which requires Nginx to temporarily proxy requests to Certbot’s designated volume.

These containers will share critical data through Docker volumes, ensuring persistent storage for configuration and SSL certificates.

Architecture Diagram

flowchart TD User_Browser[User Browser] -->|HTTP HTTPS Request| Docker_Host[Docker Host] subgraph Docker_Network["Docker Network"] Nginx_C[Nginx Container] Certbot_C[Certbot Container] end Docker_Host --> Nginx_C Nginx_C -->|Proxy ACME Challenge| Certbot_C Nginx_C -->|Serve Static Files| Static_Content_Vol[Static Content Volume] Nginx_C -->|Read Config| Nginx_Config_Vol[Nginx Config Volume] Certbot_C -->|Write Read Certificates| Certbot_Vol[Certbot Certificates Volume] Nginx_C -->|Read Certificates| Certbot_Vol

This diagram illustrates how user requests first reach the Docker host. They are then routed to the Nginx container, which either serves static files, redirects HTTP traffic to HTTPS, or proxies requests for Certbot’s domain verification challenge. All critical data—static files, Nginx configuration, and Certbot certificates—is stored in named Docker volumes, ensuring data persistence even if containers are stopped or replaced.

Build Plan

We will implement this project in the following incremental steps:

  1. Initialize Project Structure: Set up the necessary directories.
  2. Create Static Content: Add a simple index.html file.
  3. Define Nginx Dockerfile: Create a custom image for Nginx.
  4. Configure Nginx: Write the Nginx configuration for HTTP, HTTPS, and Certbot challenges.
  5. Configure Docker Compose: Orchestrate Nginx and Certbot services, volumes, and networks.
  6. Set Environment Variables: Define the domain name in a .env file.
  7. Generate Initial Certificates: Use Certbot to acquire SSL certificates. This involves a temporary Nginx configuration.
  8. Deploy Final Configuration: Start all services with the permanent Nginx configuration.

Step-by-Step Implementation

Let’s build this project incrementally, starting with our project structure and static content.

1. Initialize Project Directory Structure

First, create a clean directory for our project. This structure helps organize configuration, static assets, and Docker-related files.

mkdir -p secure-static-site/nginx/conf.d
mkdir -p secure-static-site/www
cd secure-static-site

This command creates the following:

  • secure-static-site/: The root directory for our entire project.
  • secure-static-site/nginx/conf.d/: This is where our Nginx server configuration files will be stored.
  • secure-static-site/www/: This directory will hold our static website content (HTML, CSS, JavaScript, images).

2. Create Static Website Content

For demonstration purposes, let’s create a very simple index.html file in the www directory. This will be the content Nginx serves.

File: secure-static-site/www/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Secure Static Site</title>
    <style>
        body { font-family: sans-serif; text-align: center; margin-top: 50px; background-color: #f4f4f4; color: #333; }
        h1 { color: #007bff; }
        p { font-size: 1.1em; }
    </style>
</head>
<body>
    <h1>Welcome to Your Secure Static Website!</h1>
    <p>This site is served by Nginx and secured with Let's Encrypt via Certbot.</p>
    <p>If you see this over HTTPS, it's working!</p>
</body>
</html>

This basic HTML file provides visual confirmation that our Nginx server is successfully serving content.

3. Define Nginx Dockerfile

We’ll create a custom Docker image for Nginx. Using a custom Dockerfile allows us to precisely control the Nginx environment and integrate our static files and configuration.

File: secure-static-site/nginx/Dockerfile

# Use the official Nginx stable image as a base for our web server.
# The `alpine-slim` variant is chosen for its minimal size, enhancing security and reducing download times.
# Docker Engine version 29.1.x is current as of 2026-08-06.
# Nginx stable version 1.26.x is current as of 2026-08-06.
FROM nginx:1.26-alpine-slim

# Copy our custom Nginx configuration files into the container.
# These files will override or supplement the default Nginx configuration.
COPY conf.d/ /etc/nginx/conf.d/

# Copy the static website content into Nginx's default serving directory.
# The --chown=nginx:nginx flag ensures the files are owned by the 'nginx' user and group inside the container.
# This adheres to the principle of least privilege, preventing Nginx from running with unnecessary root permissions.
COPY --chown=nginx:nginx ../www/ /usr/share/nginx/html/

# Expose ports 80 (HTTP) and 443 (HTTPS).
# This informs Docker that the container listens on these ports, serving as documentation.
# It does not automatically open these ports on the host system's firewall.
EXPOSE 80
EXPOSE 443

# Define the command to run Nginx when the container starts.
# "daemon off;" ensures Nginx runs in the foreground, which is essential for Docker containers
# as Docker expects the main process to remain active to keep the container running.
CMD ["nginx", "-g", "daemon off;"]

Explanation of the Nginx Dockerfile:

  • FROM nginx:1.26-alpine-slim: We base our image on the official Nginx 1.26 stable release, specifically the alpine-slim variant. Alpine Linux is known for its small footprint, which translates to smaller, more secure Docker images. The version 1.26 is stable as of our checked date, 2026-08-06.
  • COPY conf.d/ /etc/nginx/conf.d/: This instruction copies our custom Nginx configuration files from the host’s nginx/conf.d directory into the container’s /etc/nginx/conf.d/. These files will define how Nginx handles requests.
  • COPY --chown=nginx:nginx ../www/ /usr/share/nginx/html/: Here, we copy our static website content from the host’s www directory into the container’s /usr/share/nginx/html/. The --chown flag is a crucial security measure, ensuring that the files are owned by the nginx user and group inside the container. This prevents Nginx from needing to run as root to access its content, adhering to the principle of least privilege.
  • EXPOSE 80 / EXPOSE 443: These lines indicate that the Nginx container is configured to listen on ports 80 (HTTP) and 443 (HTTPS). This is primarily for documentation and network introspection within Docker; actual port mapping to the host is handled in docker-compose.yml.
  • CMD ["nginx", "-g", "daemon off;"]: This specifies the command that will be executed when the container starts. nginx -g "daemon off;" runs Nginx in the foreground. In Docker, a container typically runs only as long as its primary process is active. Running Nginx in the foreground ensures the container stays alive.

4. Configure Nginx for Certbot and HTTPS

We need a single Nginx configuration file that handles both HTTP and HTTPS traffic, and importantly, allows Certbot to perform its domain ownership challenges.

File: secure-static-site/nginx/conf.d/default.conf

# Server block for HTTP traffic (port 80)
server {
    listen 80;
    listen [::]:80; # Listen on IPv6 as well

    server_name ${DOMAIN}; # Dynamically set domain from environment variable

    # Configuration to handle Certbot's ACME challenge requests.
    # Certbot will place temporary files in /var/www/certbot, and Nginx needs to serve them.
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    # All other HTTP traffic is redirected to HTTPS.
    # This is a security best practice to ensure all communication is encrypted.
    location / {
        return 301 https://$host$request_uri;
    }
}

# Server block for HTTPS traffic (port 443)
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2; # Listen on IPv6

    server_name ${DOMAIN};

    # SSL certificates and key provided by Certbot.
    # These paths point to the shared volume where Certbot stores them.
    ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;

    # Essential SSL/TLS security settings for robust encryption and performance.
    # These settings prioritize strong ciphers and modern protocols.
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1h;
    ssl_protocols TLSv1.2 TLSv1.3; # Only allow modern, secure TLS protocols
    ssl_prefer_server_ciphers on;
    ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
    ssl_ecdh_curve secp384r1; # Strong elliptic curve, requires OpenSSL >= 1.0.2
    ssl_stapling on; # Enable OCSP stapling for faster certificate validation
    ssl_stapling_verify on; # Verify OCSP response
    
    # HTTP Strict Transport Security (HSTS) header.
    # Tells browsers to only connect via HTTPS for a long period, preventing downgrade attacks.
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
    
    # Security headers to prevent common web vulnerabilities.
    add_header X-Frame-Options DENY; # Prevents clickjacking
    add_header X-Content-Type-Options nosniff; # Prevents MIME-sniffing attacks

    # Root directory for serving static files over HTTPS.
    root /usr/share/nginx/html;
    index index.html index.htm; # Default files to serve if a directory is requested

    # Custom error pages (optional, but good for user experience)
    error_page 404 /404.html;
    location = /404.html {
        internal;
    }
}

Explanation of Nginx default.conf:

  • HTTP Server Block (listen 80):
    • server_name ${DOMAIN};: Nginx will use the DOMAIN environment variable (which we’ll define in docker-compose.yml and a .env file) to match incoming requests. This makes the configuration flexible.
    • location /.well-known/acme-challenge/: This block is crucial for Certbot. It instructs Nginx to serve files from the /var/www/certbot directory whenever a request comes in for the /.well-known/acme-challenge/ path. This allows Certbot to place temporary files here to prove domain ownership to Let’s Encrypt.
    • location / { return 301 https://$host$request_uri; }: This is a fundamental security best practice. It ensures that all unencrypted HTTP traffic arriving on port 80 is automatically and permanently redirected (301 Moved Permanently) to the secure HTTPS equivalent.
  • HTTPS Server Block (listen 443 ssl http2):
    • ssl_certificate and ssl_certificate_key: These directives point to the full certificate chain and the private key, respectively. These files will be generated by Certbot and stored in a shared Docker volume, making them accessible to Nginx.
    • ssl_* directives: These lines define robust SSL/TLS security settings:
      • ssl_session_cache, ssl_session_timeout: Optimize SSL session handling for performance.
      • ssl_protocols TLSv1.2 TLSv1.3: Restrict Nginx to only use modern and secure TLS protocols, disabling older, vulnerable versions.
      • ssl_prefer_server_ciphers on, ssl_ciphers, ssl_ecdh_curve: Prioritize strong, secure cryptographic ciphers and elliptic curves.
      • ssl_stapling on, ssl_stapling_verify on: Enable OCSP stapling, which improves SSL handshake performance and privacy by allowing the server to provide certificate revocation status directly.
    • add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";: This sets the HSTS header. HSTS tells compliant browsers to only connect to your domain over HTTPS for the specified duration (two years in this case), even if a user types http://. This is a powerful defense against downgrade attacks.
    • add_header X-Frame-Options DENY;, add_header X-Content-Type-Options nosniff;: These are additional security headers to mitigate common web vulnerabilities like clickjacking and MIME-sniffing.
    • root /usr/share/nginx/html;: This specifies the directory from which Nginx will serve our static files when accessed over HTTPS.
    • index index.html index.htm;: Defines the default files Nginx should look for if a directory is requested (e.g., https://yourdomain.com/).

5. Configure Docker Compose

Now, let’s define our nginx and certbot services, along with their shared networks and volumes, using a docker-compose.yml file. This file orchestrates our multi-container application.

File: secure-static-site/docker-compose.yml

# Specify the Docker Compose file format version.
# Version '3.8' is a recent and widely supported standard as of 2026-08-06.
# Docker Compose version 2.40.3 is current as of 2026-08-06.
version: '3.8'

services:
  nginx:
    # Build the Nginx image from the Dockerfile located in the './nginx' directory.
    build:
      context: ./nginx
      dockerfile: Dockerfile
    container_name: nginx # Assign a memorable name to the Nginx container.
    restart: unless-stopped # Automatically restart the container if it stops, unless explicitly stopped.
    
    # Map host ports to container ports.
    # "80:80" maps host port 80 to container port 80.
    # "443:443" maps host port 443 to container port 443.
    ports:
      - "80:80"
      - "443:443"
    
    # Define volumes for data persistence and sharing.
    volumes:
      - static_www:/usr/share/nginx/html:ro  # Mount static content as read-only.
      - nginx_conf:/etc/nginx/conf.d:ro     # Mount Nginx configuration as read-only.
      - certbot_certs:/etc/letsencrypt      # Mount Certbot certificates (read-write for Certbot, read-only for Nginx).
      - certbot_www:/var/www/certbot        # Mount Certbot challenge files (read-write for Certbot, read-only for Nginx).
    
    # Pass the DOMAIN environment variable from the .env file into the container.
    environment:
      - DOMAIN=${DOMAIN}
    
    # Nginx needs certificates, so it logically depends on Certbot.
    # However, for initial certificate generation, Certbot needs Nginx to serve the challenge.
    # We'll manage this "chicken-and-egg" scenario with specific commands later.
    depends_on:
      - certbot
    
    # Connect Nginx to our custom 'webnet' network for inter-container communication.
    networks:
      - webnet

  certbot:
    # Use the official Certbot Docker image. Pinning to a specific version (v2.10.0)
    # ensures reproducibility and avoids unexpected changes from 'latest'.
    # Certbot version v2.10.0 is current as of 2026-08-06.
    image: certbot/certbot:v2.10.0
    container_name: certbot # Assign a memorable name to the Certbot container.
    restart: unless-stopped # Automatically restart the container if it stops.
    
    # Define volumes for Certbot to store certificates and handle challenges.
    # These volumes are shared with the Nginx container.
    volumes:
      - certbot_certs:/etc/letsencrypt
      - certbot_www:/var/www/certbot
    
    # Define the entrypoint for Certbot. This script runs 'certbot renew' every 12 hours.
    # 'trap exit TERM; while :; do ...' ensures the script gracefully handles termination signals,
    # and 'sleep 12h & wait $!' allows the sleep to be interrupted for faster shutdown.
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $!; done;'"
    
    # Pass the DOMAIN environment variable to the Certbot container.
    environment:
      - DOMAIN=${DOMAIN}
    
    # Connect Certbot to our custom 'webnet' network.
    networks:
      - webnet

# Define custom networks. 'webnet' is a bridge network, ideal for communication
# between services on the same Docker host.
networks:
  webnet:
    driver: bridge

# Define named volumes for persistent data storage.
# Named volumes are the recommended way to manage data in Docker,
# as they are managed by Docker and easier to back up.
volumes:
  static_www: # Stores our static website files.
  nginx_conf: # Stores our Nginx configuration files.
  certbot_certs: # Stores SSL certificates and keys generated by Certbot.
  certbot_www: # Stores temporary files for Certbot's ACME challenges.

Explanation of docker-compose.yml:

  • version: '3.8': Specifies the Compose file format version. Using a specific version (like 3.8) ensures compatibility and predictable behavior.
  • nginx service:
    • build: ./nginx: Instructs Docker Compose to build the Nginx image using the Dockerfile found in the ./nginx directory.
    • container_name: nginx: Assigns a fixed, human-readable name to the container, making it easier to manage.
    • restart: unless-stopped: This policy ensures that the Nginx container automatically restarts if it crashes or if the Docker daemon itself restarts, enhancing uptime.
    • ports: - "80:80" - "443:443": These lines map host machine ports 80 and 443 to the Nginx container’s internal ports 80 and 443, respectively. This makes your web server accessible from outside the Docker host.
    • volumes:
      • static_www:/usr/share/nginx/html:ro: Mounts a named volume called static_www to the Nginx container’s /usr/share/nginx/html directory. The :ro (read-only) flag is a security measure, preventing Nginx from accidentally modifying your static content.
      • nginx_conf:/etc/nginx/conf.d:ro: Mounts the nginx_conf volume to hold our Nginx configuration, also read-only.
      • certbot_certs:/etc/letsencrypt: This volume is shared with Certbot and will store the SSL certificates. Nginx reads from this.
      • certbot_www:/var/www/certbot: This volume is also shared with Certbot and is used for the ACME challenge files.
    • environment: - DOMAIN=${DOMAIN}: Passes the value of the DOMAIN variable (defined in our .env file) into the Nginx container as an environment variable. Nginx uses this in its configuration.
    • depends_on: - certbot: This tells Docker Compose that the nginx service depends on the certbot service. While depends_on ensures certbot is started before nginx, it doesn’t wait for Certbot to actually generate certificates. We’ll address this initial certificate generation process manually.
    • networks: - webnet: Connects the nginx container to our custom webnet bridge network.
  • certbot service:
    • image: certbot/certbot:v2.10.0: Uses the official Certbot Docker image. Pinning to a specific version (v2.10.0 as of 2026-08-06) is critical for consistent and reproducible deployments.
    • volumes: Shares the same certbot_certs and certbot_www volumes with Nginx. This is how Certbot places certificates for Nginx to use and receives challenge requests.
    • entrypoint: This is a custom shell command that instructs the Certbot container to run certbot renew every 12 hours. Let’s Encrypt certificates are valid for 90 days, so this frequent check ensures renewal well before expiration. The trap exit TERM; ... & wait $! pattern handles graceful shutdown.
    • environment: - DOMAIN=${DOMAIN}: Passes the domain name to Certbot.
    • networks: - webnet: Connects Certbot to the webnet network.
  • networks: Defines our custom bridge network named webnet. This provides a private, isolated network for our containers to communicate with each other securely using their service names (e.g., nginx can talk to certbot by hostname).
  • volumes: Declares the named volumes used by our services. Named volumes are the preferred method for persistent data storage in Docker as they are managed by Docker and are easier to back up and restore than host-mounted bind mounts.

6. Set Environment Variables

Create a .env file in the secure-static-site root directory to store your domain name. This keeps sensitive or environment-specific values out of your docker-compose.yml.

CRITICAL: Replace yourdomain.com with your actual domain name. You must own this domain and point its DNS A record to your Docker host’s public IP address. Without correct DNS, Certbot cannot verify domain ownership, and your site will not be accessible.

File: secure-static-site/.env

DOMAIN=yourdomain.com

7. Generate Initial Certificates

This is a crucial step that resolves the “chicken-and-egg” problem: Nginx needs certificates to run HTTPS, but Certbot needs Nginx to be running (and serving the .well-known path) to obtain those certificates. We’ll temporarily configure Nginx to serve the challenge, get the certificates, then revert Nginx to its full HTTPS configuration.

Step 7.1: Temporarily Modify Nginx Configuration First, we need to modify secure-static-site/nginx/conf.d/default.conf to serve the Certbot challenge directly on port 80 without attempting to redirect to HTTPS, and without requiring SSL certificates.

IMPORTANT: After successfully obtaining certificates, you will revert this file to the original default.conf content shown in Step 4.

Temporary secure-static-site/nginx/conf.d/default.conf (for initial cert generation):

server {
    listen 80;
    listen [::]:80;

    server_name ${DOMAIN};

    # This block is essential for Certbot to verify domain ownership.
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    # During initial certificate generation, we serve static files directly on HTTP.
    # The HTTP-to-HTTPS redirect is disabled temporarily.
    location / {
        root /usr/share/nginx/html; # Serve static files directly
        index index.html index.htm;
    }
}

Step 7.2: Build and Start Nginx (Temporarily)

Now, build the Nginx image with this temporary configuration and start only the Nginx service.

docker compose build nginx
docker compose up -d nginx

Step 7.3: Request Certificates with Certbot

Once Nginx is running and serving the challenge path, request the certificates using Certbot.

CRITICAL: Ensure your domain’s DNS A record points to your server’s public IP address before running this command.

docker compose run --rm certbot certonly --webroot --webroot-path=/var/www/certbot -d ${DOMAIN} --email [email protected] --agree-tos --no-eff-email

Explanation of the Certbot command:

  • docker compose run --rm certbot: This executes the certbot service as a one-off command. The --rm flag ensures the container is removed immediately after it finishes, keeping your system clean.
  • certonly: This option tells Certbot to only obtain certificates and not to install them into a web server configuration (as Nginx is already configured).
  • --webroot: Specifies that Certbot should use the webroot plugin, which places challenge response files in a designated directory.
  • --webroot-path=/var/www/certbot: This specifies the directory inside the Certbot container where challenge files will be placed. This path corresponds to the shared certbot_www volume, which Nginx is configured to serve.
  • -d ${DOMAIN}: Specifies the domain for which you are requesting certificates. Certbot will read this from your .env file.
  • --email [email protected]: Provides your email address for urgent notices from Let’s Encrypt regarding certificate expiration or issues.
  • --agree-tos: Automatically agrees to Let’s Encrypt’s Terms of Service.
  • --no-eff-email: Opts out of sharing your email with the Electronic Frontier Foundation (EFF).

If successful, you will see a message similar to: Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/yourdomain.com/fullchain.pem.

Step 7.4: Stop Nginx and Revert Configuration

Now that certificates are obtained, stop the temporarily running Nginx container.

docker compose stop nginx

CRITICAL: Revert the secure-static-site/nginx/conf.d/default.conf file to its original content (the one with both HTTP and HTTPS server blocks, including the HTTP-to-HTTPS redirect, as shown in Step 4). This is vital for Nginx to serve HTTPS and automatically redirect HTTP traffic.

8. Deploy Final Configuration

With the certificates in place and the Nginx configuration reverted to handle HTTPS and redirects, we can now start both services together.

docker compose up -d

This command will bring up both the nginx and certbot containers in detached mode (-d). Nginx will now correctly use the newly acquired SSL certificates and enforce HTTPS redirects. The Certbot container will run in the background, automatically renewing certificates before they expire, ensuring your site remains secure.

Testing & Verification

It’s crucial to verify that everything is working as expected after deployment.

  1. Check Container Status: Confirm both containers are running.

    docker compose ps

    You should see both nginx and certbot containers listed with a running status.

  2. Access the Website (HTTP and HTTPS):

    • Open your web browser and navigate to http://yourdomain.com. You should observe an automatic redirection to https://yourdomain.com.
    • Navigate directly to https://yourdomain.com.
    • Verify Secure Connection: Look for a padlock icon in your browser’s address bar. Click on it to inspect the certificate details. It should be issued by “Let’s Encrypt” and valid for your domain.
  3. Inspect Nginx Logs: Review Nginx logs to confirm traffic is being handled correctly.

    docker compose logs nginx

    You should see Nginx access logs for your requests. Look for 301 status codes for successful HTTP-to-HTTPS redirects and 200 status codes for successful HTTPS requests serving your index.html.

  4. Inspect Certbot Logs (Optional): While Certbot mostly runs in the background for renewals, you can check its logs for initial setup messages and renewal attempts.

    docker compose logs certbot

    You’ll see output from Certbot’s initial run confirming certificate acquisition and subsequent messages indicating it’s waiting for renewal schedules.

Production Considerations

When deploying to a production environment, several factors beyond basic functionality become critical for reliability, security, and maintainability.

  • Non-Root User: Our Dockerfile copies static content with chown=nginx:nginx, and the official Nginx image runs Nginx processes as a non-root user by default. This is a fundamental security best practice, limiting potential damage if the Nginx process were compromised.
  • Resource Limits: To prevent a single container from monopolizing host resources, define CPU and memory limits in your docker-compose.yml. This is vital for host stability and multi-service deployments.
    services:
      nginx:
        # ... existing config ...
        deploy:
          resources:
            limits:
              cpus: '0.5' # Limit to 50% of one CPU core
              memory: 128M # Limit to 128 MB RAM
            reservations:
              cpus: '0.1' # Reserve 10% of one CPU core
              memory: 32M # Reserve 32 MB RAM
    limits define the maximum, while reservations guarantee a minimum amount of resources.
  • Firewall Configuration: Ensure your host machine’s firewall (e.g., ufw on Linux, Windows Firewall) explicitly allows incoming traffic on TCP ports 80 (HTTP) and 443 (HTTPS). Without these ports open, users cannot reach your website.
  • DNS Configuration: Verify that your domain’s A record (and AAAA record for IPv6) correctly points to the public IP address of your Docker host. This is non-negotiable for Certbot validation and user access.
  • Automated Renewal: The Certbot container’s entrypoint ensures certificates are renewed every 12 hours. Let’s Encrypt certificates are valid for 90 days, so this frequent check provides ample buffer against expiration, preventing service outages.
  • Backup Volumes: For real production systems, regularly back up your certbot_certs volume. This volume contains your SSL certificates and private keys. Losing them would necessitate re-issuing certificates and potential downtime.

Common Issues & Solutions

Even with careful planning, issues can arise. Here are some common problems and their solutions:

  • Certbot Challenge Failure:
    • Issue: Certbot fails with an error message like “Failed to connect to host” or “Invalid response from [yourdomain.com]”.
    • Solution:
      1. DNS Check: Confirm your DOMAIN’s A record (and AAAA record if applicable) correctly points to your server’s public IP address. Use dig +short yourdomain.com or nslookup yourdomain.com to verify.
      2. Firewall: Ensure TCP ports 80 and 443 are open on your server’s firewall. Let’s Encrypt must be able to reach your server on port 80 for the HTTP-01 challenge.
      3. Nginx Config (Temporary): Double-check that the temporary Nginx configuration (used for initial cert generation) is correctly configured to serve files from /var/www/certbot for the /.well-known/acme-challenge/ path.
      4. Nginx Running: Make sure the nginx service is actually running when you execute the certbot certonly command (docker compose ps should show it as running).
  • Nginx Fails to Start with HTTPS:
    • Issue: The Nginx container exits immediately or its logs show errors like “No such file or directory” related to SSL certificate paths (fullchain.pem, privkey.pem).
    • Solution: This almost always means Certbot did not successfully generate the certificates, or Nginx’s configuration points to incorrect paths. Carefully re-run the Certbot initial generation step. Verify that the paths specified in default.conf for ssl_certificate and ssl_certificate_key exactly match where Certbot saves certificates (/etc/letsencrypt/live/${DOMAIN}/). Ensure you reverted to the correct default.conf after generating certificates.
  • Port Conflicts:
    • Issue: docker compose up fails with an error such as “port is already allocated” or “Bind for 0.0.0.0:80 failed: port is already in use”.
    • Solution: Another process on your host machine is already using port 80 or 443. This could be another web server (e.g., Apache, a system Nginx instance) or a previous Docker container that wasn’t properly shut down. Stop any conflicting services or change the port mapping in your docker-compose.yml (e.g., change "80:80" to "8080:80" if you want to access it on http://yourdomain.com:8080).

Summary & Next Step

Congratulations! You’ve successfully designed, implemented, and deployed a secure static website using Nginx, Certbot, and Docker Compose. This project provided hands-on experience with several critical concepts:

  • Building custom Docker images and understanding the benefits of minimal base images.
  • Orchestrating multiple interdependent services using docker-compose.yml.
  • Managing persistent data and configuration through Docker volumes.
  • Implementing HTTPS with automated SSL certificate acquisition and renewal from Let’s Encrypt.
  • Applying basic container security principles and considering production deployment aspects like resource limits and firewalls.

This project establishes a solid, reusable pattern for serving static content and securing web services, forming a foundational piece for more complex architectures.

Next, we’ll dive into building a more intricate full-stack web application. This will introduce database services, demonstrate more advanced inter-service communication, and further refine your Docker Compose skills for multi-tier applications.


This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.

References