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 atfoldername.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:
- Add an A record for
yourdomain.compointing to your server IP - Add an A record for
*.yourdomain.com(literally type the asterisk) pointing to the same IP - Set both to DNS only (gray cloud) - Traefik handles TLS, not Cloudflare
Get Your Cloudflare API Token
- Go to My Profile > API Tokens > Create Token
- Use the Edit zone DNS template
- Scope it to your specific zone/domain
- 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:
- Cloudflare DNS - the wildcard A record sends ALL subdomains to your server
- 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.
- 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-1works,My_Projectdoes 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.