Sign inSign up

andelink/docker-tailnet-edge:homepage

Manifest digest

sha256:1f4474de681c22ba97a79d1e95ba1ad8a42ceb440b0063481fd0a36d43f3a824

Last pushed

3 months by andelink

Type

Compose

Manifest digest

sha256:1f4474de681c22ba97a79d1e95ba1ad8a42ceb440b0063481fd0a36d43f3a824

Compose file content

name: ${COMPOSE_PROJECT_NAME:-xxx}

# # This block is used to get compose to fail fast if these env vars are missing
# x-required:
#   - ${TS_CLIENT_ID:?see Trust Credentials in admin UI}
#   - ${TS_CLIENT_SECRET:?see Trust Credentials in admin UI}

services:
  tailscale:
    restart: unless-stopped
    image: tailscale/tailscale:${TS_VERSION:-stable}
    pull_policy: every_1w
    hostname: &hostname ${TS_HOSTNAME:-traefik}-${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+-test}
    environment:
      TZ: ${TZ:-America/Los_Angeles}
      PUID: ${PUID:-1000}
      PGID: ${PGID:-1000}
      TS_HOSTNAME: *hostname
      TS_ACCEPT_DNS: ${TS_ACCEPT_DNS:-true}
      TS_AUTH_ONCE: ${TS_AUTH_ONCE:-false}
      TS_USERSPACE: ${TS_USERSPACE:-false}
      TS_STATE_DIR: ${TS_STATE_DIR:-/var/lib/tailscale}
      TS_LOCAL_ADDR_PORT: ${TS_LOCAL_ADDR_PORT:-127.0.0.1:9002}
      TS_ENABLE_HEALTH_CHECK: ${TS_ENABLE_HEALTH_CHECK:-false}
      TS_ENABLE_METRICS: ${TS_ENABLE_METRICS:-true}
      TS_NO_LOGS_NO_SUPPORT: true
      TS_TAILSCALED_EXTRA_ARGS: --no-logs-no-support --verbose=${TS_TAILSCALED_VERBOSITY:-2}
      TS_EXTRA_ARGS: >-
          --accept-dns=${TS_ACCEPT_DNS:-true}
          --accept-routes=${TS_ACCEPT_ROUTES:-true}
          --advertise-tags=${TS_ADVERTISE_TAGS:-tag:container,tag:proxy}
          --auth-key=file:/run/secrets/ts-client-secret
    ports:
      - 80:80       # traefik web
      - 443:443     # traefik websecure
      - 8080:8080   # traefik dashboard
      - 8082:8082   # dnscrypt ui
    networks:
      - frontend
    secrets:
      - ts-client-id
      - ts-client-secret
    volumes:
      - ts-state:${TS_STATE_DIR:-/var/lib/tailscale}
      - ts-binary:/usr/local/bin
      - ts-socket:/tmp
    devices:
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - net_admin
      - net_raw
    # TODO: add check that tailscale status --json | jq '.Self.DNSName' == TS_HOSTNAME
    # TODO: in tsapi, use DELETE /device/{id} to prevent new device name being provisioned
    healthcheck:
      test: ["CMD", "tailscale", "status"]
      start_period: 1m
    labels:
      homepage.group: Edge
      homepage.name: tailscale
      homepage.description: Makes your services accessible from your tailnet (via Traefik)
      homepage.siteMonitor: http://${COMPOSE_PROJECT_NAME:-xxx}-tailscale-1

volumes:
  ts-state:
  ts-socket:
  ts-binary:

secrets:
  ts-client-id:
    environment: TS_CLIENT_ID
  ts-client-secret:
    environment: TS_CLIENT_SECRET

networks:
  frontend:
    name: frontend
    driver: bridge
    attachable: true
    driver_opts:
      com.docker.network.bridge.host_binding_ipv4: 127.0.0.1
# May consider also using this to copy default traefik and dnscrypt configs into a volume to attach
# to their respective service, which would allow consumers to more easily apply custom settings via
# their own volumes or bind mounts.
# Also consider using the tailscale API to remove devices with the same TS_HOSTNAME.

