Was this helpful? Support me via buymeacoffee.com and help me create lots more great content!

Wildcard SSLs on EC2 using Caddy, Docker AWS Route53

Assumptions / Pre-requisites

I'm assuming you have

  • Created a clean EC2 instance running Ubuntu 24.04 with a public IP address
  • Created a hosted zone in AWS Route53 for your domain name
  • Configured your domain to use the name servers provided in the AWS Route53 zone file
  • Added DNS entries for the apex domain and wildcard subdomain to point to your EC2 instance public IP address. For this post we'll assume we're setting up for the domain supercoolwidgets.xyz and the wildcard subdomain of *.supercoolwidgets.xyz

Setting up your server

Connect to your EC2 instance via its public IP address

Configure UFW to allow port 22, 80 & 443

# Add rules
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https

# Check what's configured
sudo ufw show added

# Enable the firewall
sudo ufw enable

Install Docker

Set up Docker's apt repository.

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

Install docker packages

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Ref: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository

AWS Permissions

Permission Policy

Create a permission policy (named webserver-caddy-route53) to allow Caddy (/your EC2 instance) to update DNS records (required for wildcard DNS)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "route53:ListResourceRecordSets",
                "route53:GetChange",
                "route53:ChangeResourceRecordSets"
            ],
            "Resource": [
                "arn:aws:route53:::hostedzone/... INSERT HOSTED ZONE ID HERE ...",
                "arn:aws:route53:::change/*"
            ]
        },
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "route53:ListHostedZonesByName",
                "route53:ListHostedZones"
            ],
            "Resource": "*"
        }
    ]
}json

IAM Role

Create a role (named webserver-role) and add the permission policy you just created above.

Edit the Trust Policy for your newly created webserver-role as per the example below, substituting your role ARN in the AWS section below. The role ARN should be at the top of the page.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "... INSERT ROLE ARN HERE ...",
                "Service": "ec2.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

EC2

Amend your EC2 instance and assign your webserver-role as the IAM Role.

Setup your site

Project Folder

Create the folder to house your site

mkdir /opt/sites/supercoolwidgets.xyz

then go into this directory

cd /opt/sites/supercoolwidgets.xyz

Add a Caddy configuration file (Caddyfile)

{
    email your@email.address
}

supercoolwidgets.xyz, *.supercoolwidgets.xyz {
    tls {
        dns route53 {
            profile "default"
        }
    }
    root * /app/public

    # Handle static files directly
    file_server

    # Symfony specific rewrites
    try_files {path} {path}/ /index.html
}

Add an AWS config file (.aws/config)

[profile default]
role_arn = ... INSERT ROLE ARN HERE ...
credential_source = Ec2InstanceMetadata
region = eu-west-2

Create a hello world page (public/index.html)

<html>
<body>
Hello World!
</body>
</html>

Folder structure

You should now have the following folder structure

|- .aws
  |- config
|- Caddyfile
|- compose.yaml
|- public
  |- index.html

Build the Caddy docker image with Route53 plugin

Add a docker file with the following contents

FROM caddy:2.9.1-builder-alpine AS caddy-builder

RUN xcaddy build v2.9.1 \
    --with github.com/caddy-dns/route53@v1.5.1

FROM caddy:2.9.1-alpine AS app-caddy

COPY --from=caddy-builder /usr/bin/caddy /usr/bin/caddy
COPY Caddyfile /etc/caddy

mkdir /app/public
COPY public/ /app/public

Build the docker image and tag it with website:1.0.0

docker build -t website:1.0.0 .

Create a Docker compose file (compose.yaml)

name: website_prod

services:
  caddy:
    restart: unless-stopped
    image: website:1.0.0
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./.aws:/root/.aws:ro
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:

Start your website

docker compose up -d

You should now be able to open the site in a browser and have it show the hello world! page, complete with a valid SSL certificate.

Note: The first time you open your site it will likely return an SSL error as it takes several seconds to get the SSL certificate initially. You can see what's happening by tailing the logs of your caddy container i.e.

docker logs -t ... CADDY CONTAINER NAME HERE ...

Originally published at https://chrisshennan.com/blog/wildcard-ssls-on-ec2-using-caddy-docker-aws-route53