58.7 Project 23: Traefik multi-site host with automatic HTTPS (many apps on one EC2)
Why: the problem
Suppose we are a small agency in Pune. We build websites for five shops, like a bakery and a gym. Each site is small. Paying for five servers is a waste. So we put all five sites on one EC2 server. But only one program can listen on port 80 and port 443. Who decides which site gets a request? And every site needs HTTPS. Buying and renewing certificates by hand for five domains is slow, and we forget the renewal date. Traefik solves both. It is a reverse proxy made for containers. It watches Docker. When we start a container with a few labels like "my domain is bakery.example.com", Traefik routes that domain to it. It also asks Let's Encrypt for a free certificate and renews it before it expires. No nginx file to edit, no reload. To add a sixth site, we start one more container with labels. That is all.
What: every term in plain English
- Reverse proxy — One program in front that receives all requests and sends each to the right app.
- Traefik — A reverse proxy that reads Docker labels and configures itself. Used by many teams on single servers and in Kubernetes.
- Entrypoint — A port Traefik listens on, such as web (80) and websecure (443).
- Router — A rule like Host(
bakery.example.com) that matches requests. - Service — Where the router sends traffic: a container and its port.
- Middleware — A step between router and service: redirect to HTTPS, basic auth, rate limit, headers.
- Certificate resolver — Traefik's setting for getting certificates from Let's Encrypt with the ACME protocol.
- acme.json — The file where Traefik saves certificates. It must be kept on a volume.
How: step by step
- Point DNS to the server. A records for each domain go to the EC2 Elastic IP.
- Start Traefik. One compose file with ports 80 and 443 and the Docker socket read-only.
- Start sites with labels. Each site has its own compose file and joins the shared network edge.
- Check HTTPS. Traefik gets certificates on the first request.
- Add middlewares. HTTPS redirect, basic auth for the dashboard, security headers, rate limit.
- Clean up. Stop the stacks, delete volumes, release the Elastic IP.
Architecture
Folder layout
server/
edge/compose.yaml edge/.env.example
bakery/compose.yaml bakery/html/index.html
gym/compose.yaml gym/.env.example
blog/compose.yaml
local/compose.yaml (laptop practice with *.localhost)
ops/certs.sh ops/cleanup.sh
Example 1: Traefik itself: one container that owns ports 80 and 443 and reads Docker labels
Goal. Traefik itself: one container that owns ports 80 and 443 and reads Docker labels.
edge/compose.yaml
services:
traefik:
image: traefik:v3.1
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=edge
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --certificatesresolvers.le.acme.email=admin@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
- --api.dashboard=true
- --accesslog=true
- --log.level=INFO
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- letsencrypt:/letsencrypt
networks: [edge]
restart: unless-stopped
labels:
- traefik.enable=true
- traefik.http.routers.dash.rule=Host(`traefik.example.com`)
- traefik.http.routers.dash.entrypoints=websecure
- traefik.http.routers.dash.tls.certresolver=le
- traefik.http.routers.dash.service=api@internal
- traefik.http.routers.dash.middlewares=dash-auth
- traefik.http.middlewares.dash-auth.basicauth.users=${DASH_USERS}
networks:
edge:
name: edge
volumes:
letsencrypt:
edge/.env.example
# make with: docker run --rm httpd:2.4-alpine htpasswd -nbB admin change-me-dash
# then double every $ sign so Compose does not read it as a variable
DASH_USERS=admin:$$2y$$05$$replace-with-your-hash
What each line does
exposedbydefault=false— A container is published only if it has traefik.enable=true. Safer: databases are never exposed by mistake.network=edge— Traefik talks to sites over one shared network called edge.redirections to websecure— Every http request is redirected to https automatically.acme.email— Let's Encrypt sends expiry warnings here. Use a real team mailbox.httpchallenge— Let's Encrypt checks port 80 on your server to prove you own the domain. Port 80 must be open.docker.sock:ro— Traefik reads container labels. Read-only lowers the risk, but the socket is still powerful; keep Traefik updated.api@internal + basicauth— The dashboard is reachable only with a password over HTTPS.
Green output
$ docker compose -f edge/compose.yaml --env-file edge/.env up -d
Network edge Created
Volume "edge_letsencrypt" Created
Container edge-traefik-1 Started
$ docker logs edge-traefik-1 2>&1 | tail -2
level=INFO msg="Starting provider *docker.Provider"
level=INFO msg="Starting provider *acme.ChallengeHTTP"
Expected result. Traefik is running and watching Docker. No sites yet.
Example 2: Three sites, each in its own folder and compose file, joined to the edge network with labels
Goal. Three sites, each in its own folder and compose file, joined to the edge network with labels.
bakery/compose.yaml
services:
web:
image: nginx:1.27-alpine
volumes: [./html:/usr/share/nginx/html:ro]
networks: [edge]
restart: unless-stopped
labels:
- traefik.enable=true
- traefik.http.routers.bakery.rule=Host(`bakery.example.com`) || Host(`www.bakery.example.com`)
- traefik.http.routers.bakery.entrypoints=websecure
- traefik.http.routers.bakery.tls.certresolver=le
- traefik.http.routers.bakery.middlewares=sec-headers@docker
- traefik.http.middlewares.sec-headers.headers.stsSeconds=31536000
- traefik.http.middlewares.sec-headers.headers.contentTypeNosniff=true
- traefik.http.middlewares.sec-headers.headers.frameDeny=true
- traefik.http.services.bakery.loadbalancer.server.port=80
networks:
edge:
external: true
gym/compose.yaml
services:
app:
image: registry.example.com/gym-site:1.4
environment:
DATABASE_URL: postgresql://gym:${DB_PASSWORD}@db:5432/gym
networks: [edge, internal]
restart: unless-stopped
labels:
- traefik.enable=true
- traefik.http.routers.gym.rule=Host(`gym.example.com`)
- traefik.http.routers.gym.entrypoints=websecure
- traefik.http.routers.gym.tls.certresolver=le
- traefik.http.routers.gym.middlewares=gym-rl
- traefik.http.middlewares.gym-rl.ratelimit.average=20
- traefik.http.middlewares.gym-rl.ratelimit.burst=40
- traefik.http.services.gym.loadbalancer.server.port=8080
- traefik.http.services.gym.loadbalancer.healthcheck.path=/healthz
- traefik.http.services.gym.loadbalancer.healthcheck.interval=10s
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: gym
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: gym
volumes: [gym-db:/var/lib/postgresql/data]
networks: [internal]
restart: unless-stopped
networks:
edge:
external: true
internal: {}
volumes:
gym-db:
blog/compose.yaml
services:
ghost:
image: ghost:5-alpine
environment:
url: https://blog.example.com
database__client: sqlite3
database__connection__filename: /var/lib/ghost/content/data/ghost.db
volumes: [ghost-content:/var/lib/ghost/content]
networks: [edge]
restart: unless-stopped
labels:
- traefik.enable=true
- traefik.http.routers.blog.rule=Host(`blog.example.com`)
- traefik.http.routers.blog.entrypoints=websecure
- traefik.http.routers.blog.tls.certresolver=le
- traefik.http.services.blog.loadbalancer.server.port=2368
networks:
edge:
external: true
volumes:
ghost-content:
What each line does
external: true— The site joins the edge network made by the Traefik stack. It does not create its own.Router name per site— bakery, gym and blog must be unique names, or routers overwrite each other.loadbalancer.server.port— The port inside the container. No ports: line, so nothing is published on the host except by Traefik.gym db only on internal— Postgres is not on edge, so Traefik and other sites cannot reach it.ratelimit— Average 20 requests per second per client, bursts to 40. Stops simple floods.healthcheck in labels— Traefik stops sending traffic to an unhealthy gym copy.registry.example.com/gym-site— An example image name. Use your own image from your registry.
Green output
$ echo 'Bakery is open' | tee bakery/html/index.html
$ docker compose -f bakery/compose.yaml up -d
$ docker compose -f gym/compose.yaml --env-file gym/.env up -d
$ docker compose -f blog/compose.yaml up -d
$ curl -sI http://bakery.example.com | head -2
HTTP/1.1 308 Permanent Redirect
Location: https://bakery.example.com/
$ curl -sI https://bakery.example.com | grep -iE 'HTTP|strict'
HTTP/2 200
strict-transport-security: max-age=31536000
Expected result. Three sites share one server. HTTP redirects to HTTPS and each domain has its own certificate.
Example 3: Local practice with *
Goal. Local practice with *.localhost (no domain needed), scaling one site, and checking certificates.
local/compose.yaml
services:
traefik:
image: traefik:v3.1
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --api.insecure=true
ports: ["80:80", "8080:8080"]
volumes: ["/var/run/docker.sock:/var/run/docker.sock:ro"]
whoami:
image: traefik/whoami:v1.10
labels:
- traefik.enable=true
- traefik.http.routers.who.rule=Host(`who.localhost`)
- traefik.http.routers.who.entrypoints=web
shop:
image: nginx:1.27-alpine
labels:
- traefik.enable=true
- traefik.http.routers.shop.rule=Host(`shop.localhost`) && PathPrefix(`/`)
- traefik.http.routers.shop.entrypoints=web
ops/certs.sh
for d in bakery.example.com gym.example.com blog.example.com; do
echo "== $d"
echo | openssl s_client -connect "$d:443" -servername "$d" 2>/dev/null | openssl x509 -noout -issuer -enddate
done
ops/cleanup.sh
for s in blog gym bakery; do docker compose -f "$s/compose.yaml" down -v; done
docker compose -f edge/compose.yaml down -v
docker compose -f local/compose.yaml down
docker network rm edge 2>/dev/null || true
echo "all sites removed"
What each line does
*.localhost— Browsers and curl send every name ending in .localhost to 127.0.0.1. Good for local tests with no DNS.api.insecure=true— Opens the dashboard on port 8080 with no password. Only for the laptop, never on a server.traefik/whoami— A tiny test app that prints the request and its own hostname. Great for seeing load balancing.--scale whoami=3— Traefik finds all copies by itself and balances between them. No reload needed.openssl s_client— Shows who issued the certificate and when it expires. Let's Encrypt certificates live 90 days; Traefik renews about 30 days before.cleanup order— Sites first, then Traefik, then the shared network.
Green output
$ docker compose -f local/compose.yaml up -d --scale whoami=3
$ for i in 1 2 3; do curl -s who.localhost | grep Hostname; done
Hostname: 4b1f0c2a9e3d
Hostname: 9a7e6d5c4b3a
Hostname: 1c2d3e4f5a6b
$ bash ops/certs.sh
== bakery.example.com
issuer=C = US, O = Let's Encrypt, CN = R11
notAfter=Jan 22 10:14:03 2027 GMT
Expected result. Traefik balances copies on its own, and every live domain shows a Let's Encrypt certificate.
Every command, step by step (plain docker and Compose)
-
Point DNS
Create A records bakery, gym, blog, traefik to the Elastic IPWhat it does: Let's Encrypt can only reach you if DNS is correct.
dig +short bakery.example.com shows your IP -
Open ports
Security group: 80 and 443 from anywhere, 22 from your IPWhat it does: Port 80 is needed for the HTTP challenge.
Inbound rules saved -
Copy env files
cp edge/.env.example edge/.envWhat it does: Put your hashed dashboard user in it.
(no output) -
Start Traefik
docker compose -f edge/compose.yaml --env-file edge/.env up -dWhat it does: Creates the edge network.
Container edge-traefik-1 Started -
Start the bakery
docker compose -f bakery/compose.yaml up -dWhat it does: Static site with labels.
Container bakery-web-1 Started -
Start the gym and blog
docker compose -f gym/compose.yaml --env-file gym/.env up -d && docker compose -f blog/compose.yaml up -dWhat it does: Two more sites.
Container blog-ghost-1 Started -
Test redirect
curl -sI http://bakery.example.comWhat it does: Should be 308 to https.
HTTP/1.1 308 Permanent Redirect -
Test HTTPS
curl -sI https://gym.example.comWhat it does: Valid certificate, 200.
HTTP/2 200 -
Read Traefik logs
docker logs edge-traefik-1 2>&1 | grep -i acmeWhat it does: Shows certificate requests.
Certificates obtained for domains [gym.example.com] -
Open the dashboard
https://traefik.example.com in a browserWhat it does: Asks for the dashboard user, then shows routers and services.
Routers: 4, all green -
Check certificates
bash ops/certs.shWhat it does: Issuer and expiry for each domain.
issuer=... Let's Encrypt -
Clean up
bash ops/cleanup.shWhat it does: Removes all stacks, volumes and the edge network.
all sites removed
When it is red 1
acme: error: 400 :: urn:ietf:params:acme:error:connection :: Timeout during connect (likely firewall problem)
Cause. Let's Encrypt could not reach port 80 on your server.
Fix, step by step
- Open port 80 in the security group and any host firewall.
- Check DNS points to this server with dig.
When it is red 2
acme: error: 429 :: too many certificates already issued
Cause. You hit the Let's Encrypt rate limit by recreating certificates too many times, often because acme.json was not on a volume.
Fix, step by step
- Keep the letsencrypt volume.
- Test with the staging server: --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
When it is red 3
404 page not found
Cause. Traefik has no router that matches this host, or the container lacks traefik.enable=true.
Fix, step by step
- Check the Host rule spelling and backticks.
- Check docker inspect labels and the dashboard routers list.
When it is red 4
Gateway Timeout (504) or Bad Gateway (502)
Cause. Traefik can see the router but cannot reach the container: wrong port or not on the edge network.
Fix, step by step
- Set loadbalancer.server.port to the port inside the container.
- Add the edge network to the service and mark it external.
When it is red 5
network edge declared as external, but could not be found
Cause. The site stack was started before the Traefik stack created edge.
Fix, step by step
- Start edge/compose.yaml first, or run docker network create edge.
When it is red 6
Browser shows TRAEFIK DEFAULT CERT
Cause. The certificate is not issued yet or failed, so Traefik uses its self-signed default.
Fix, step by step
- Read docker logs for acme errors.
- Make sure tls.certresolver=le is on the router.
Practice tasks
Easy Task 1: Local
Why: Labels.
What to do: Run local/compose.yaml and open who.localhost and shop.localhost.
- Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
- Create the edge folder and one folder per site with the files from this lesson.
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
docker compose -f local/compose.yaml up -dEasy Task 2: Dashboard
Why: Traefik.
What to do: Open localhost:8080 and find the who router.
- Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
- Create the edge folder and one folder per site with the files from this lesson.
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
http://localhost:8080Easy Task 3: Scale
Why: Load balance.
What to do: Scale whoami to 3 and see 3 hostnames.
- Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
- Create the edge folder and one folder per site with the files from this lesson.
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
--scale whoami=3Medium Task 4: New site
Why: Labels.
What to do: Add a fourth local site docs.localhost with nginx and no Traefik restart.
- Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
- Create the edge folder and one folder per site with the files from this lesson.
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
traefik.http.routers.docs.rule=Host(`docs.localhost`)Medium Task 5: Basic auth
Why: Middleware.
What to do: Protect shop.localhost with a basicauth middleware.
- Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
- Create the edge folder and one folder per site with the files from this lesson.
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
htpasswd -nbB admin change-meMedium Task 6: Path routing
Why: Router.
What to do: Route who.localhost/api to whoami with a StripPrefix middleware.
- Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
- Create the edge folder and one folder per site with the files from this lesson.
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
PathPrefix(`/api`)Medium Task 7: Staging certs
Why: ACME.
What to do: On the server, use the Let's Encrypt staging caserver first.
- Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
- Create the edge folder and one folder per site with the files from this lesson.
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
--certificatesresolvers.le.acme.caserver=...Hard Task 8: Three domains
Why: Deploy.
What to do: Run bakery, gym and blog on one EC2 with real HTTPS.
- Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
- Create the edge folder and one folder per site with the files from this lesson.
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
bash ops/certs.shHard Task 9: Rate limit
Why: Middleware.
What to do: Fire 200 requests at gym quickly and see 429 replies.
- Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
- Create the edge folder and one folder per site with the files from this lesson.
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
for i in $(seq 200); do curl -s -o /dev/null -w '%{http_code} ' https://gym.example.com; doneEasy Task 10: Clean
Why: Habit.
What to do: Remove everything and release the Elastic IP.
- Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
- Create the edge folder and one folder per site with the files from this lesson.
- Run the steps and compare with the solution. Always run the cleanup step.
Solution
bash ops/cleanup.shHomework: try at home
Add a fourth site that serves two paths from two containers: shop.example.com/ goes to a frontend and shop.example.com/api goes to an API, with a StripPrefix middleware for /api.
Recap
- Traefik is a reverse proxy that reads Docker labels and configures itself.
- One shared external network edge connects Traefik to every site.
- Each site is its own compose stack with a unique router name.
- Let's Encrypt certificates are issued and renewed automatically; keep acme.json on a volume.
- Use exposedbydefault=false and keep databases off the edge network.
- Use *.localhost for practice and the staging CA before real domains.
FAQ
Q. Traefik or nginx?
Both are fine. Traefik is easier when containers come and go; nginx gives fine control and is very common.
Q. Can Traefik do wildcard certificates?
Yes, with the DNS challenge and your DNS provider such as Route 53 or Cloudflare.
Q. Is mounting the Docker socket safe?
It gives strong power. Use read-only, keep Traefik updated, or use a socket proxy container.
Q. How many sites can one t3.small hold?
Many small static sites; it depends on RAM used by each app and database.
Q. Does Traefik work with Swarm and Kubernetes?
Yes. It has Swarm and Kubernetes providers with the same router ideas.
Q. What if my domain is on Cloudflare proxy?
Use the DNS challenge, or turn off the orange cloud while testing the HTTP challenge.
Interview questions
-
How do you host many websites on one server with Docker?
Model answer: A reverse proxy such as Traefik or nginx on 80 and 443 routes by host name to each container.
-
How does Traefik discover services?
Model answer: Through providers. The Docker provider reads labels from running containers.
-
How do you get free HTTPS certificates automatically?
Model answer: Let's Encrypt with the ACME protocol, through Traefik, Caddy or certbot.
-
What are Traefik routers, services and middlewares?
Model answer: Routers match requests, services send them to containers, middlewares change requests between them.
-
How do you keep internal services private?
Model answer: Do not label them, keep them on an internal network, and do not publish their ports.
Quick quiz
Q1. Why use Traefik instead of hand-written nginx files?
Traefik configures itself from labels and gets HTTPS certificates automatically.
Q2. Why exposedbydefault=false?
So only containers we label are published. Databases stay hidden.
Q3. What is the HTTP challenge?
Let's Encrypt fetches a token from port 80 on your domain to prove you control it.
Q4. Why must acme.json be on a volume?
So certificates survive restarts and we do not hit rate limits.
Q5. How do sites reach Traefik?
They join the shared external network edge.
Samajla ka?
Revision card
Interview card