22 May, 2025
This is a basic Traefik and docker compose setup designed for production with Lets Encrypt, Prometheus (linked to Traefik), PostgreSQL, NGINX (for static files only) and FastAPI. There are scripts for setting up automatic updates, firewall and fail2ban for basic hardening.
You'll need to update the scripts with your own passwords, emails and domains.
Let's get started:
cat << 'EOINSTALL' > install.sh
#!/usr/bin/env bash
set -euo pipefail
# 1) Install prerequisites
apt-get update
apt-get install -y ca-certificates curl gnupg lsb-release
# 2) Add Docker’s official GPG key and repo
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg \
| gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/debian \
$(lsb_release -cs) stable" \
> /etc/apt/sources.list.d/docker.list
# 3) Install Docker Engine & Compose plugin
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
mkdir -p static
cat << 'EOF' > static/index.html
Hello!
EOF
cat << 'EOF' > prometheus.yml
scrape_configs:
- job_name: 'traefik'
static_configs:
- targets: ['traefik:9100']
EOF
# 7) Create a starter .env (edit as needed)
# Install if missing:
sudo apt install apache2-utils
# Replace 'admin' and 'yourpassword' with your desired username/pass
CREDS=`htpasswd -nb admin yourpassword`
cat > .env <<EOF
# Replace these with your real values
DOMAIN=innovation.2.example.com
LETSENCRYPT_EMAIL=james@example.com
# In dev you can set:
# DOMAIN=localhost
# DASHBOARD_INSECURE="--api.insecure=true"
DASHBOARD_INSECURE=false
# .env
LETSENCRYPT_CA_SERVER=https://acme-v02.api.letsencrypt.org/directory # prod
# LETSENCRYPT_CA_SERVER=https://acme-staging-v02.api.letsencrypt.org/directory # staging
CREDS='${CREDS}'
POSTGRES_USER=appuser
POSTGRES_PASSWORD=supersecure
POSTGRES_DB=appdb
EOF
# 8) Fire it up
docker compose pull
docker compose up -d
echo "✅ Traefik stack launched. Make sure DNS for domain → this host’s IP (ports 80 & 443 open)."
EOINSTALL
cat << 'EOF' > updates.sh
#!/bin/bash
set -e
# Variables
EMAIL="james@example.com"
# Install unattended-upgrades
echo "Installing unattended-upgrades..."
apt update
apt install -y unattended-upgrades apt-listchanges
# Enable the unattended-upgrades service
echo "Enabling unattended-upgrades..."
dpkg-reconfigure --priority=low unattended-upgrades --frontend=noninteractive
# Configure automatic updates
echo "Configuring unattended-upgrades..."
cat > /etc/apt/apt.conf.d/50unattended-upgrades <<EOL
Unattended-Upgrade::Origins-Pattern {
"o=Debian,n=bullseye";
"o=Debian,n=bullseye-updates";
"o=Debian,n=bullseye-security";
"o=Debian,n=bullseye-backports";
};
Unattended-Upgrade::Package-Blacklist {
};
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
Unattended-Upgrade::Mail "$EMAIL";
Unattended-Upgrade::MailOnlyOnError "true";
EOL
# Configure periodic updates
cat > /etc/apt/apt.conf.d/20auto-upgrades <<EOL
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
EOL
# Enable and restart the unattended-upgrades service
systemctl enable --now unattended-upgrades
echo "Simulating an upgrade ..."
sudo unattended-upgrades --dry-run --debug
echo "done."
echo "Unattended upgrades setup completed. Updates will be applied automatically, and email notifications will be sent to $EMAIL."
admin@ip-172-26-2-246:~$ cat secure.sh
#!/bin/bash
set -e
# Update and Install Required Security Packages
apt update && apt upgrade -y
apt install -y fail2ban ufw apache2-utils
# Configure UFW
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow http
ufw allow https
ufw enable
# Fail2Ban Configuration
cat > /etc/fail2ban/jail.local <<EOL
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
destemail = james@example.com
sender = james@example.com
mta = sendmail
[sshd]
enabled = true
EOL
systemctl restart fail2ban ufw
echo "Security configuration completed successfully."
Docker compose file:
cat << 'EOF' > compose.yml
services:
traefik:
image: traefik:v3.4
restart: always
command:
- "--global.sendAnonymousUsage=false"
# Metrics
- "--metrics.prometheus=true"
- "--entryPoints.metrics.address=:9100"
- "--metrics.prometheus.entryPoint=metrics"
# static provider
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
# entrypoints
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entryPoints.web.transport.lifeCycle.graceTimeOut=1"
- "--entryPoints.websecure.transport.lifeCycle.graceTimeOut=1"
- "--entryPoints.metrics.transport.lifeCycle.graceTimeOut=1"
# ACME via HTTP challenge
- "--certificatesresolvers.le.acme.httpchallenge=true"
- "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.le.acme.email=${LETSENCRYPT_EMAIL}"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
# always present, defaulting to prod; override in .env when you want staging
- "--certificatesresolvers.le.acme.caServer=${LETSENCRYPT_CA_SERVER:-https://acme-v02.api.letsencrypt.org/directory}"
# logging
- "--log.level=INFO"
- "--accesslog=true"
# enable only in dev
- "--api.dashboard=true"
- "${DASHBOARD_INSECURE:-false}"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik_letsencrypt:/letsencrypt
nginx-static:
image: nginx:alpine
restart: always
volumes:
- ./static:/usr/share/nginx/html:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.static.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.static.entrypoints=websecure"
- "traefik.http.routers.static.tls.certresolver=le"
- "traefik.http.services.static.loadbalancer.server.port=80"
# oauth-provider:
# build: ./provider
# restart: always
# labels:
# - "traefik.enable=true"
# - "traefik.http.routers.oauth-provider.rule=Host(`auth.${DOMAIN}`) && PathPrefix(`/`)"
# - "traefik.http.routers.oauth-provider.tls.certresolver=le"
# - "traefik.http.services.oauth-provider.loadbalancer.server.port=10000"
# oauth-client:
# build: ./client
# restart: always
# labels:
# - "traefik.enable=true"
# - "traefik.http.routers.oauth-client.rule=Host(`api.${DOMAIN}`) && PathPrefix(`/`)"
# - "traefik.http.routers.oauth-client.tls.certresolver=le"
# - "traefik.http.services.oauth-client.loadbalancer.server.port=11000"
prometheus:
image: prom/prometheus:latest
command:
- "--web.external-url=http://localhost/prometheus"
volumes:
- ./prometheus.yml:/prometheus/prometheus.yml:ro
# ports:
# - "9090:9090"
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.routers.prometheus.rule=Host(`prometheus.${DOMAIN}`)"
- "traefik.http.routers.prometheus.entrypoints=websecure"
- "traefik.http.routers.prometheus.tls.certresolver=le"
- "traefik.http.services.prometheus.loadbalancer.server.port=9090"
- "traefik.http.middlewares.prometheus-auth.basicauth.users=${CREDS}"
- "traefik.http.routers.prometheus.middlewares=prometheus-auth"
postgres:
image: postgres:16
restart: always
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-appdb}
volumes:
- pg_data:/var/lib/postgresql/data
# ports:
# - "5432:5432"
fastapi:
build:
context: ./fastapi
dockerfile: Dockerfile
restart: always
depends_on:
- postgres
labels:
- "traefik.enable=true"
- "traefik.http.routers.fastapi.rule=Host(`api.${DOMAIN}`)"
- "traefik.http.routers.fastapi.entrypoints=websecure"
- "traefik.http.routers.fastapi.tls.certresolver=le"
- "traefik.http.services.fastapi.loadbalancer.server.port=8000"
volumes:
pg_data:
traefik_letsencrypt:
EOF
$ cat fastapi/Dockerfile fastapi/app/main.py
FROM python:3.12-slim-bookworm
# Install pip & FastAPI + Uvicorn
RUN pip install --no-cache-dir fastapi uvicorn asyncpg
WORKDIR /app
COPY ./app /app
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
class MessageResponse(BaseModel):
message: str
app = FastAPI()
origins=["https://innovation.2.example.com", "http://localhost:5173"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"],
)
@app.get(
"/",
response_model=MessageResponse,
summary="Health check",
tags=["Health"],
description="Returns a welcome message proving the API is up and CORS is enabled."
)
def read_root():
return {"message": "Hello from FastAPI with CORS enabled"}
Launch with:
docker compose build fastapi && docker compose down fastapi && docker compose up -d --no-deps fastapi
Then test CORS with:
curl -i -X OPTIONS https://api.innovation.2.example.com/ \
-H "Origin: http://localhost:5173" \
-H "Access-Control-Request-Method: POST"
HTTP/2 200
access-control-allow-methods: DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT
access-control-allow-origin: http://localhost:5173
access-control-max-age: 600
content-type: text/plain; charset=utf-8
date: Thu, 22 May 2025 14:51:41 GMT
server: uvicorn
vary: Origin
content-length: 2
OK
And test it:
curl https://api.innovation.2.example.com/
{"message":"Hello from FastAPI with CORS enabled"}
You can get openapi.json
with:
curl https://api.innovation.2.example.com/openapi.json
{"openapi":"3.1.0","info":{"title":"FastAPI","version":"0.1.0"},"paths":{"/":{"get":{"tags":["Health"],"summary":"Health check","description":"Returns a welcome message proving the API is up and CORS is enabled.","operationId":"read_root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}}}}}}},"components":{"schemas":{"MessageResponse":{"properties":{"message":{"type":"string","title":"Message"}},"type":"object","required":["message"],"title":"MessageResponse"}}}}
Be the first to comment.
Copyright James Gardner 1996-2020 All Rights Reserved. Admin.