Zero-Config Wildcard Static Site Hosting with Docker, Traefik & Nginx

Created: 2026-02-23 18:20:58 | Last updated: 2026-02-23 18:20:58 | Status: Public

Drop a folder, get a site. This guide sets up automatic subdomain routing so that any folder you create instantly becomes a live HTTPS website. No config changes, no container restarts, no DNS updates.

myproject/index.html -> https://myproject.yourdomain.com - automatically.


What You Get

  • Create a folder with an index.html - it’s live at foldername.yourdomain.com
  • Delete the folder - it’s gone
  • Wildcard TLS certs via Let’s Encrypt + Cloudflare - every subdomain gets HTTPS
  • Existing named services (Jellyfin, SearXNG, etc.) keep working untouched
  • Total cost: one cheap VPS + a domain

Prerequisites

  • A VPS or server with Docker and Docker Compose installed
  • A domain with DNS managed by Cloudflare
  • A Cloudflare API token with DNS edit permissions
  • Basic comfort with SSH and the terminal

Step 1: Set Up Cloudflare DNS

In your Cloudflare dashboard for your domain:

  1. Add an A record for yourdomain.com pointing to your server IP
  2. Add an A record for *.yourdomain.com (literally type the asterisk) pointing to the same IP
  3. Set both to DNS only (gray cloud) - Traefik handles TLS, not Cloudflare

Get Your Cloudflare API Token

  1. Go to My Profile > API Tokens > Create Token
  2. Use the Edit zone DNS template
  3. Scope it to your specific zone/domain
  4. Save the token - you need it in Step 2

Step 2: Deploy Traefik

Create a directory on your server:

mkdir -p /opt/traefik && cd /opt/traefik
touch acme.json && chmod 600 acme.json

Create /opt/traefik/docker-compose.yml:

version: '3.8'
services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: unless-stopped
    command:
      - --api.dashboard=true
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --certificatesresolvers.cloudflare.acme.dnschallenge=true
      - --certificatesresolvers.cloudflare.acme.dnschallenge.provider=cloudflare
      - --certificatesresolvers.cloudflare.acme.email=you@yourdomain.com
      - --certificatesresolvers.cloudflare.acme.storage=/acme.json
    environment:
      - CF_API_EMAIL=you@yourdomain.com
      - CF_DNS_API_TOKEN=your-cloudflare-api-token-here
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./acme.json:/acme.json
    networks:
      - traefik

networks:
  traefik:
    name: traefik

Replace you@yourdomain.com and the API token with your actual values.

docker compose up -d

Verify Traefik is running:

docker logs traefik

You should see it start up with no errors. Give it a minute to request certs.


Step 3: Create the Sites Directory

mkdir -p /srv/sites

This is where all your site folders will live. Each subfolder becomes a subdomain.


Step 4: Deploy the Nginx Wildcard Config

Create the nginx config at /srv/sites/nginx-wildcard.conf:

server {
    listen 80;
    server_name ~^(?<subdomain>[a-z0-9-]+)\.yourdomain\.com$;

    root /sites/$subdomain;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    # Block dotfiles
    location ~ /\. {
        deny all;
    }

    error_page 404 /404.html;
    location = /404.html {
        root /sites;
        internal;
    }
}

server {
    listen 80 default_server;
    server_name _;
    return 404;
}

Replace yourdomain\.com with your actual domain (keep the backslash-escaped dots).


Step 5: Deploy the Wildcard Sites Stack

Create /opt/wildcard-sites/docker-compose.yml:

