From 516eb7883245c2190388ad6181b10f6918cffdf9 Mon Sep 17 00:00:00 2001 From: Max Richter Date: Mon, 12 May 2025 13:09:43 +0200 Subject: [PATCH] feat: init --- .gitignore | 4 + README.md | 104 ++++++++++++++++++ compose.yml | 84 ++++++++++++++ config/dns/config.sample.json | 41 +++++++ config/dns/entrypoint.sh | 25 +++++ config/traefik/dynamic/dashboard.yaml | 5 + config/traefik/dynamic/ssl.yaml | 16 +++ config/traefik/traefik.yml | 37 +++++++ services/.gitignore | 0 services/docker-compose.yml | 67 +++++++++++ .../grafana/datasources/default.yaml | 8 ++ .../provisioning/grafana/plugins/app.yaml | 12 ++ 12 files changed, 403 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 compose.yml create mode 100644 config/dns/config.sample.json create mode 100755 config/dns/entrypoint.sh create mode 100644 config/traefik/dynamic/dashboard.yaml create mode 100644 config/traefik/dynamic/ssl.yaml create mode 100644 config/traefik/traefik.yml create mode 100644 services/.gitignore create mode 100644 services/docker-compose.yml create mode 100644 services/provisioning/grafana/datasources/default.yaml create mode 100644 services/provisioning/grafana/plugins/app.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e40c159 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/compose.override.yml +/config/traefik/dynamic/* +!/config/traefik/dynamic/ssl.yaml +!/config/traefik/dynamic/dashboard.yaml diff --git a/README.md b/README.md new file mode 100644 index 0000000..e014145 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# Local HTTPS with Traefik and Step-CA + +This setup enables automatic HTTPS and local domain routing for Docker Compose services. Traefik uses Step-CA to issue certificates and route .dev.local domains securely with minimal configuration. + +## Setup + +1. Clone this repository and navigate into the directory. + +2. Start the services: +```bash +docker compose up -d +``` + +3. Trust the Step-CA root certificate: +```bash +curl -k https://localhost:9000/roots.pem -o roots.pem +sudo trust anchor --store roots.pem +rm roots.pem +``` + +## How to Use + +1. Add a label to your docker compose service: +```yaml +labels: + serviceName: my-app +``` + +2. Your service will be accessible at `https://my-app.dev.local`. + +## Troubleshooting + +If certificates are not renewed or have expired +```bash +docker compose up -d --force-recreate traefik +``` + +## Tipps + +### Custom Domain Suffix + +For example `dev.cool` 😎 + +Replace `.dev.local` with your custom domain suffix in the `config/traefik/traefik.yml` file: +```yaml +... + docker: + defaultRule: | + Host(`{{ trim (index .Labels "serviceName") }}.dev.cool`) {{range $i, $domain := splitList "," (index .Labels "serviceDomains")}}{{if ne $domain ""}}|| Host(`{{$domain}}`){{end}}{{end}} +... +``` + +Replace `.dev.local` with your custom domain suffix in the `config/dns/config.sample.json` file: +```json +... +{ + "id": 2, + "hostname": ".dev.cool", + "ip": "", + "target": "host.docker", + "ttl": 3600, + "type": "CNAME" +} +... +``` + +Remove the dns_config volume +```bash +docker compose down +docker compose volukme rm dns_config +docker compose up -d +``` + + +### Certificate Lifetime + +To ensure Traefik has enough time to renew certificates, increase their duration: +```bash +docker compose exec step step ca provisioner update acme \ + --x509-min-dur=20m \ + --x509-max-dur=8760h \ + --x509-default-dur=2160h +``` + +### Use the preconfigured services + +If you use the preconfigured services, you can add the following snippet to you `.bashrc/.zshrc` to easily start, stop, and manage the services. + +```bash +dev () { + PROJECT_DIR="$HOME/Projects/dev/services" + case "$1" in + (start) shift + docker compose -f "$PROJECT_DIR/docker-compose.yml" --profile "$@" up -d ;; + (restart) shift + docker compose -f "$PROJECT_DIR/docker-compose.yml" --profile "$@" restart ;; + (stop) shift + docker compose -f "$PROJECT_DIR/docker-compose.yml" --profile "$@" down --remove-orphans ;; + (logs) shift + docker compose -f "$PROJECT_DIR/docker-compose.yml" --profile "$@" logs -f ;; + (*) echo "Usage: dev {start|restart|stop|logs} [services...]" ;; + esac +} +``` diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..010ecc3 --- /dev/null +++ b/compose.yml @@ -0,0 +1,84 @@ +services: + dns: + image: defreitas/dns-proxy-server:3.32.4 + restart: unless-stopped + entrypoint: /conf/entrypoint.sh + environment: + MG_LOG_LEVEL: info + MG_DOMAIN: docker + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./config/dns:/conf + - dns_config:/app/conf + labels: + serviceName: dps + expose: + - "5380" + networks: + default: + ipv4_address: 172.157.5.249 + + traefik: + image: traefik:3.3 + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./config/traefik:/etc/traefik + - traefik:/traefik + - step:/step:ro + network_mode: host + environment: + LEGO_CA_CERTIFICATES: /step/certs/root_ca.crt + LEGO_CA_SERVERNAME: localhost + depends_on: + step: + condition: service_healthy + restart: false + + step: + image: smallstep/step-ca:latest + working_dir: /home/step + restart: unless-stopped + volumes: + - step:/home/step + environment: + DOCKER_STEPCA_INIT_NAME: Max authority + DOCKER_STEPCA_INIT_DNS_NAMES: localhost,step.dev.local + DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT: "false" + DOCKER_STEPCA_INIT_ACME: "true" + labels: + serviceName: step + traefik.tcp.routers.step.rule: HostSNI(`step.dev.local`) + traefik.tcp.routers.step.tls.passthrough: "true" + ports: + - "9000:9000" + command: step-ca --resolver "172.157.5.249:53" --password-file "/home/step/secrets/password" "/home/step/config/ca.json" + healthcheck: + test: ["CMD", "step", "ca", "health"] + interval: 60s + start_period: 10s + start_interval: 1s + dns: + - 172.157.5.249 + depends_on: + dns: + condition: service_started + restart: false + +volumes: + dns_config: ~ + traefik: ~ + step: ~ + +networks: + default: + name: dps + driver: bridge + ipam: + driver: default + config: + - subnet: 172.157.0.0/16 + ip_range: 172.157.5.0/24 + gateway: 172.157.5.1 + - subnet: fc00:5c6f:db50::/64 + gateway: fc00:5c6f:db50::1 diff --git a/config/dns/config.sample.json b/config/dns/config.sample.json new file mode 100644 index 0000000..9d8fd02 --- /dev/null +++ b/config/dns/config.sample.json @@ -0,0 +1,41 @@ +{ + "version": 2, + "activeEnv": "", + "webServerPort": null, + "dnsServerPort": null, + "defaultDns": null, + "logLevel": null, + "logFile": null, + "registerContainerNames": null, + "hostMachineHostname": null, + "domain": "docker", + "dpsNetwork": true, + "dpsNetworkAutoConnect": true, + "resolvConfOverrideNameServers": false, + "noRemoteServers": true, + "noEntriesResponseCode": 2, + "remoteDnsServers": [], + "envs": [ + { + "name": "", + "hostnames": [ + { + "id": 1, + "hostname": ".vm", + "ip": "", + "target": "host.docker", + "ttl": 3600, + "type": "CNAME" + }, + { + "id": 2, + "hostname": ".dev.local", + "ip": "", + "target": "host.docker", + "ttl": 3600, + "type": "CNAME" + } + ] + } + ] +}, diff --git a/config/dns/entrypoint.sh b/config/dns/entrypoint.sh new file mode 100755 index 0000000..c671a2c --- /dev/null +++ b/config/dns/entrypoint.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +if [ ! -d "/app/conf" ]; then + mkdir /app/conf +fi + +if [ ! -f "/app/conf/config.json" ]; then + cp /conf/config.sample.json /app/conf/config.json +elif [ "/conf/config.sample.json" -nt "/app/conf/config.json" ]; then + echo "example config file is newer than existing config." + echo "replacing existing file by new config" + + CONFIG_BACKUP="/app/conf/config_$(date +"%Y%m%d_%H%M%s").json" + + echo "saving existing config as ${CONFIG_BACKUP}" + + cp /app/conf/config.json ${CONFIG_BACKUP} + cp /conf/config.sample.json /app/conf/config.json +fi + +if [ -z "$@" ] ; then + exec "/app/dns-proxy-server" -XX:MaxHeapSize=50m -XX:MaxNewSize=10m +else + exec "$@" +fi diff --git a/config/traefik/dynamic/dashboard.yaml b/config/traefik/dynamic/dashboard.yaml new file mode 100644 index 0000000..9be069b --- /dev/null +++ b/config/traefik/dynamic/dashboard.yaml @@ -0,0 +1,5 @@ +http: + routers: + api: + service: api@internal + rule: "Host(`traefik.vm`)" diff --git a/config/traefik/dynamic/ssl.yaml b/config/traefik/dynamic/ssl.yaml new file mode 100644 index 0000000..3889e63 --- /dev/null +++ b/config/traefik/dynamic/ssl.yaml @@ -0,0 +1,16 @@ +http: + middlewares: + redirect-https: + redirectScheme: + scheme: https + permanent: false + + routers: + https-router: + rule: "!Host(`localhost`)" + priority: 999999 + middlewares: + - redirect-https + service: noop@internal + entrypoints: + - http diff --git a/config/traefik/traefik.yml b/config/traefik/traefik.yml new file mode 100644 index 0000000..61d36ba --- /dev/null +++ b/config/traefik/traefik.yml @@ -0,0 +1,37 @@ +log: + level: INFO + +api: + dashboard: true + disableDashboardAd: true + +entryPoints: + http: + address: :80 + + https: + address: :443 + asDefault: true + http: + tls: + certResolver: step + +providers: + file: + directory: /etc/traefik/dynamic + watch: true + + docker: + defaultRule: | + Host(`{{ trim (index .Labels "serviceName") }}.dev.local`) {{range $i, $domain := splitList "," (index .Labels "serviceDomains")}}{{if ne $domain ""}}|| Host(`{{$domain}}`){{end}}{{end}} + constraints: LabelRegex(`serviceName`, `.+`) && !Label(`com.docker.compose.oneoff`, `True`) + +certificatesResolvers: + step: + acme: + caServer: https://localhost:9000/acme/acme/directory + certificatesDuration: 24 + email: dev@example.com + storage: /traefik/certs.json + httpChallenge: + entryPoint: http diff --git a/services/.gitignore b/services/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/services/docker-compose.yml b/services/docker-compose.yml new file mode 100644 index 0000000..af56844 --- /dev/null +++ b/services/docker-compose.yml @@ -0,0 +1,67 @@ +services: + mailpit: + profiles: + - mail + image: axllent/mailpit + ports: + - 1025:1025 + environment: + MP_UI_BIND_ADDR: "0.0.0.0:8085" + labels: + serviceName: mail + traefik.http.services.mail.loadbalancer.server.port: 8085 + + datasette: + ports: + - 8001:8001 + volumes: + - datasette:/mnt + image: datasetteproject/datasette + command: datasette -p 8001 -h 0.0.0.0 --plugins-dir=/mnt/plugins/ --config default_page_size:500 /mnt/data/qs-monitor-usage.db + profiles: + - data + labels: + serviceName: data + traefik.http.services.data.loadbalancer.server.port: 8001 + + silverbullet: + profiles: + - notes + image: ghcr.io/silverbulletmd/silverbullet:v2 + volumes: + - ~/Notes:/space + labels: + serviceName: notes + + db: + profiles: + - db + build: docker + image: dbgate/dbgate:beta-alpine + labels: + serviceName: db + volumes: + - dbgate:/root/.dbgate + + grafana: + profiles: + - logs + image: grafana/grafana:11.2.0 + environment: + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_BASIC_ENABLED=false + - GF_FEATURE_TOGGLES_ENABLE=accessControlOnCall lokiLogsDataplane exploreLogsShardSplitting + - GF_PLUGINS_PREINSTALL_DISABLED=true + - GF_INSTALL_PLUGINS=https://storage.googleapis.com/integration-artifacts/grafana-lokiexplore-app/grafana-lokiexplore-app-latest.zip;grafana-lokiexplore-app + labels: + serviceName: grafana + volumes: + - ./provisioning/grafana:/etc/grafana/provisioning + extra_hosts: + - 'host.docker.internal:host-gateway' + + +volumes: + dbgate: ~ + datasette: ~ diff --git a/services/provisioning/grafana/datasources/default.yaml b/services/provisioning/grafana/datasources/default.yaml new file mode 100644 index 0000000..c8421c8 --- /dev/null +++ b/services/provisioning/grafana/datasources/default.yaml @@ -0,0 +1,8 @@ +apiVersion: 1 + +datasources: + - name: gdev-loki + type: loki + uid: gdev-loki + access: proxy + url: http://host.docker.internal:3100 diff --git a/services/provisioning/grafana/plugins/app.yaml b/services/provisioning/grafana/plugins/app.yaml new file mode 100644 index 0000000..09568b9 --- /dev/null +++ b/services/provisioning/grafana/plugins/app.yaml @@ -0,0 +1,12 @@ +apiVersion: 1 + +apps: + - type: "grafana-lokiexplore-app" + org_id: 1 + org_name: "Grafana" + disabled: false + jsonData: + apiUrl: http://default-url.com + isApiKeySet: true + secureJsonData: + apiKey: secret-key