services:
  dnssplit:
    network_mode: service:tailscale
    restart: unless-stopped
    image: docker.io/andelink/docker-tailnet-edge:latest
    pull_policy: every_1h
    deploy:
      resources:
        limits:
          cpus: 0.5
          memory: 16M
    environment:
      TS_HOSTNAME: ${TS_HOSTNAME:-traefik}-${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+-test}
      SPLIT_DNS_DOMAIN: ${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+.test}   # e.g. "home" or, if TEST=1, "home.test"
      SPLIT_DNS_NAMESERVER: ${SPLIT_DNS_NAMESERVER:-}                   # Default will auto-detect via local API
    secrets:
      # defined in compose.tailscale.yaml
      - ts-client-id
      - ts-client-secret
    volumes:
      - ts-socket:/var/run/tailscale
      - ts-binary:/usr/local/bin
    depends_on:
      tailscale:
        condition: service_healthy
    labels:
      homepage.group: Edge
      homepage.name: dnssplit
      homepage.description: Configures split DNS for ${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+.test}
      homepage.siteMonitor: http://${COMPOSE_PROJECT_NAME:-xxx}-dnssplit-1
services:
  dnscrypt:
    image: klutchell/dnscrypt-proxy:${DNSCRYPT_VERSION:-latest}
    restart: unless-stopped
    network_mode: service:tailscale
    depends_on:
      tailscale:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "dnsprobe", "google.com", "127.0.0.1:53"]
      start_period: 15s
      interval: 5s
    environment:
      TZ: ${TZ:-America/Los_Angeles}
      PUID: ${PUID:-1000}
      PGID: ${PGID:-1000}
      TS_HOSTNAME: ${TS_HOSTNAME:-traefik}-${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+-test}
    volumes:
      - dnscrypt-config:/config
      - /etc/localtime:/etc/localtime:ro
    configs:
      - source: dnscrypt-config
        target: /config/dnscrypt-proxy.toml
      - source: dnscrypt-cloaking
        target: /config/cloaking-rules.txt
      - source: dnscrypt-forwarding
        target: /config/forwarding-rules.txt
    labels:
      traefik.enable: true
      traefik.hostname: dnscrypt
      traefik.http.services.dnscrypt.loadbalancer.server.port: 8082
      traefik.http.routers.dnscrypt.tls.domains[0].main: "${TS_HOSTNAME:-traefik}-${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+-test}.${TS_DOMAIN:-porgy-little.ts.net}"
      traefik.http.routers.dnscrypt.tls.domains[0].sans: "*${TS_HOSTNAME:-traefik}-${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+-test}.${TS_DOMAIN:-porgy-little.ts.net}"
      homepage.group: Edge
      homepage.name: dnscrypt
      homepage.description: Small DNS proxy to forward *.${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+.test} traffic
      homepage.href: http://dnscrypt.${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+.test}
      homepage.siteMonitor: http://${COMPOSE_PROJECT_NAME:-xxx}-dnscrypt-1

volumes:
  dnscrypt-config:

configs:
  dnscrypt-forwarding:
    content: |
        ts.net  100.100.100.100
  dnscrypt-cloaking:
    # TODO: hydrate this with the real provisioned tailscale hostname and error on duplicates/increments
    content: |
        *.${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+.test}    ${TS_HOSTNAME:-traefik}-${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+-test}.${TS_DOMAIN:-porgy-little.ts.net}
  dnscrypt-config:
    content: |
        listen_addresses = ['0.0.0.0:53', '0.0.0.0:5053']
        cloaking_rules = '/config/cloaking-rules.txt'
        forwarding_rules = '/config/forwarding-rules.txt'
        ipv6_servers = false
        dnscrypt_servers = true
        doh_servers = true
        require_nolog = true
        require_dnssec = true
        require_nofilter = true
        log_file = '/config/dnscrypt.log'
        log_level = ${DNSCRYPT_LOG_LEVEL:-2}   # 0 verbose - 6 fatal only
        log_file_latest = false
        log_files_max_backups = 3
        bootstrap_resolvers = ['9.9.9.11:53', '9.9.9.9:53']
        [monitoring_ui]
          enabled = true
          username = ''
          password = ''
          enable_query_log = true
          listen_address = '0.0.0.0:8082'
        [query_log]
          format = 'tsv'
          ignored_qtypes = []
          file = '/config/dnscrypt.query.log'
        [nx_log]
          file = '/config/dnscrypt.nx.log'
        [allowed_names]
          log_file = '/config/dnscrypt.allowed-names.log'
          # allowed_names_file = '/config/allowed-names.txt'
        [allowed_ips]
          log_file = '/config/dnscrypt.allowed-ips.log'
          # allowed_ips_file = '/config/allowed-ips.txt'
        [blocked_names]
          log_file = '/config/dnscrypt.blocked-names.log'
          # blocked_names_file = '/config/blocked-names.txt'
        [blocked_ips]
          log_file = '/config/dnscrypt.blocked-ips.log'
          # blocked_ips_file = '/config/blocked-ips.txt'
        [sources.'public-resolvers']
          urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md', 'https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md', 'https://cdn.jsdelivr.net/gh/DNSCrypt/dnscrypt-resolvers@master/v3/public-resolvers.md']
          minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
          cache_file = 'public-resolvers.md'