version: '3.8'
services:
  wildcard-sites:
    image: nginx:alpine
    container_name: wildcard-sites
    restart: unless-stopped
    volumes:
      - type: bind
        source: /srv/sites
        target: /sites
        read_only: true
      - type: bind
        source: /srv/sites/nginx-wildcard.conf
        target: /etc/nginx/conf.d/default.conf
        read_only: true
    networks:
      - traefik
    labels:
      - "traefik.enable=true"

      # Wildcard HTTPS router - low priority so named services win
      - "traefik.http.routers.wildcard-sites.rule=HostRegexp(`[a-z0-9-]+\\.yourdomain\\.com`)"
      - "traefik.http.routers.wildcard-sites.priority=1"
      - "traefik.http.routers.wildcard-sites.entrypoints=websecure"
      - "traefik.http.routers.wildcard-sites.tls.certresolver=cloudflare"
      - "traefik.http.routers.wildcard-sites.tls.domains[0].main=yourdomain.com"
      - "traefik.http.routers.wildcard-sites.tls.domains[0].sans=*.yourdomain.com"
      - "traefik.http.services.wildcard-sites.loadbalancer.server.port=80"

      # HTTP to HTTPS redirect
      - "traefik.http.routers.wildcard-sites-http.entrypoints=web"
      - "traefik.http.routers.wildcard-sites-http.rule=HostRegexp(`[a-z0-9-]+\\.yourdomain\\.com`)"
      - "traefik.http.routers.wildcard-sites-http.priority=1"
      - "traefik.http.routers.wildcard-sites-http.middlewares=wildcard-https-redirect"
      - "traefik.http.middlewares.wildcard-https-redirect.redirectscheme.scheme=https"
      - "traefik.http.middlewares.wildcard-https-redirect.redirectscheme.permanent=true"

      # Rate limiting
      - "traefik.http.middlewares.wildcard-ratelimit.ratelimit.burst=100"
      - "traefik.http.middlewares.wildcard-ratelimit.ratelimit.average=50"
      - "traefik.http.routers.wildcard-sites.middlewares=wildcard-ratelimit"

networks:
  traefik:
    external: true

Replace all instances of yourdomain.com with your actual domain.

cd /opt/wildcard-sites && docker compose up -d

Step 6: Test It

mkdir -p /srv/sites/hello
echo "<h1>it works</h1>" > /srv/sites/hello/index.html

Visit https://hello.yourdomain.com in your browser. You should see “it works” with a valid HTTPS cert.


Usage

From this point on, the workflow is:

# Create a site
mkdir /srv/sites/myproject
# Put files in it (copy, rsync, sftp, whatever)
cp -r ~/myproject/* /srv/sites/myproject/
# It's live at https://myproject.yourdomain.com

# Remove a site
rm -rf /srv/sites/myproject
# It's gone

No restarts. No config changes. No DNS updates.


Portainer Users

If you manage your stacks through Portainer, paste the Traefik compose and the wildcard-sites compose as two separate stacks. Everything else is the same. Just make sure your sites directory path matches wherever your Portainer host stores files.


How It Works

The architecture is three layers:

  1. Cloudflare DNS - the wildcard A record sends ALL subdomains to your server
  2. Traefik - catches incoming requests, terminates TLS with wildcard certs, routes based on labels. Your named services (Jellyfin, SearXNG, etc.) have higher priority and match first. Everything else falls through to the wildcard router.
  3. Nginx - extracts the subdomain from the Host header, maps it to a folder name, serves the files. Folder exists? Serve it. Folder missing? 404.

The priority=1 on the wildcard Traefik router is the key detail. Traefik calculates priority from rule length by default, so any explicitly named Host() rule will always be longer (and therefore higher priority) than the wildcard regex. Setting priority=1 makes this explicit and bulletproof.


Folder Naming Rules

  • Lowercase letters, numbers, and hyphens only: my-project-1 works, My_Project does not
  • No underscores, no uppercase, no spaces, no dots
  • The folder name IS the subdomain

Security Notes

  • All sites are served read-only from the mount - nginx cannot write to your files
  • Dotfiles (.git, .env, etc.) are blocked by the nginx config
  • Rate limiting is applied globally to all wildcard sites
  • Use SFTP or rsync over SSH to upload files - never FTP
  • If you need per-site auth, you’d need to add that as a separate Traefik middleware

Troubleshooting

Site shows 404 but folder exists: Check folder name matches the rules above. Check that index.html exists in the folder root (not in a subfolder).

Cert errors: Give Traefik a few minutes after first deploy. Check docker logs traefik for ACME errors. Verify your Cloudflare API token has DNS edit permissions.

Existing services stop working: The priority=1 should prevent this. If it happens, check that your other stacks have explicit Host() rules (they do based on your current configs).

Changes not appearing: Nginx caches aggressively by default. Hard refresh (Ctrl+Shift+R) or add cache-control headers to your nginx config if needed.


Credits

Built on Traefik v3, Nginx, Docker, and Cloudflare. This is a self-hosted static site platform in about 50 lines of config.