Hosting a website on Pi using caddy

Caddy is a modern, open-source web server written in Go, known for its automatic HTTPS, simple configuration, and powerful reverse proxy capabilities. It’s designed to be developer-friendly and production-ready out of the box.

πŸ” Key Features

πŸš€ Docker Installation Steps

  1. Update your system
    sudo apt update && sudo apt upgrade -y
  2. Install Docker
    curl -fsSL https://get.docker.com -o get-docker.sh
    sudo sh get-docker.sh 
  3. Install Docker Compose
    sudo apt install docker-compose-plugin
  4. Add your user to the Docker group
    sudo usermod -aG docker $USER
  5. Enable Docker at boot
    sudo systemctl enable docker
  6. Reboot
    sudo reboot
  7. Test Docker
    docker run hello-world

πŸ“ Setup Caddy

This guide will walk you through hosting two seperate websites.

mkdir ~/docker
mkdir ~/docker/caddy
mkdir ~/docker/caddy/website1  # This directory will hold all the files for website 1
mkdir ~/docker/caddy/website2  # This directory will hold all the files for website 2 

🧾 Docker Compose File

In ~/docker/caddy create docker-compose.yml I setup two websites. You can strip this down to one if you'd like. Or add more.

 services:
  caddy:
    image: caddy
    container_name: caddy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./website1:/srv/website1                     # Content for the first website
      - ./website2:/srv/website2                     # Content for the second website
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    restart: unless-stopped

    volumes:
      caddy_data:
      caddy_config: 

In ~/docker/caddy create Dockerfile

FROM caddy:builder AS builder

RUN xcaddy build \
    --with github.com/caddy-dns/cloudflare

FROM caddy:latest

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

CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]

In ~/docker/caddy create Caddyfile

website1.com {
  root * /srv/website1
  file_server
  encode gzip
  tls {
    issuer acme {
      dns cloudflare {
        api_token "API-Token-from-Cloudflare"
      }
    }
  }
}

website2.com {
  root * /srv/website2
  file_server
  encode gzip
  tls {
    issuer acme {
      dns cloudflare {
        api_token "API-Token-from-Cloudflare"
      }
    }
  }
}

Clouflare

I use Cloudflare so I'll walk through that. I also assume that you've secured two domains: website1.com and website2.com

  1. Go to cloudflare.com and create a free account
  2. Click on "Onboard a Domain".
  3. Enter your domain (ex. website1.com) and click "Continue".
  4. Select the Free plan.
  5. Click on "DNS Record" and add a type "A" record. "Name" is your domain (ex. website1.com). "IPv4 address" is your public facing IP address. You can get this by running curl ifconfig.me.
  6. Now generate the API Token. Do this by clicking on "DNS" for website1.com under "Recents" and then clicking on "Get your API Token". Or you can go here: https://dash.cloudflare.com/profile/api-tokens Click on "Create Token" and click on "Use template" next to "Edit one DNS". Under "Permissions", select "Zone", "DNS", and "Edit". Under "Zone Resources", select "Include", "Specific Zone" and selecty your domain (ex. website1.com). Click on "Continue to summary" and "Create Token". Copy your token. That page will also give you a command that you can run on your pi to test it. Add the token you the Caddyfile.
  7. Repeat these steps for your second domain

Since your IP address can change, it's important to update Cloudflare periodically

  1. In ~/docker/caddy create update_website1.sh
  2. Paste this into it:
    #!/usr/bin/env python3
    import requests
    
    # === CONFIGURATION ===
    ZONE_NAME = "website1.com"
    RECORD_NAME = "website1.com"
    API_TOKEN = "APT-TOKEN-FROM-CLOUDFLARE"
    
    # === FUNCTIONS ===
    def get_public_ip():
        return requests.get("https://api.ipify.org").text.strip()
    
    def get_zone_id():
        headers = {"Authorization": f"Bearer {API_TOKEN}"}
        r = requests.get("https://api.cloudflare.com/client/v4/zones", headers=headers)
        zones = r.json()["result"]
        for zone in zones:
            if zone["name"] == ZONE_NAME:
                return zone["id"]
        raise Exception("Zone not found")
    
    def get_record_id(zone_id):
        headers = {"Authorization": f"Bearer {API_TOKEN}"}
        url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records"
        r = requests.get(url, headers=headers)
        records = r.json()["result"]
        for record in records:
            if record["name"] == RECORD_NAME and record["type"] == "A":
                return record["id"]
        raise Exception("A record not found")
    
    def update_dns_record(zone_id, record_id, ip):
        headers = {
            "Authorization": f"Bearer {API_TOKEN}",
            "Content-Type": "application/json"
        }
        url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{record_id}"
        data = {
            "type": "A",
            "name": RECORD_NAME,
            "content": ip,
            "ttl": 300,
            "proxied": False
        }
        r = requests.put(url, headers=headers, json=data)
        return r.json()
    
    # === MAIN EXECUTION ===
    if __name__ == "__main__":
        try:
            ip = get_public_ip()
            zone_id = get_zone_id()
            record_id = get_record_id(zone_id)
            result = update_dns_record(zone_id, record_id, ip)
            print(f"[βœ“] Updated {RECORD_NAME} to {ip}")
            print("Cloudflare response:", result)
        except Exception as e:
            print(f"[βœ—] Error: {e}")
    
  3. If you don't have Python, install it
  4. Add it to cron using crontab -e
  5. */10 * * * * /yourdirectory/update_website1.sh >> /yourdirectory/website1-ddns.log 2>&1
  6. Repeat these steps for website2.com

Create a very simple web page to test

  • In ~docker/caddy/website1 create index.html and paste this into it
  • 
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Website1<title>
    <head>
    <body>
      <h1>This is website1<h1>
    <body>
    <html>
    
  • In ~docker/caddy/website2 create index.html and paste this into it
  • 
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Website2<title>
    <head>
    <body>
      <h1>This is website1<h1>
    <body>
    <html>
    

    Build and start caddy

    From within ~/docker/caddy run:

    
    cd ~docker/caddy
    docker compose  build
    docker compose up -d
    

    πŸ“œ View Logs

    docker logs caddy

    🌐 Access your website

    Open your browser and go to website1.com and then try website2.com

    If this doesn't work or you're having difficulty, feel free to contact me. Also, if you find any errors, or ways to imporove on this, let me know.This is fairly complicated and there are a lot of moving parts.

    Add analytics

    Now that your website is up, you can add analytics to it by installing kaunta . You can also use Google Analytics.

    If you find my content useful, please consider supporting this page:

    β˜• Buy Me a Coffee