services:
  traefik:
    image: traefik:${TRAEFIK_VERSION:-v3.7}
    restart: unless-stopped
    network_mode: service:tailscale
    security_opt:
      - no-new-privileges=true
    deploy:
      resources:
        limits:
          cpus: 0.5
          memory: 256M
    depends_on:
      tailscale:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "traefik", "healthcheck"]
      start_period: 1m
    environment:
      TZ: ${TZ:-America/Los_Angeles}
      PUID: ${PUID:-1000}
      PGID: ${PGID:-1000}
      TS_DOMAIN: ${TS_DOMAIN:-porgy-little.ts.net}
      TS_HOSTNAME: ${TS_HOSTNAME:-traefik}-${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+-test}
      SPLIT_DNS_DOMAIN: ${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+.test}
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - traefik-config:/etc/traefik
      - traefik-dynamic:/etc/traefik/dynamic/dynamic.d
    configs:
      - source: traefik-static
        target: /etc/traefik/traefik.yml
      - source: traefik-dynamic
        target: /etc/traefik/dynamic/services.yml
    labels:
      traefik.enable: true
      traefik.hostname: traefik
      traefik.http.routers.dashboard.service: api@internal
      traefik.http.services.dashboard.loadbalancer.server.port: 8080
      traefik.http.routers.dashboard.tls.domains[0].main: "${TS_HOSTNAME:-traefik}-${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+-test}.${TS_DOMAIN:-porgy-little.ts.net}"
      traefik.http.routers.dashboard.tls.domains[0].sans: "*${TS_HOSTNAME:-traefik}-${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+-test}.${TS_DOMAIN:-porgy-little.ts.net}"
      homepage.group: Edge
      homepage.name: traefik
      homepage.description: Reverse proxy requests to the appropriate service endpoint
      homepage.href: http://traefik.${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+.test}
      homepage.siteMonitor: http://${COMPOSE_PROJECT_NAME:-xxx}-traefik-1

volumes:
  traefik-config:
  traefik-dynamic:

