Ravindra BagaleCourses & study guides मराठी

Chapter 58: Docker Projects Part 2: AWS RDS, DynamoDB, S3, Reel Upload, Microservices, Queues, Logging, Monitoring and Cloud Deploys

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.
Point DNS to the server. Start Traefik. Start sites with labels. Check HTTPS. Add middlewares. Clean up.
One EC2 with Traefik in front; each site is a separate compose stack with labels and its own certificate

How: step by step

  1. Point DNS to the server. A records for each domain go to the EC2 Elastic IP.
  2. Start Traefik. One compose file with ports 80 and 443 and the Docker socket read-only.
  3. Start sites with labels. Each site has its own compose file and joins the shared network edge.
  4. Check HTTPS. Traefik gets certificates on the first request.
  5. Add middlewares. HTTPS redirect, basic auth for the dashboard, security headers, rate limit.
  6. Clean up. Stop the stacks, delete volumes, release the Elastic IP.

Architecture

Users: bakery, gym, blog domains DNS A records to one Elastic IP Traefik :80 :443 routers by Host, HTTPS by Let's Encrypt network edge (shared) labels pick the container and port bakery nginx gym app ghost blog one compose stack per site gym Postgres on network internal never published
Traefik multi-site host

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)

  1. Point DNS

    Create A records bakery, gym, blog, traefik to the Elastic IP
    

    What it does: Let's Encrypt can only reach you if DNS is correct.

    dig +short bakery.example.com shows your IP
    
  2. Open ports

    Security group: 80 and 443 from anywhere, 22 from your IP
    

    What it does: Port 80 is needed for the HTTP challenge.

    Inbound rules saved
    
  3. Copy env files

    cp edge/.env.example edge/.env
    

    What it does: Put your hashed dashboard user in it.

    (no output)
    
  4. Start Traefik

    docker compose -f edge/compose.yaml --env-file edge/.env up -d
    

    What it does: Creates the edge network.

    Container edge-traefik-1 Started
    
  5. Start the bakery

    docker compose -f bakery/compose.yaml up -d
    

    What it does: Static site with labels.

    Container bakery-web-1 Started
    
  6. Start the gym and blog

    docker compose -f gym/compose.yaml --env-file gym/.env up -d && docker compose -f blog/compose.yaml up -d
    

    What it does: Two more sites.

    Container blog-ghost-1 Started
    
  7. Test redirect

    curl -sI http://bakery.example.com
    

    What it does: Should be 308 to https.

    HTTP/1.1 308 Permanent Redirect
    
  8. Test HTTPS

    curl -sI https://gym.example.com
    

    What it does: Valid certificate, 200.

    HTTP/2 200
    
  9. Read Traefik logs

    docker logs edge-traefik-1 2>&1 | grep -i acme
    

    What it does: Shows certificate requests.

    Certificates obtained for domains [gym.example.com]
    
  10. Open the dashboard

    https://traefik.example.com in a browser
    

    What it does: Asks for the dashboard user, then shows routers and services.

    Routers: 4, all green
    
  11. Check certificates

    bash ops/certs.sh
    

    What it does: Issuer and expiry for each domain.

    issuer=... Let's Encrypt
    
  12. Clean up

    bash ops/cleanup.sh
    

    What 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

  1. Open port 80 in the security group and any host firewall.
  2. 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

  1. Keep the letsencrypt volume.
  2. 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

  1. Check the Host rule spelling and backticks.
  2. 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

  1. Set loadbalancer.server.port to the port inside the container.
  2. 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

  1. 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

  1. Read docker logs for acme errors.
  2. 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.

How:
  1. Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
  2. Create the edge folder and one folder per site with the files from this lesson.
  3. Run the steps and compare with the solution. Always run the cleanup step.
Solution
docker compose -f local/compose.yaml up -d

Easy Task 2: Dashboard

Why: Traefik.

What to do: Open localhost:8080 and find the who router.

How:
  1. Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
  2. Create the edge folder and one folder per site with the files from this lesson.
  3. Run the steps and compare with the solution. Always run the cleanup step.
Solution
http://localhost:8080

Easy Task 3: Scale

Why: Load balance.

What to do: Scale whoami to 3 and see 3 hostnames.

How:
  1. Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
  2. Create the edge folder and one folder per site with the files from this lesson.
  3. Run the steps and compare with the solution. Always run the cleanup step.
Solution
--scale whoami=3

Medium Task 4: New site

Why: Labels.

What to do: Add a fourth local site docs.localhost with nginx and no Traefik restart.

How:
  1. Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
  2. Create the edge folder and one folder per site with the files from this lesson.
  3. 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.

How:
  1. Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
  2. Create the edge folder and one folder per site with the files from this lesson.
  3. Run the steps and compare with the solution. Always run the cleanup step.
Solution
htpasswd -nbB admin change-me

Medium Task 6: Path routing

Why: Router.

What to do: Route who.localhost/api to whoami with a StripPrefix middleware.

How:
  1. Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
  2. Create the edge folder and one folder per site with the files from this lesson.
  3. 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.

How:
  1. Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
  2. Create the edge folder and one folder per site with the files from this lesson.
  3. 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.

How:
  1. Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
  2. Create the edge folder and one folder per site with the files from this lesson.
  3. Run the steps and compare with the solution. Always run the cleanup step.
Solution
bash ops/certs.sh

Hard Task 9: Rate limit

Why: Middleware.

What to do: Fire 200 requests at gym quickly and see 429 replies.

How:
  1. Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
  2. Create the edge folder and one folder per site with the files from this lesson.
  3. 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; done

Easy Task 10: Clean

Why: Habit.

What to do: Remove everything and release the Elastic IP.

How:
  1. Use one Ubuntu VM (EC2 t3.small) with Docker and a domain you own. For local tests use Docker Desktop and *.localhost names.
  2. Create the edge folder and one folder per site with the files from this lesson.
  3. Run the steps and compare with the solution. Always run the cleanup step.
Solution
bash ops/cleanup.sh

Homework: 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

  1. 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.

  2. How does Traefik discover services?

    Model answer: Through providers. The Docker provider reads labels from running containers.

  3. How do you get free HTTPS certificates automatically?

    Model answer: Let's Encrypt with the ACME protocol, through Traefik, Caddy or certbot.

  4. What are Traefik routers, services and middlewares?

    Model answer: Routers match requests, services send them to containers, middlewares change requests between them.

  5. 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?

Ravi Sir's Cards for this lesson

मराठी cards