configs:
  traefik-dynamic:
    content: |-
      http:
        middlewares:
            wwwremover:
              redirectRegex:
                  regex: ^(https?)://(?:www\.)?(.+)
                  replacement: $${1}://$${2}
                  permanent: true
            rr-strip:
              stripPrefixRegex:
                  regex:
                    - "^/rr/[^/]+"
            rd-redirect:
              redirectRegex:
                  regex: ^(https?)://[^/]+/rd/([^/]+)(.*)
                  replacement: $${1}://$${2}.${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+.test}$${3}
                  permanent: false

  traefik-static:
    content: |-
      ping: {}

      global:
        checkNewVersion: false
        sendAnonymousUsage: false

      log:
        filePath: /etc/traefik/traefik.log.json
        format: json
        level: ${TRAEFIK_LOG_LEVEL:-INFO}
        compress: true
        maxBackups: 30
        maxAge: 7

      accesslog:
        format: json
        filepath: /etc/traefik/traefik.access.log.json
        dualOutput: true
        bufferingSize: 10
        addInternals: true
        filters:
          retryAttempts: true
          minDuration: 10ms
        fields:
          defaultMode: keep
          headers:
            defaultMode: keep
            names:
              Authorization: drop

      api:
        dashboard: true
        insecure: ${TRAEFIK_API_INSECURE:-true}
        debug: ${TRAEFIK_API_DEBUG:-false}
        disableDashboardAd: true

      experimental:
        fastProxy: {}

      certificatesResolvers:
        tsresolver:
          tailscale: {}

      entryPoints:
        web:
          address: ":80"
          http:
            redirections:
              entryPoint:
                to: websecure
                scheme: https
                permanent: true

        websecure:
          address: ":443"
          http:
            tls:
              certResolver: tsresolver
            middlewares:
              - wwwremover@file
              - rr-strip@file
              - rd-redirect@file

      providers:
        # # I have an http provider running on my laptop here
        # http:
        #   endpoint: http://host.docker.internal:8192
        #   pollInterval: 5s
        file:
          directory: /etc/traefik/dynamic
          watch: true
        docker:
          exposedByDefault: ${TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT:-${TRAEFIK_EXPOSEDBYDEFAULT:-false}}
          defaultRule: >-
            {{ $$x := without (splitList "-" (normalize .ContainerName | trimSuffix "-1")) "app" | uniq | join "-" -}}
            {{ $$y := coalesce (index .Labels "traefik.hostname") $$x -}}
            Host(`{{ $$y }}.{{ env "SPLIT_DNS_DOMAIN" }}`)
            || Host(`{{ $$y }}.localhost`)
            || Host(`www.{{ $$y }}.{{ env "SPLIT_DNS_DOMAIN" }}`)
            || Host(`www.{{ $$y }}.localhost`)
            || (Host(`{{ env "TS_HOSTNAME" }}`) && PathPrefix(`/rr/{{ $$y }}`))
            || (Host(`{{ env "TS_HOSTNAME" }}.localhost`) && PathPrefix(`/rr/{{ $$y }}`))
            || (Host(`{{ env "TS_HOSTNAME" }}.{{ env "TS_DOMAIN" }}`) && PathPrefix(`/rr/{{ $$y }}`))
            || (Host(`{{ env "TS_HOSTNAME" }}`) && PathPrefix(`/rd/{{ $$y }}`))
            || (Host(`{{ env "TS_HOSTNAME" }}.localhost`) && PathPrefix(`/rd/{{ $$y }}`))
            || (Host(`{{ env "TS_HOSTNAME" }}.{{ env "TS_DOMAIN" }}`) && PathPrefix(`/rd/{{ $$y }}`))
services:
  homepage:
    profiles: [homepage]
    image: ghcr.io/gethomepage/homepage:${HOMEPAGE_VERSION:-v1.13}
    restart: unless-stopped
    network_mode: service:tailscale
    environment:
      LOG_TARGETS: both
      HOMEPAGE_ALLOWED_HOSTS: "*"
      HOMEPAGE_VAR_NAME: homepage
      HOMEPAGE_VAR_SPLIT_DNS_DOMAIN: ${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+.test}
      HOMEPAGE_VAR_GREETING: ${HOMEPAGE_VAR_GREETING:-Shaka, ${SPLIT_DNS_DOMAIN:-${LOGNAME}}${TEST:+.test} 🤙}
      HOMEPAGE_VAR_BACKGROUND_IMAGE: ${HOMEPAGE_VAR_BACKGROUND_IMAGE:-https://images.unsplash.com/vector-1777336971484-ac9da9a9b837?q=80&w=1932&auto=format&fit=crop}
    labels:
      traefik.enable: true
      traefik.hostname: homepage
      homepage.group: Edge
      homepage.name: "{{HOMEPAGE_VAR_NAME}}"
      homepage.description: Personal homepage dashboard
      homepage.href: http://{{HOMEPAGE_VAR_NAME}}.{{HOMEPAGE_VAR_SPLIT_DNS_DOMAIN}}
      homepage.siteMonitor: http://${COMPOSE_PROJECT_NAME:-xxx}-homepage-1
    volumes:
      - homepage-config:/app/config
      - /var/run/docker.sock:/var/run/docker.sock:ro
    configs:
      - source: homepage-settings
        target: /app/config/settings.yaml
      - source: homepage-docker
        target: /app/config/docker.yaml
      - source: homepage-bookmarks
        target: /app/config/bookmarks.yaml
      - source: homepage-widgets
        target: /app/config/widgets.yaml
      - source: homepage-services
        target: /app/config/services.yaml

volumes:
  homepage-config:

configs:
  homepage-docker:
    content: |-
      docker:
        socket: /var/run/docker.sock

  homepage-settings:
    content: |-
      hideErrors: false
      disableIndexing: true
      disableUpdateCheck: true

      # https://unsplash.com/illustrations
      # background: https://images.unsplash.com/photo-1619199003576-7cf4e1f7b25f?q=80&w=1533&auto=format&fit=crop
      # background: https://plus.unsplash.com/premium_photo-1754433115781-a0f536b10258?q=80&w=1450&auto=format&fit=crop
      # background: https://images.unsplash.com/photo-1729575846511-f499d2e17d79?q=80&w=1932&auto=format&fit=crop
      # background: https://images.unsplash.com/photo-1528459801416-a9e53bbf4e17?q=80&w=1932&auto=format&fit=crop
      # background: https://images.unsplash.com/photo-1579547621706-1a9c79d5c9f1?q=80&w=1932&auto=format&fit=crop
      # background: https://images.unsplash.com/vector-1777336946040-0b175274f345?q=80&w=1932&auto=format&fit=crop
      # background: https://images.unsplash.com/vector-1781014333122-59e169b6ecda?q=80&w=1932&auto=format&fit=crop
      # background: https://images.unsplash.com/vector-1779081173932-5b2f732aa43a?q=80&w=1932&auto=format&fit=crop
      background: {{HOMEPAGE_BACKGROUND_IMAGE}}

      # blur: sm # sm, "", md, xl... see https://tailwindcss.com/docs/backdrop-blur
      # saturate: 50 # 0, 50, 100... see https://tailwindcss.com/docs/backdrop-saturate
      # brightness: 50 # 0, 50, 75... see https://tailwindcss.com/docs/backdrop-brightness
      opacity: 50

      language: en
      target: _blank
      instanceName: {{HOMEPAGE_VAR_SPLIT_DNS_DOMAIN}}

      title: Homelab Dashboard
      description: Homelab landing page

      quicklaunch:
        searchDescriptions: true
        hideInternetSearch: false
        hideVisitUrl: false

      headerStyle: underlined
      # headerStyle: clean
      # headerStyle: boxed
      # headerStyle: boxedWidgets

      # https://gethomepage.dev/configs/settings/#layout
      # layout:
      #   Media:
      #     style: row
      #     columns: 4
      useEqualHeights: true

  homepage-bookmarks:
    content: |-
      - Developer:
        - Tailscale:
          - abbr: TS
            href: https://login.tailscale.com/admin/
            description: Tailscale admin console

        - GitHub:
          - abbr: GH
            href: https://github.com/emerald-oysters/
            description: Emerald Oysters GitHub

      - Tech:
        - Hacker News:
          - abbr: HN
            href: https://news.ycombinator.com/
            description: Anything that good hackers would find interesting

  homepage-widgets:
    content: |-
      - greeting:
          text_size: 2xl
          text: Shaka, brother 🤙
          text: {{HOMEPAGE_VAR_GREETING}}

      - resources:
          cpu: true
          memory: true
          uptime: false
          network: true
          disk: /

      - search:
          provider: custom
          url: https://kagi.com/search?q=
          suggestionUrl: https://kagi.com/api/autosuggest?q=
          showSearchSuggestions: true
          target: _blank

      # - datetime:
      #     text_size: xl
      #     locale: sv
      #     # format:
      #     #   timeStyle: short

      # - openmeteo:
      #     label: Seattle
      #     latitude: 47.6062
      #     longitude: -122.3321
      #     timezone: America/Los_Angeles
      #     units: imperial
      #     cache: 5
      #     format:
      #       maximumFractionDigits: 1

  homepage-services:
    content: |-
      # - My First Group:
      #   - My First Service:
      #     href: http://localhost/
      #     description: Homepage is awesome

      # - My Second Group:
      #   - My Second Service:
      #     href: http://localhost/
      #     description: Homepage is the best

      # - My Third Group:
      #   - My Third Service:
      #       href: http://localhost/
      #       description: Homepage is 😎

Docker commands

docker compose -f oci://andelink/docker-tailnet-edge:homepage up

Use the above command to pull and run the Compose file. Learn more.

Images used

Image + 1 more

Traefik, The Cloud Native Edge Router


Pulls

1B+

Stars

3665

Last Updated

about 22 hours

Image

Connect your devices and users together in your own secure virtual private network.


Pulls

100M+

Stars

366

Last Updated

about 10 hours

Image


Pulls

1M+

Stars

23

Last Updated

2 days

Image

A highly customizable homepage with Docker and service API integrations.


Pulls

1M+

Stars

42

Last Updated

about 8 hours

Image + 1 more


Pulls

1.4K

Stars

0

Last Updated

3 months