├── .drone.yml ├── .env.example ├── .gitignore ├── README.md ├── docker-compose.yml ├── docs ├── config.boot ├── edgerouter-backups.md ├── images │ ├── cfapi.png │ ├── pihole-advanced.png │ ├── pihole-dns.png │ ├── router-dhcp.png │ ├── router-dnsmasq.png │ └── router-system.png ├── lan-only-routes.md ├── pihole-dnsmasq.md ├── ubuntu-expand-lvm.md ├── wildcard-certs.md └── wireguard-question.md ├── etc ├── authconfig.ini ├── clickhouse │ ├── clickhouse-config.xml │ └── clickhouse-user-config.xml ├── mdadm.conf ├── sshd_config ├── traefik-logrotate.conf ├── traefik │ └── rules-fail2ban.yml └── unbound.conf ├── logrotate.service ├── logrotate.timer ├── media-4tb.mount ├── media-primary.mount ├── media-secondary.mount ├── media-wildy.mount └── wildy.compose.yml /.drone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | type: docker 4 | name: deploy 5 | 6 | steps: 7 | - name: ssh commands 8 | image: appleboy/drone-ssh 9 | settings: 10 | port: 22 11 | host: 12 | from_secret: ssh_host 13 | username: 14 | from_secret: ssh_user 15 | key: 16 | from_secret: ssh_key 17 | script_stop: true 18 | script: 19 | - source .profile 20 | - cd selfhosted 21 | - git pull 22 | - docker compose config 23 | - docker compose up -d --remove-orphans 24 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | COMPOSE_PROJECT_NAME="stack" 2 | 3 | # Letsencrypt email isn't associated with any account. 4 | # You can put anything here 5 | LETSENCRYPT_EMAIL=letsencrypt@subdavis.com 6 | 7 | # Your naked domain. 8 | DNS_DOMAIN=subdavis.com 9 | DNS_DOMAIN_ZONE_ID=94618f842289a88454b75dba13d9204b 10 | 11 | # The particular DNS record to update with an A record for dyndns 12 | SUBDOMAIN=core 13 | 14 | # Optional pilot token 15 | TRAEFIK_PILOT_TOKEN=CHANGEME 16 | 17 | # Your local subnet, for optional IP Whitelisting in Traefik 18 | # https://tools.ietf.org/html/rfc1918 19 | SUBNET="192.168.0.0/16,10.0.0.0/8,172.16.0.0/12" 20 | LOCAL_NETWORK="192.168.52.0/20" 21 | 22 | # Mount locations for external media 23 | PRIMARY_MOUNT=/media/primary 24 | SECONDARY_MOUNT=/media/secondary 25 | MEDIA_MOUNT=/media/4tb 26 | LOCAL_MOUNT=/media/local 27 | TIME_ZONE="America/New_York" 28 | 29 | # Docker socket path 30 | SOCK_PATH=/run/user/1000/docker.sock 31 | 32 | # Everything below here is mostly API keys and passwords. 33 | # Often, more than 1 container needs a given secret. 34 | # I could have put each of these in a separate unit conf directory 35 | # But I was too lazy 36 | AUTH_PROVIDERS_GOOGLE_CLIENT_ID=CHANGEME 37 | AUTH_PROVIDERS_GOOGLE_CLIENT_SECRET=CHANGEME 38 | AUTH_SECRET=CHANGEME 39 | AUTH_WHITELIST="CHANGEME@CHANGEME,ANOTHER@CHANGEME" 40 | 41 | # You'll use these as your AWS ACCESS KEY and AWS SECRET KEY, respectively 42 | MINIO_ACCESS_KEY=CHANGEME 43 | MINIO_SECRET_KEY=CHANGEME 44 | 45 | # For FileRun 46 | FR_DB_USER=filerun 47 | FR_DB_PASS=CHANGEME 48 | FR_DB_ROOT_PW=CHANGEME 49 | 50 | # For DYNDNS and TRAEFIK DNS ACME auth 51 | CF_EMAIL=CHANGEME 52 | CF_TOKEN=CHANGEME 53 | 54 | # PUID/PGID for linuxserver images 55 | XID=1000 56 | # Transition to safer uid 57 | PUID=1001 58 | 59 | # For DRONE 60 | DRONE_GITHUB_CLIENT_ID=CHANGEME 61 | DRONE_GITHUB_CLIENT_SECRET=CHANGEME 62 | DRONE_RPC_SECRET=CHANGEME 63 | # restrict login to me, set me as user 64 | DRONE_USER_FILTER=subdavis 65 | DRONE_USER_CREATE=username:subdavis,admin:true 66 | 67 | # For torrent 68 | OPENVPN_PROVIDER=NORDVPN 69 | TORRENT_USERNAME=CHANGEME 70 | TORRENT_PASSWORD=CHANGEME 71 | 72 | # For plausible 73 | PLAUSIBLE_SECRET_KEY=CHANGEME 74 | PLAUSIBLE_TOTP_KEY=CHANGEME 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.wants/ 2 | *.service.d/*.conf 3 | profile.env 4 | .env.prod 5 | .env 6 | etc/passwords.txt 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker selfhosted services 2 | 3 | > with docker-compose and traefik 4 | 5 | ![Uptime Robot ratio (30 days)](https://img.shields.io/uptimerobot/ratio/m784171038-19b52e00f52a8d916ba46346) 6 | [![Build Status](https://drone.subdavis.com/api/badges/subdavis/selfhosted/status.svg)](https://drone.subdavis.com/subdavis/selfhosted) 7 | 8 | This repo contains my production docker services accessible from anywhere over HTTPS using [traefik](https://traefik.io). These services (and others) run on a single server. **It used to be [rootless-mode](https://docs.docker.com/engine/security/rootless/)** but slirp4net was too slow and too much of the docker advanced configuration (permissions flags, mostly) were missing. 9 | 10 | * Jellyfin 11 | * Sonarr, Radarr, Prowlarr 12 | * Calibre Web 13 | * Kobo book downloader (kobodl) 14 | * Transmission torrent server 15 | * AdGuard Home DNS 16 | * Drone CI and runner 17 | * Duplicati 18 | * Watchtower 19 | * Cloudflare DNS Automation 20 | * Portainer 21 | 22 | # Documentation 23 | 24 | I've also written some intermediate to advanced generic usage docs for traefik, docker, pihole, and home networking. These articles are generally applicable, but some may be more useful than others. 25 | 26 | * [Configuring Wildcard Certs for Traefik](docs/wildcard-certs.md) 27 | * [LAN-only Traefik Routing with ACME SSL](docs/lan-only-routes.md) 28 | * [Configuring PiHole with dnsmasq](docs/pihole-dnsmasq.md) 29 | * [EdgeRouter Backups over SSH (SCP)](docs/edgerouter-backups.md) 30 | * [Expand LVM to fill remaining disk](docs/ubuntu-expand-lvm.md) 31 | 32 | More great documentation. 33 | 34 | * https://www.smarthomebeginner.com/traefik-2-docker-tutorial/ 35 | * https://github.com/isaacrlevin/HomeNetworkSetup 36 | * https://github.com/htpcBeginner/docker-traefik 37 | 38 | ## Prerequisites 39 | 40 | * A recent version of ubuntu server with `Docker CE` installed (see below) 41 | * A router or firewall capable of dnsmasq. I use a Ubiquiti EdgeRouter X. 42 | * A domain name. 43 | * A cloudflare account. 44 | 45 | ### Home network prep 46 | 47 | * You need to make sure that ports 80 and 443 are port-forwarded through your router to whatever host this will be on. 48 | * Your server should be assigned a static private IP by DNS. `ifconfig` will list your interfaces. 49 | * Refer to the [docker-pi-hole](https://github.com/pi-hole/docker-pi-hole) docs and [my docs](docs/pihole-dnsmasq.md) for further network setup related to that service. Even though I use AdGuard Home, those docs are relevant. 50 | 51 | ### DNS Configuration 52 | 53 | **UPDATE**: This is now done automatically with [Docker Traefik Cloudflare Companion](https://github.com/tiredofit/docker-traefik-cloudflare-companion). Instructions below are left as an explanation of how this works. 54 | 55 | In this setup, each container's service will serve from a different subdomain of your Cloudflare hosted zone dyndns subdomain. 56 | 57 | * Create an `A` record for `core.mydomain.com` to point to your public IP. 58 | * For each service, you'll need to create CNAME records for each `service.mydomain.com` to point to `core.mydomain.com` because all of your services are running on the same host but the host needs to be able to do virtual host routing based on domain name. 59 | * Your services will be publically available on `https://servicename.mydomain.com`. 60 | 61 | ### Dynamic DNS (recommended) 62 | 63 | Resolving the IP address of your home network is annoying because most DNS providers change your IP every now and again. Services like No-IP combat this, but they aren't the most reliable. However, setting DNS programatically is pretty easy with Cloudflare API. 64 | 65 | * Follow the instructions in [Configuring Wildcard Certs for Traefik](docs/wildcard-certs.md) to get this part set up. 66 | * You'll need to modify `.env` with your domain info, ACME email, and cloudflare API tokens. 67 | 68 | ## Installation 69 | 70 | 1. start with ubuntu lts 71 | 1. [Enable Unattended Upgrades](https://help.ubuntu.com/community/AutomaticSecurityUpdates) 72 | 1. clone this repo 73 | 1. Sign into any private docker registries 74 | 1. install [docker](https://docs.docker.com/engine/install/) 75 | a [Understanding UID remapping](https://medium.com/@tonistiigi/experimenting-with-rootless-docker-416c9ad8c0d6) 76 | a. ignore the env exports it says to set, see below 77 | 1. make sure `UsePAM yes` is set in `/etc/ssh/sshd_config` [read more](https://superuser.com/questions/1561076/systemctl-use-failed-to-connect-to-bus-no-such-file-or-directory-debian-9) 78 | 79 | ```bash 80 | cd selfhosted 81 | cp .env.example .env # edit this 82 | 83 | # make mount points 84 | mkdir /media/local /media/primary /media/secondary 85 | 86 | # install mounts 87 | systemctl link media-primary.mount 88 | systemctl link media-secondary.mount 89 | 90 | # install logrotate 91 | systenctl --user link $HOME/selfhosted/logrotate.timer 92 | systenctl --user link $HOME/selfhosted/logrotate.service 93 | systemctl --user enable logrotate.timer --now 94 | 95 | # enable traefik logrotate 96 | cp etc/traefik-logrotate.conf /etc/logrotate.d/traefik 97 | 98 | # Add to .profile 99 | # export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock 100 | nano .profile 101 | ``` 102 | 103 | [Set up docker daemon.json](https://forums.docker.com/t/rootless-docker-ip-range-conflicts/103341). Otherwise, you may end up with subnet ranges inside your containers that overlap with the real LAN and make hosts unreachable. 104 | 105 | ``` json 106 | { 107 | "default-address-pools": [ 108 | {"base":"172.16.0.0/16","size":24}, 109 | {"base":"172.20.0.0/16","size":24} 110 | ] 111 | } 112 | ``` 113 | 114 | Edit `/lib/systemd/system/user@.service` to include dependencies on mounts 115 | 116 | ```conf 117 | [Unit] 118 | Requires=user-runtime-dir@%i.service media-primary.mount media-secondary.mount 119 | ``` 120 | 121 | ## Automatic deployments and drone 122 | 123 | * Create a github api app. Follow drone setup instructions. 124 | * Make sure the user filtering config is set correctly so other users can't log in 125 | * Add secrets `ssh_key`, `ssh_host`, `ssh_user` for your deploy user. 126 | * Open `drone.yourdomain.com` and finish configuring your repo. 127 | 128 | ## Adguard DNS 129 | 130 | You may need to disable ubuntu's default dns service and remove resolf.conf [read more](https://www.smarthomebeginner.com/run-pihole-in-docker-on-ubuntu-with-reverse-proxy/). 131 | 132 | After disabling `systemd-resolved.service`, I ususally set a different DNS server in `/etc/resolv.conf` so that DNS doesn't break when I screw up the stack. 133 | 134 | `systemd-resolve --help` is your friend. 135 | 136 | ## WireGurad and subnet overlap 137 | 138 | * use `wg-quick` for simplicity 139 | * May need to [install or symlink resolvconf](https://superuser.com/questions/1500691/usr-bin-wg-quick-line-31-resolvconf-command-not-found-wireguard-debian) 140 | * Need to avoid [overlapping subnets](https://www.reddit.com/r/WireGuard/comments/bp01ci/connecting_to_services_through_vpn_when_the/). 141 | * Set MTU down to 1280 for issues with cellular networks, on BOTH sides of the connection. 142 | * Update: As of September 19, had to drop to 1250 for TMobile LTE to work.... 143 | 144 | * My subnet is `192.168.48.0/20` 145 | * The mask is `255.255.240.0` 146 | * The default LAN will be `192.168.52.0` 147 | * The gateway is `192.168.52.1` 148 | 149 | ``` 150 | Gateway: 11000000.10101000.0011 | 0100.00000001 151 | Mask: 11111111.11111111.1111 | 0000.00000000 152 | ``` 153 | 154 | * The upper 4 bits will be used for VLANs (16). 155 | * The lower 8 shoud belong to a single VLAN. 156 | 157 | Using wireguard: 158 | 159 | ```bash 160 | sudo systemctl enable wg-quick@peerN --now 161 | ``` 162 | 163 | I have aliases `wgup` and `wgdown` for this in my `.bashrc`. 164 | 165 | ## IPv6 166 | 167 | Some references I encountered while rolling out ipv6. 168 | 169 | [My full edgerouter config](docs/config.boot) 170 | 171 | * [Docker IPV6](https://docs.docker.com/config/daemon/ipv6/) 172 | * [Kernel modules lazy-load ip6tables](https://github.com/moby/moby/issues/33605#issuecomment-307361421) 173 | * `SYS_MODULE` capability doesn't seemt to do it. issuing an `ip6tables` dummy rule worked 174 | * [IPv6 Firewall Rules](https://community.ui.com/questions/Can-someone-let-us-know-the-added-default-IPv6-firewall-rule-mentioned-in-the-new-Edge-OS-2-01/9683f591-6cd2-4677-83c9-e90d2b7c3fbe) 175 | * Must [Block LAN to WLAN Multicast and Broadcast Data for ipv6 over wifi](https://community.ui.com/questions/IPv6-for-UniFi-WiFi/fa7109bb-c33f-4af4-9d98-dc82f0e31d99) 176 | * [You might have to disable some firewall stuff on the upstream ISP gateway](https://community.ui.com/questions/Allow-HTTPs-over-IPv6-in-firewall-Edgemax/c5f00707-4476-4b1b-91d4-7391f73aafa6) 177 | * [Disable ISP IPv6 DNS](https://kazoo.ga/dhcpv6-pd-for-native-ipv6/#) 178 | * `no-dns` in `interface` config for `rdnss` 179 | * I have not been able to get wireguard to route ipv6 traffic under slirp4netns (rootlesss). As a result, it is not possible to connect to wireguard via an ipv6 endpoint. Wireguard for ios prefers ipv4, but the desktop client prefers ipv6. Recommend creating an ipv4-only DNS record such as `wireguard4.domain.com` to force ipv4. 180 | 181 | ## Other useful nonsense 182 | 183 | ```bash 184 | # set own IP, delete set 185 | ifconfig eth0 192.168.1.5 netmask 255.255.255.0 up 186 | ifconfig en1 delete 192.168.1.5 187 | ``` 188 | 189 | 190 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | ############################# 4 | ## ADGUARD - ROOT 5 | ############################# 6 | 7 | adguard: 8 | image: adguard/adguardhome:latest 9 | container_name: adguardhome 10 | restart: unless-stopped 11 | labels: 12 | - traefik.enable=true 13 | - traefik.docker.network=adguard-net 14 | - traefik.http.services.adguard-svc.loadbalancer.server.port=80 15 | - traefik.http.routers.adguard-rtr.rule=Host(`adguard.${DNS_DOMAIN}`) 16 | - traefik.http.routers.adguard-rtr.entrypoints=websecure 17 | - traefik.http.routers.adguard-rtr.tls=true 18 | - traefik.http.routers.adguard-rtr.middlewares=ipwhitelist-mddl@docker 19 | networks: 20 | - adguard-net 21 | ports: 22 | - "53:53/udp" 23 | - "53:53/tcp" 24 | volumes: 25 | - "${LOCAL_MOUNT}/adguard/data:/opt/adguardhome/work" 26 | - "${LOCAL_MOUNT}/adguard/conf:/opt/adguardhome/conf" 27 | dns: "1.1.1.1" 28 | 29 | ############################# 30 | ## ARRS 31 | ############################# 32 | 33 | prowlarr: 34 | image: ghcr.io/linuxserver/prowlarr:develop 35 | container_name: prowlarr 36 | environment: 37 | - PUID=${PUID:-1000} 38 | - PGID=${PUID:-1000} 39 | - TZ=${TIME_ZONE} 40 | volumes: 41 | - ${PRIMARY_MOUNT}/prowlarr/config:/config 42 | labels: 43 | - traefik.enable=true 44 | - traefik.docker.network=arr-net 45 | - traefik.http.services.prowlarr-svc.loadbalancer.server.port=9696 46 | - traefik.http.routers.prowlarr-rtr.rule=Host(`prowlarr.${DNS_DOMAIN}`) 47 | - traefik.http.routers.prowlarr-rtr.entrypoints=websecure 48 | - traefik.http.routers.prowlarr-rtr.tls=true 49 | - traefik.http.routers.prowlarr-rtr.middlewares=ipwhitelist-mddl@docker,traefik-forward-auth@docker 50 | restart: unless-stopped 51 | networks: 52 | - arr-net 53 | 54 | radarr: 55 | image: ghcr.io/linuxserver/radarr:latest 56 | container_name: radarr 57 | restart: unless-stopped 58 | environment: 59 | - PUID=${PUID:-1000} 60 | - PGID=${PUID:-1000} 61 | - TZ=${TIME_ZONE} 62 | labels: 63 | - traefik.enable=true 64 | - traefik.docker.network=arr-net 65 | - traefik.http.services.radarr-svc.loadbalancer.server.port=7878 66 | - traefik.http.routers.radarr-rtr.rule=Host(`radarr.${DNS_DOMAIN}`) 67 | - traefik.http.routers.radarr-rtr.entrypoints=websecure 68 | - traefik.http.routers.radarr-rtr.tls=true 69 | - traefik.http.routers.radarr-rtr.middlewares=ipwhitelist-mddl@docker 70 | volumes: 71 | - ${PRIMARY_MOUNT}/radarr/data:/config 72 | - ${MEDIA_MOUNT}/plex/media/:/data 73 | networks: 74 | - arr-net 75 | 76 | sonarr: 77 | image: ghcr.io/linuxserver/sonarr 78 | container_name: sonarr 79 | restart: unless-stopped 80 | environment: 81 | - PUID=${PUID:-1000} 82 | - PGID=${PUID:-1000} 83 | - TZ=${TIME_ZONE} 84 | labels: 85 | - traefik.enable=true 86 | - traefik.docker.network=arr-net 87 | - traefik.http.services.sonarr-svc.loadbalancer.server.port=8989 88 | - traefik.http.routers.sonarr-rtr.rule=Host(`sonarr.${DNS_DOMAIN}`) 89 | - traefik.http.routers.sonarr-rtr.entrypoints=websecure 90 | - traefik.http.routers.sonarr-rtr.tls=true 91 | - traefik.http.routers.sonarr-rtr.middlewares=ipwhitelist-mddl@docker 92 | volumes: 93 | - ${PRIMARY_MOUNT}/sonarr/data:/config 94 | - ${MEDIA_MOUNT}/plex/media/:/data 95 | networks: 96 | - arr-net 97 | 98 | ############################# 99 | ## CALIBRE 100 | ############################# 101 | 102 | calibre_web: 103 | image: ghcr.io/linuxserver/calibre-web:latest 104 | container_name: calibre_web 105 | restart: unless-stopped 106 | labels: 107 | - traefik.enable=true 108 | - traefik.docker.network=calibre-net 109 | - traefik.http.services.calibre-svc.loadbalancer.server.port=8083 110 | - traefik.http.routers.calibre-rtr.rule=Host(`calibre.${DNS_DOMAIN}`) 111 | - traefik.http.routers.calibre-rtr.entrypoints=websecure 112 | - traefik.http.routers.calibre-rtr.tls=true 113 | - traefik.http.routers.calibre-rtr.middlewares=traefik-forward-auth@docker 114 | environment: 115 | - PUID=${PUID:-1000} 116 | - PGID=${PUID:-1000} 117 | - "TZ=${TIME_ZONE}" 118 | - "DOCKER_MODS=linuxserver/calibre-web:calibre" 119 | volumes: 120 | - ${PRIMARY_MOUNT}/calibre/config:/config 121 | - ${PRIMARY_MOUNT}/calibre/books:/books 122 | networks: 123 | - calibre-net 124 | 125 | ############################# 126 | ## CHANGEDETECTION 127 | ############################# 128 | 129 | changedetection: 130 | image: ghcr.io/dgtlmoon/changedetection.io 131 | container_name: changedetection 132 | restart: unless-stopped 133 | volumes: 134 | - ${SECONDARY_MOUNT}/changedetection/datastore:/datastore 135 | environment: 136 | - PUID=${PUID:-1000} 137 | - PGID=${PUID:-1000} 138 | - WEBDRIVER_URL="http://changedetection-selenium:4444/wd/hub" 139 | labels: 140 | - traefik.enable=true 141 | - traefik.docker.network=changedetection-net 142 | - traefik.http.services.changedetection-svc.loadbalancer.server.port=5000 143 | - traefik.http.routers.changedetection-rtr.rule=Host(`changedetection.${DNS_DOMAIN}`) 144 | - traefik.http.routers.changedetection-rtr.entrypoints=websecure 145 | - traefik.http.routers.changedetection-rtr.tls=true 146 | - traefik.http.routers.changedetection-rtr.middlewares=traefik-forward-auth@docker 147 | networks: 148 | - changedetection-net 149 | - changedetection-private-net 150 | 151 | changedetection-selenium: 152 | image: selenium/standalone-chrome-debug:3.141.59 153 | container_name: changedetection-selenium 154 | restart: unless-stopped 155 | volumes: 156 | # Workaround to avoid the browser crashing inside a docker container 157 | # See https://github.com/SeleniumHQ/docker-selenium#quick-start 158 | - /dev/shm:/dev/shm 159 | shm_size: '2gb' 160 | labels: 161 | - traefik.enable=false 162 | networks: 163 | - changedetection-private-net 164 | 165 | ############################# 166 | ## CLOUDFLARE - ROOT 167 | ############################# 168 | 169 | cloudflare: 170 | image: oznu/cloudflare-ddns:latest 171 | restart: unless-stopped 172 | container_name: cloudflare 173 | labels: 174 | - traefik.enable=false 175 | environment: 176 | - "API_KEY=${CF_TOKEN}" 177 | - "ZONE=${DNS_DOMAIN}" 178 | - "SUBDOMAIN=${SUBDOMAIN}" 179 | - "DNS_SERVER=1.0.0.1" 180 | dns: 1.1.1.1 181 | 182 | ############################# 183 | ## CLOUDFLARE-COMPANION - ROOT 184 | ############################# 185 | 186 | cloudflare-companion: 187 | image: tiredofit/traefik-cloudflare-companion 188 | container_name: cloudflare-companion 189 | restart: unless-stopped 190 | volumes: 191 | - ${SOCK_PATH:-/var/run/docker.sock}:/var/run/docker.sock 192 | environment: 193 | - "TRAEFIK_VERSION=2" 194 | # - "CF_EMAIL=" Leave blank for scopepd 195 | - "CF_TOKEN=${CF_TOKEN}" 196 | - "TARGET_DOMAIN=${SUBDOMAIN}.${DNS_DOMAIN}" 197 | - "DOMAIN1=${DNS_DOMAIN}" 198 | - "DOMAIN1_ZONE_ID=${DNS_DOMAIN_ZONE_ID}" 199 | dns: 1.1.1.1 200 | 201 | ############################# 202 | ## DRONE CI - ROOT 203 | ############################# 204 | 205 | drone: 206 | image: drone/drone:1 207 | restart: unless-stopped 208 | container_name: drone 209 | user: ${PUID:-1000} 210 | labels: 211 | - traefik.enable=true 212 | - traefik.docker.network=drone-net 213 | - traefik.http.services.drone-svc.loadbalancer.server.port=80 214 | - traefik.http.routers.drone-rtr.rule=Host(`drone.${DNS_DOMAIN}`) 215 | - traefik.http.routers.drone-rtr.entrypoints=websecure 216 | - traefik.http.routers.drone-rtr.tls=true 217 | environment: 218 | - DRONE_GITHUB_CLIENT_ID=${DRONE_GITHUB_CLIENT_ID} 219 | - DRONE_GITHUB_CLIENT_SECRET=${DRONE_GITHUB_CLIENT_SECRET} 220 | - DRONE_RPC_SECRET=${DRONE_RPC_SECRET} 221 | - DRONE_SERVER_HOST=drone.${DNS_DOMAIN} 222 | - DRONE_SERVER_PROTO=https 223 | - DRONE_USER_CREATE=${DRONE_USER_CREATE} 224 | - DRONE_USER_FILTER=${DRONE_USER_FILTER} 225 | volumes: 226 | - ${PRIMARY_MOUNT}/drone/server/data:/data 227 | networks: 228 | - drone-net 229 | - drone-private-net 230 | 231 | drone-runner: 232 | image: drone/drone-runner-docker:1 233 | restart: unless-stopped 234 | container_name: drone_runner_1 235 | environment: 236 | - DRONE_RPC_PROTO=http 237 | - DRONE_RPC_HOST=drone 238 | - DRONE_RPC_SECRET=${DRONE_RPC_SECRET} 239 | - DRONE_RUNNER_CAPACITY=2 240 | - DRONE_RUNNER_NAME=drone-runner-1 241 | labels: 242 | - traefik.enable=false 243 | volumes: 244 | - ${SOCK_PATH:-/var/run/docker.sock}:/var/run/docker.sock 245 | networks: 246 | - drone-private-net 247 | 248 | ############################# 249 | ## DUPLICATI 250 | ############################# 251 | 252 | duplicati: 253 | image: ghcr.io/linuxserver/duplicati:latest 254 | container_name: duplicati 255 | restart: unless-stopped 256 | labels: 257 | - traefik.enable=true 258 | - traefik.docker.network=duplicati-net 259 | - traefik.http.services.duplicati-svc.loadbalancer.server.port=8200 260 | - traefik.http.routers.duplicati-rtr.rule=Host(`backups.${DNS_DOMAIN}`) 261 | - traefik.http.routers.duplicati-rtr.entrypoints=websecure 262 | - traefik.http.routers.duplicati-rtr.tls=true 263 | - traefik.http.routers.duplicati-rtr.middlewares=ipwhitelist-mddl@docker,traefik-forward-auth@docker 264 | environment: 265 | - "TZ=${TIME_ZONE}" 266 | - "PUID=${XID:-1000}" 267 | - "PGID=${XID:-1000}" 268 | volumes: 269 | - ${PRIMARY_MOUNT}:/sources/primary 270 | - ${LOCAL_MOUNT}:/sources/local 271 | - ${SECONDARY_MOUNT}/duplicati/config:/config 272 | - ${SECONDARY_MOUNT}/duplicati/backups:/backups 273 | networks: 274 | - duplicati-net 275 | 276 | ############################# 277 | ## JELLYFIN 278 | ############################# 279 | 280 | jellyfin: 281 | image: lscr.io/linuxserver/jellyfin 282 | container_name: jellyfin 283 | restart: unless-stopped 284 | labels: 285 | - traefik.enable=true 286 | - traefik.docker.network=plex-net 287 | - traefik.http.services.jellyfin-svc.loadbalancer.server.port=8096 288 | - traefik.http.routers.jellyfin-rtr.rule=Host(`jellyfin.${DNS_DOMAIN}`) 289 | - traefik.http.routers.jellyfin-rtr.entrypoints=websecure 290 | - traefik.http.routers.jellyfin-rtr.tls=true 291 | environment: 292 | - "TZ=${TIME_ZONE}" 293 | - "PUID=${PUID:-1000}" 294 | - "PGID=${PUID:-1000}" 295 | - "JELLYFIN_PublishedServerUrl=jellyfin.${DNS_DOMAIN}" 296 | volumes: 297 | - ${PRIMARY_MOUNT}/jellyfin/config:/config 298 | - ${MEDIA_MOUNT}/plex/media:/data 299 | devices: 300 | - /dev/dri:/dev/dri 301 | networks: 302 | - plex-net 303 | 304 | ############################# 305 | ## MINIFLUX 306 | ############################# 307 | 308 | miniflux_postgres: 309 | image: postgres:15-alpine 310 | restart: unless-stopped 311 | container_name: miniflux_postgres 312 | volumes: 313 | - ${PRIMARY_MOUNT}/miniflux_postgres_15:/var/lib/postgresql/data 314 | environment: 315 | POSTGRES_DB: miniflux 316 | POSTGRES_USER: miniflux 317 | POSTGRES_PASSWORD: miniflux 318 | networks: 319 | - miniflux-net 320 | healthcheck: 321 | test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] 322 | interval: 5s 323 | timeout: 5s 324 | retries: 5 325 | 326 | miniflux: 327 | image: ghcr.io/miniflux/miniflux:latest 328 | container_name: miniflux 329 | restart: unless-stopped 330 | depends_on: 331 | miniflux_postgres: 332 | condition: service_healthy 333 | healthcheck: 334 | test: ["CMD", "/usr/bin/miniflux", "-healthcheck", "auto"] 335 | interval: 10s 336 | timeout: 5s 337 | retries: 5 338 | environment: 339 | - "BASE_URL=https://miniflux.${DNS_DOMAIN}" 340 | - "DATABASE_URL=postgres://miniflux:miniflux@miniflux_postgres/miniflux?sslmode=disable" 341 | - RUN_MIGRATIONS=1 342 | - CREATE_ADMIN=1 343 | - ADMIN_USERNAME=admin 344 | - ADMIN_PASSWORD=miniflux 345 | volumes: 346 | - ${PRIMARY_MOUNT}/miniflux/config:/config 347 | labels: 348 | - traefik.enable=true 349 | - traefik.docker.network=miniflux-net 350 | - traefik.http.services.miniflux-svc.loadbalancer.server.port=8080 351 | - traefik.http.routers.miniflux-rtr.rule=Host(`miniflux.${DNS_DOMAIN}`) 352 | - traefik.http.routers.miniflux-rtr.entrypoints=websecure 353 | - traefik.http.routers.miniflux-rtr.tls=true 354 | networks: 355 | - miniflux-net 356 | 357 | ############################# 358 | ## KOBODL 359 | ############################# 360 | 361 | kobodl: 362 | image: ghcr.io/subdavis/kobodl:latest 363 | container_name: kobodl 364 | restart: unless-stopped 365 | user: ${PUID:-1000} 366 | labels: 367 | - traefik.enable=true 368 | - traefik.docker.network=kobodl-net 369 | - traefik.http.services.kobodl-svc.loadbalancer.server.port=5000 370 | - traefik.http.routers.kobodl-rtr.rule=Host(`kobodl.${DNS_DOMAIN}`) 371 | - traefik.http.routers.kobodl-rtr.entrypoints=websecure 372 | - traefik.http.routers.kobodl-rtr.tls=true 373 | - traefik.http.routers.kobodl-rtr.middlewares=traefik-forward-auth@docker 374 | volumes: 375 | - ${PRIMARY_MOUNT}/kobodl/kobodl.json:/home/kobodl.json 376 | - ${PRIMARY_MOUNT}/kobodl/downloads:/home/downloads 377 | command: --config /home/kobodl.json serve -h 0.0.0.0 --output-dir /home/downloads 378 | networks: 379 | - kobodl-net 380 | 381 | ############################# 382 | ## PORTAINER - ROOT 383 | ############################# 384 | 385 | portainer: 386 | image: portainer/portainer-ce:latest 387 | restart: unless-stopped 388 | container_name: portainer 389 | labels: 390 | - traefik.enable=true 391 | - traefik.docker.network=portainer-net 392 | - traefik.http.services.portainer-svc.loadbalancer.server.port=9000 393 | - traefik.http.routers.portainer-rtr.rule=Host(`portainer.${DNS_DOMAIN}`) 394 | - traefik.http.routers.portainer-rtr.entrypoints=websecure 395 | - traefik.http.routers.portainer-rtr.tls=true 396 | - traefik.http.routers.portainer-rtr.middlewares=ipwhitelist-mddl@docker,traefik-forward-auth@docker 397 | volumes: 398 | - ${PRIMARY_MOUNT}/portainer/data/:/data 399 | - ${SOCK_PATH:-/var/run/docker.sock}:/var/run/docker.sock 400 | networks: 401 | - portainer-net 402 | 403 | ############################# 404 | ## RSSHUB 405 | ############################# 406 | 407 | rsshub: 408 | image: diygod/rsshub:chromium-bundled 409 | container_name: rsshub 410 | restart: always 411 | user: ${PUID:-1000} 412 | environment: 413 | NODE_ENV: production 414 | CACHE_TYPE: redis 415 | REDIS_URL: "redis://rsshub_redis:6379/" 416 | labels: 417 | - traefik.enable=true 418 | - traefik.docker.network=rsshub-net 419 | - traefik.http.services.rsshub-svc.loadbalancer.server.port=1200 420 | - traefik.http.routers.rsshub-rtr.rule=Host(`rsshub.${DNS_DOMAIN}`) 421 | - traefik.http.routers.rsshub-rtr.entrypoints=websecure 422 | - traefik.http.routers.rsshub-rtr.tls=true 423 | depends_on: 424 | - redis 425 | networks: 426 | - rsshub-net 427 | - rsshub-private-net 428 | 429 | redis: 430 | image: redis:alpine 431 | restart: always 432 | container_name: rsshub_redis 433 | volumes: 434 | - redis-data:/data 435 | healthcheck: 436 | test: ["CMD", "redis-cli", "ping"] 437 | interval: 30s 438 | timeout: 10s 439 | retries: 5 440 | start_period: 5s 441 | networks: 442 | - rsshub-private-net 443 | 444 | ############################# 445 | ## TRANSMISSION_TORRENT 446 | ############################# 447 | 448 | transmission: 449 | image: haugene/transmission-openvpn:4 450 | container_name: transmission 451 | restart: unless-stopped 452 | labels: 453 | - "com.centurylinklabs.watchtower.enable=false" 454 | - traefik.enable=true 455 | - traefik.docker.network=transmission-net 456 | - traefik.http.services.transmission-svc.loadbalancer.server.port=9091 457 | - traefik.http.routers.transmission-rtr.rule=Host(`torrent.${DNS_DOMAIN}`) 458 | - traefik.http.routers.transmission-rtr.entrypoints=websecure 459 | - traefik.http.routers.transmission-rtr.tls=true 460 | - traefik.http.routers.transmission-rtr.middlewares=traefik-forward-auth@docker,ipwhitelist-mddl@docker 461 | networks: 462 | - arr-net 463 | - transmission-net 464 | dns: 1.1.1.1 465 | sysctls: 466 | - net.ipv6.conf.all.disable_ipv6=1 467 | cap_add: 468 | - NET_ADMIN 469 | volumes: 470 | - ${MEDIA_MOUNT}/plex/media/:/data 471 | - /etc/localtime:/etc/localtime:ro 472 | environment: 473 | - PUID=${PUID:-1000} 474 | - PGID=${PUID:-1000} 475 | - "OPENVPN_CONFIG=${OPENVPN_CONFIG}" 476 | - "OPENVPN_PROVIDER=${OPENVPN_PROVIDER}" 477 | - "OPENVPN_USERNAME=${TORRENT_USERNAME}" 478 | - "OPENVPN_PASSWORD=${TORRENT_PASSWORD}" 479 | - "OPENVPN_OPTS=--inactive 3600 --ping 10 --ping-exit 60" 480 | - "LOCAL_NETWORK=${LOCAL_NETWORK}" 481 | - "PIA_OPENVPN_CONFIG_BUNDLE=openvpn-tcp" 482 | 483 | ############################# 484 | ## TRAEFIK - ROOT 485 | ############################# 486 | 487 | traefik: 488 | image: traefik:v2.4 489 | restart: unless-stopped 490 | container_name: traefik 491 | command: > 492 | --api.insecure=true 493 | --serversTransport.insecureSkipVerify=true 494 | --accesslog=true 495 | --accesslog.filepath=/var/log/traefik/access.log 496 | --accesslog.fields.headers.names.Content-Type=keep 497 | --accesslog.fields.headers.names.Referer=keep 498 | --accesslog.fields.headers.names.User-Agent=keep 499 | --providers.docker=true 500 | --providers.docker.exposedByDefault=false 501 | --entrypoints.web.address=:80 502 | --entrypoints.websecure.address=:443 503 | --entrypoints.websecure.forwardedHeaders.trustedIPs=173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/12,172.64.0.0/13,131.0.72.0/22 504 | --entrypoints.websecure.http.tls.certresolver=myresolver 505 | --entrypoints.websecure.http.tls.domains[0].main=*.${DNS_DOMAIN} 506 | --entrypoints.websecure.http.tls.domains[0].sans=${DNS_DOMAIN} 507 | --certificatesResolvers.myresolver.acme.caServer="https://acme-v02.api.letsencrypt.org/directory" 508 | --certificatesresolvers.myresolver.acme.dnschallenge=true 509 | --certificatesresolvers.myresolver.acme.dnschallenge.provider=cloudflare 510 | --certificatesresolvers.myresolver.acme.email="${LETSENCRYPT_EMAIL}" 511 | --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json 512 | --certificatesresolvers.myresolver.acme.dnschallenge.resolvers==1.1.1.1:53,1.0.0.1:53 513 | labels: 514 | - "traefik.enable=true" 515 | # Traefik HTTPS Redirect 516 | - "traefik.http.routers.http-catchall.entrypoints=web" 517 | - "traefik.http.routers.http-catchall.rule=HostRegexp(`{host:.+}`)" 518 | - "traefik.http.routers.http-catchall.middlewares=redirect-to-https-mddl@docker" 519 | - "traefik.http.middlewares.redirect-to-https-mddl.redirectscheme.scheme=https" 520 | # Other middlewares 521 | - "traefik.http.middlewares.ipwhitelist-mddl.ipwhitelist.sourcerange=127.0.0.1/32,${SUBNET}" 522 | - "traefik.http.middlewares.traefik-forward-auth.forwardauth.address=http://auth:4181" 523 | - "traefik.http.middlewares.traefik-forward-auth.forwardauth.authResponseHeaders=X-Forwarded-User" 524 | # For Vaultwarden 525 | - "traefik.http.middlewares.bw-stripPrefix.stripprefix.prefixes=/notifications/hub" 526 | - "traefik.http.middlewares.bw-stripPrefix.stripprefix.forceSlash=false" 527 | # Traefik Dashboard config 528 | - traefik.http.services.traefik-svc.loadbalancer.server.port=8080 529 | - traefik.http.routers.traefik-rtr.rule=Host(`traefik.${DNS_DOMAIN}`) 530 | - traefik.http.routers.traefik-rtr.entrypoints=websecure 531 | - traefik.http.routers.traefik-rtr.tls=true 532 | - traefik.http.routers.traefik-rtr.middlewares=traefik-forward-auth@docker 533 | environment: 534 | - CF_API_EMAIL=${CF_EMAIL} 535 | - CF_DNS_API_TOKEN=${CF_TOKEN} 536 | - CF_ZONE_API_TOKEN=${CF_TOKEN} 537 | volumes: 538 | - ${SOCK_PATH:-/var/run/docker.sock}:/var/run/docker.sock 539 | - "${LOCAL_MOUNT}/traefik/letsencrypt:/letsencrypt" 540 | - "${LOCAL_MOUNT}/traefik/logs:/var/log/traefik" 541 | - "./etc/traefik:/etc/traefik" 542 | ports: 543 | - "80:80" 544 | - "443:443" 545 | networks: 546 | - traefik-net 547 | - adguard-net 548 | - arr-net 549 | - auth-net 550 | - calibre-net 551 | - changedetection-net 552 | - drone-net 553 | - duplicati-net 554 | - miniflux-net 555 | - kobodl-net 556 | - plex-net 557 | - portainer-net 558 | - rsshub-net 559 | - transmission-net 560 | - umami-net 561 | - unifi-net 562 | - vaultwarden-net 563 | - webdav-net 564 | 565 | traefik-forward-auth: 566 | image: thomseddon/traefik-forward-auth:latest 567 | container_name: auth 568 | restart: unless-stopped 569 | user: ${PUID:-1000} 570 | networks: 571 | - auth-net 572 | environment: 573 | - "PROVIDERS_GOOGLE_CLIENT_ID=${AUTH_PROVIDERS_GOOGLE_CLIENT_ID}" 574 | - "PROVIDERS_GOOGLE_CLIENT_SECRET=${AUTH_PROVIDERS_GOOGLE_CLIENT_SECRET}" 575 | - "SECRET=${AUTH_SECRET}" 576 | - "WHITELIST=${AUTH_WHITELIST}" 577 | command: > 578 | --cookie-domain="${DNS_DOMAIN}" 579 | --auth-host="auth.${DNS_DOMAIN}" 580 | --config=/etc/authconfig.ini 581 | volumes: 582 | - "./etc/authconfig.ini:/etc/authconfig.ini:ro" 583 | labels: 584 | - traefik.enable=true 585 | - traefik.docker.network=auth-net 586 | - traefik.http.services.traefik-forward-auth-svc.loadbalancer.server.port=4181 587 | - traefik.http.routers.auth-rtr.rule=Host(`auth.${DNS_DOMAIN}`) 588 | - traefik.http.routers.auth-rtr.entrypoints=websecure 589 | - traefik.http.routers.auth-rtr.tls=true 590 | - traefik.http.routers.auth-rtr.middlewares=traefik-forward-auth 591 | 592 | ############################# 593 | ## Umami 594 | ############################# 595 | 596 | umami_postgres: 597 | image: postgres:15-alpine 598 | restart: always 599 | container_name: umami_postgres 600 | volumes: 601 | - ${PRIMARY_MOUNT}/postgres_15:/var/lib/postgresql/data 602 | environment: 603 | POSTGRES_DB: umami 604 | POSTGRES_USER: umami 605 | POSTGRES_PASSWORD: umami 606 | networks: 607 | - umami-private-net 608 | healthcheck: 609 | test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] 610 | interval: 5s 611 | timeout: 5s 612 | retries: 5 613 | 614 | umami: 615 | image: ghcr.io/umami-software/umami:postgresql-latest 616 | restart: always 617 | container_name: umami 618 | user: ${PUID:-1000} 619 | depends_on: 620 | umami_postgres: 621 | condition: service_healthy 622 | environment: 623 | DATABASE_URL: postgresql://umami:umami@umami_postgres:5432/umami 624 | DATABASE_TYPE: postgresql 625 | APP_SECRET: ${DNS_DOMAIN_ZONE_ID} 626 | labels: 627 | - traefik.enable=true 628 | - traefik.docker.network=umami-net 629 | - traefik.http.services.umami-svc.loadbalancer.server.port=3000 630 | - traefik.http.routers.umami-rtr.rule=Host(`umami.${DNS_DOMAIN}`) 631 | - traefik.http.routers.umami-rtr.entrypoints=websecure 632 | - traefik.http.routers.umami-rtr.tls=true 633 | networks: 634 | - umami-net 635 | - umami-private-net 636 | healthcheck: 637 | test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"] 638 | interval: 5s 639 | timeout: 5s 640 | retries: 5 641 | 642 | ############################# 643 | ## UNIFI CONTROLLER 644 | ############################# 645 | 646 | unifi_controller: 647 | image: ghcr.io/linuxserver/unifi-controller:latest 648 | container_name: unifi_controller 649 | restart: unless-stopped 650 | environment: 651 | - PUID=${PUID:-1000} 652 | - PGID=${PUID:-1000} 653 | - MEM_LIMIT=1024 654 | ports: 655 | - "3478:3478/udp" # STUN 656 | - "10001:10001/udp" # Discovery 657 | - "8080:8080" # Device comms 658 | - "6789:6789" # Mobile speedtest 659 | labels: 660 | - traefik.enable=true 661 | - traefik.docker.network=unifi-net 662 | - traefik.http.routers.ubiq-rtr.rule=Host(`unifi.${DNS_DOMAIN}`) 663 | - traefik.http.routers.ubiq-rtr.entrypoints=websecure 664 | - traefik.http.routers.ubiq-rtr.tls=true 665 | - traefik.http.routers.ubiq-rtr.middlewares=ipwhitelist-mddl@docker 666 | - traefik.http.services.ubiq-svc.loadbalancer.server.scheme=https 667 | - traefik.http.services.ubiq-svc.loadbalancer.server.port=8443 668 | volumes: 669 | - "${PRIMARY_MOUNT}/unifi/config/:/config" 670 | networks: 671 | - unifi-net 672 | 673 | ############################# 674 | ## VAULTWARDEN 675 | ############################# 676 | 677 | vaultwarden: 678 | image: vaultwarden/server:latest 679 | container_name: vaultwarden 680 | restart: always 681 | user: ${PUID:-1000} 682 | volumes: 683 | - "${PRIMARY_MOUNT}/vaultwarden/data/:/data" 684 | environment: 685 | - WEBSOCKET_ENABLED=true 686 | networks: 687 | - vaultwarden-net 688 | labels: 689 | - "traefik.enable=true" 690 | - "traefik.docker.network=vaultwarden-net" 691 | # Entry Point for https 692 | - "traefik.http.routers.vaultwarden-rtr.entrypoints=websecure" 693 | - "traefik.http.routers.vaultwarden-rtr.rule=Host(`vaultwarden.${DNS_DOMAIN}`)" 694 | - "traefik.http.routers.vaultwarden-rtr.service=vaultwarden-svc" 695 | - "traefik.http.services.vaultwarden-svc.loadbalancer.server.port=80" 696 | # websocket 697 | - "traefik.http.routers.vaultwarden-ws-rtr.entrypoints=websecure" 698 | - "traefik.http.routers.vaultwarden-ws-rtr.rule=Host(`vaultwarden.${DNS_DOMAIN}`) && Path(`/notifications/hub`)" 699 | - "traefik.http.middlewares.vaultwarden-ws-rtr=bw-stripPrefix@file" 700 | - "traefik.http.routers.vaultwarden-ws-rtr.service=vaultwarden-ws-svc" 701 | - "traefik.http.services.vaultwarden-ws-svc.loadbalancer.server.port=3012" 702 | 703 | ############################# 704 | ## WEBDAV 705 | ############################# 706 | 707 | webdav: 708 | image: bytemark/webdav 709 | container_name: webdav 710 | restart: always 711 | environment: 712 | AUTH_TYPE: Digest 713 | USERNAME: admin 714 | PASSWORD: password 715 | volumes: 716 | - "${PRIMARY_MOUNT}/webdav:/var/lib/dav" 717 | networks: 718 | - webdav-net 719 | labels: 720 | - "traefik.enable=true" 721 | - "traefik.docker.network=webdav-net" 722 | - "traefik.http.routers.webdav-rtr.entrypoints=websecure" 723 | - "traefik.http.routers.webdav-rtr.rule=Host(`webdav.${DNS_DOMAIN}`)" 724 | - "traefik.http.routers.webdav-rtr.service=webdav-svc" 725 | - "traefik.http.services.webdav-svc.loadbalancer.server.port=80" 726 | - "traefik.http.routers.webdav-rtr.middlewares=ipwhitelist-mddl@docker" 727 | 728 | ############################# 729 | ## WATCHTOWER - ROOT 730 | ############################# 731 | 732 | watchtower: 733 | image: containrrr/watchtower:latest 734 | container_name: watchtower 735 | restart: unless-stopped 736 | command: --schedule "0 10 3 * * *" --cleanup 737 | labels: 738 | - traefik.enable=false 739 | volumes: 740 | - ${SOCK_PATH:-/var/run/docker.sock}:/var/run/docker.sock 741 | - "${PRIMARY_MOUNT}/watchtower/config/:/config" 742 | - "${PRIMARY_MOUNT}/watchtower/docker-config.json:/config.json" 743 | 744 | networks: 745 | adguard-net: 746 | name: adguard-net 747 | arr-net: 748 | name: arr-net 749 | auth-net: 750 | name: auth-net 751 | calibre-net: 752 | name: calibre-net 753 | changedetection-net: 754 | name: changedetection-net 755 | changedetection-private-net: 756 | name: changedetection-private-net 757 | drone-net: 758 | name: drone-net 759 | drone-private-net: 760 | name: drone-private-net 761 | duplicati-net: 762 | name: duplicati-net 763 | miniflux-net: 764 | name: miniflux-net 765 | kobodl-net: 766 | name: kobodl-net 767 | plex-net: 768 | name: plex-net 769 | portainer-net: 770 | name: portainer-net 771 | rsshub-net: 772 | name: rsshub-net 773 | rsshub-private-net: 774 | name: rsshub-private-net 775 | transmission-net: 776 | name: transmission-net 777 | traefik-net: 778 | name: traefik-net 779 | umami-net: 780 | name: umami-net 781 | umami-private-net: 782 | name: umami-private-net 783 | unifi-net: 784 | name: unifi-net 785 | vaultwarden-net: 786 | name: vaultwarden-net 787 | webdav-net: 788 | name: webdav-net 789 | 790 | volumes: 791 | redis-data: 792 | -------------------------------------------------------------------------------- /docs/config.boot: -------------------------------------------------------------------------------- 1 | firewall { 2 | all-ping enable 3 | broadcast-ping disable 4 | group { 5 | address-group DNS-Servers { 6 | address 192.168.52.175 7 | address 192.168.52.112 8 | } 9 | } 10 | ipv6-name WANv6_IN { 11 | default-action drop 12 | description "WAN inbound traffic forwarded to LAN" 13 | enable-default-log 14 | rule 10 { 15 | action accept 16 | description "Allow established/related sessions" 17 | state { 18 | established enable 19 | related enable 20 | } 21 | } 22 | rule 20 { 23 | action drop 24 | description "Drop invalid state" 25 | state { 26 | invalid enable 27 | } 28 | } 29 | rule 201 { 30 | action accept 31 | description "icmpv6 destination-unreachable" 32 | log enable 33 | protocol ipv6-icmp 34 | } 35 | rule 300 { 36 | action accept 37 | description Traefik 38 | destination { 39 | address ::8e89:a5ff:fe3b:3b41/::ffff:ffff:ffff:ffff # This is a neat trick 40 | port 80,443 41 | } 42 | protocol tcp 43 | } 44 | rule 301 { 45 | action accept 46 | description Wireguard 47 | destination { 48 | address ::8e89:a5ff:fe3b:3b41/::ffff:ffff:ffff:ffff 49 | port 51820 50 | } 51 | protocol tcp_udp 52 | } 53 | } 54 | ipv6-name WANv6_LOCAL { 55 | default-action drop 56 | description "WAN inbound traffic to the router" 57 | enable-default-log 58 | rule 10 { 59 | action accept 60 | description "Allow established/related sessions" 61 | state { 62 | established enable 63 | related enable 64 | } 65 | } 66 | rule 20 { 67 | action drop 68 | description "Drop invalid state" 69 | state { 70 | invalid enable 71 | } 72 | } 73 | rule 30 { 74 | action accept 75 | description "Allow IPv6 icmp" 76 | protocol ipv6-icmp 77 | } 78 | rule 40 { 79 | action accept 80 | description "allow dhcpv6" 81 | destination { 82 | port 546 83 | } 84 | protocol udp 85 | source { 86 | port 547 87 | } 88 | } 89 | } 90 | ipv6-receive-redirects disable 91 | ipv6-src-route disable 92 | ip-src-route disable 93 | log-martians enable 94 | name WAN_IN { 95 | default-action drop 96 | description "WAN to internal" 97 | rule 10 { 98 | action accept 99 | description "Allow established/related" 100 | state { 101 | established enable 102 | related enable 103 | } 104 | } 105 | rule 20 { 106 | action drop 107 | description "Drop invalid state" 108 | state { 109 | invalid enable 110 | } 111 | } 112 | } 113 | name WAN_LOCAL { 114 | default-action drop 115 | description "WAN to router" 116 | rule 10 { 117 | action accept 118 | description "Allow established/related" 119 | state { 120 | established enable 121 | related enable 122 | } 123 | } 124 | rule 20 { 125 | action drop 126 | description "Drop invalid state" 127 | state { 128 | invalid enable 129 | } 130 | } 131 | } 132 | receive-redirects disable 133 | send-redirects enable 134 | source-validation disable 135 | syn-cookies enable 136 | } 137 | interfaces { 138 | ethernet eth0 { 139 | address dhcp 140 | description Internet 141 | dhcp-options { 142 | default-route update 143 | default-route-distance 210 144 | name-server no-update 145 | } 146 | dhcpv6-pd { 147 | no-dns 148 | pd 0 { 149 | interface switch0 { 150 | no-dns 151 | service slaac 152 | } 153 | prefix-length /64 154 | } 155 | rapid-commit enable 156 | } 157 | duplex auto 158 | firewall { 159 | in { 160 | ipv6-name WANv6_IN 161 | name WAN_IN 162 | } 163 | local { 164 | ipv6-name WANv6_LOCAL 165 | name WAN_LOCAL 166 | } 167 | out { 168 | } 169 | } 170 | ipv6 { 171 | address { 172 | autoconf 173 | } 174 | dup-addr-detect-transmits 1 175 | } 176 | pppoe 0 { 177 | default-route auto 178 | ipv6 { 179 | address { 180 | autoconf 181 | } 182 | dup-addr-detect-transmits 1 183 | enable { 184 | } 185 | } 186 | mtu 1492 187 | name-server auto 188 | } 189 | speed auto 190 | } 191 | ethernet eth1 { 192 | description Local 193 | duplex auto 194 | speed auto 195 | } 196 | ethernet eth2 { 197 | description Local 198 | duplex auto 199 | speed auto 200 | } 201 | ethernet eth3 { 202 | description Local 203 | duplex auto 204 | speed auto 205 | } 206 | ethernet eth4 { 207 | description Local 208 | duplex auto 209 | poe { 210 | output off 211 | } 212 | speed auto 213 | } 214 | loopback lo { 215 | } 216 | switch switch0 { 217 | address 192.168.52.1/20 218 | description Local 219 | mtu 1500 220 | switch-port { 221 | interface eth1 { 222 | } 223 | interface eth2 { 224 | } 225 | interface eth3 { 226 | } 227 | vlan-aware disable 228 | } 229 | } 230 | } 231 | port-forward { 232 | auto-firewall enable 233 | hairpin-nat enable 234 | lan-interface switch0 235 | rule 1 { 236 | description http 237 | forward-to { 238 | address 192.168.52.175 239 | port 80 240 | } 241 | original-port http 242 | protocol tcp 243 | } 244 | rule 2 { 245 | description https 246 | forward-to { 247 | address 192.168.52.175 248 | port 443 249 | } 250 | original-port https 251 | protocol tcp 252 | } 253 | rule 3 { 254 | description wireguard 255 | forward-to { 256 | address 192.168.52.175 257 | port 51820 258 | } 259 | original-port 51820 260 | protocol tcp_udp 261 | } 262 | wan-interface eth0 263 | } 264 | service { 265 | dhcp-server { 266 | disabled false 267 | hostfile-update disable 268 | shared-network-name lan { 269 | authoritative disable 270 | subnet 192.168.52.0/24 { 271 | default-router 192.168.52.1 272 | lease 86400 273 | start 192.168.52.38 { 274 | stop 192.168.52.243 275 | } 276 | static-mapping Draynor { 277 | ip-address 192.168.52.112 278 | mac-address b8:27:eb:b3:9f:ce 279 | } 280 | static-mapping MainAP { 281 | ip-address 192.168.52.185 282 | mac-address e0:63:da:3c:68:be 283 | } 284 | static-mapping PhilipsHue { 285 | ip-address 192.168.52.230 286 | mac-address 00:17:88:6d:95:69 287 | } 288 | static-mapping lumbridge { 289 | ip-address 192.168.52.175 290 | mac-address 8c:89:a5:3b:3b:41 291 | } 292 | static-mapping varrock { 293 | ip-address 192.168.52.40 294 | mac-address 54:bf:64:6c:91:e9 295 | } 296 | static-mapping wildy { 297 | ip-address 192.168.52.45 298 | mac-address 00:23:24:64:4b:98 299 | } 300 | unifi-controller 192.168.52.175 301 | } 302 | } 303 | static-arp disable 304 | use-dnsmasq enable 305 | } 306 | dns { 307 | dynamic { 308 | interface eth0 { 309 | service dyndns { 310 | host-name subdavis 311 | login nouser 312 | password ******** 313 | protocol dyndns2 314 | server www.duckdns.org 315 | } 316 | web dyndns 317 | } 318 | } 319 | forwarding { 320 | cache-size 150 321 | listen-on switch0 322 | name-server 192.168.52.175 323 | options dhcp-option=6,192.168.52.1 324 | options bind-interfaces 325 | options listen-address=127.0.0.1 326 | options listen-address=192.168.52.1 327 | } 328 | } 329 | gui { 330 | http-port 80 331 | https-port 443 332 | older-ciphers enable 333 | } 334 | nat { 335 | rule 1002 { 336 | description "Redirect DNS" 337 | destination { 338 | port 53 339 | } 340 | inbound-interface switch0 341 | inside-address { 342 | address 192.168.52.175 343 | port 53 344 | } 345 | log disable 346 | protocol tcp_udp 347 | source { 348 | group { 349 | address-group !DNS-Servers 350 | } 351 | } 352 | type destination 353 | } 354 | rule 5002 { 355 | description "Translate DNS to Internal reply" 356 | destination { 357 | group { 358 | address-group DNS-Servers 359 | } 360 | port 53 361 | } 362 | log disable 363 | outbound-interface switch0 364 | protocol tcp_udp 365 | type masquerade 366 | } 367 | rule 5010 { 368 | description "masquerade for WAN" 369 | outbound-interface eth0 370 | type masquerade 371 | } 372 | } 373 | ssh { 374 | port 22 375 | protocol-version v2 376 | } 377 | unms { 378 | disable 379 | } 380 | } 381 | system { 382 | config-management { 383 | commit-archive { 384 | } 385 | } 386 | domain-name lan 387 | host-name ubnt 388 | login { 389 | user ubnt { 390 | authentication { 391 | encrypted-password ******** 392 | plaintext-password "" 393 | } 394 | full-name "" 395 | level admin 396 | } 397 | } 398 | name-server 127.0.0.1 399 | ntp { 400 | server 0.ubnt.pool.ntp.org { 401 | } 402 | server 1.ubnt.pool.ntp.org { 403 | } 404 | server 2.ubnt.pool.ntp.org { 405 | } 406 | server 3.ubnt.pool.ntp.org { 407 | } 408 | } 409 | offload { 410 | hwnat enable 411 | } 412 | package { 413 | repository stretch { 414 | components "main contrib non-free" 415 | distribution stretch 416 | password "" 417 | url http://http.us.debian.org/debian 418 | username "" 419 | } 420 | } 421 | syslog { 422 | global { 423 | facility all { 424 | level notice 425 | } 426 | facility protocols { 427 | level debug 428 | } 429 | } 430 | } 431 | task-scheduler { 432 | } 433 | time-zone America/New_York 434 | } 435 | -------------------------------------------------------------------------------- /docs/edgerouter-backups.md: -------------------------------------------------------------------------------- 1 | # Backing up EdgeRouter X 2 | 3 | On commit, save the router config to an external server 4 | 5 | * scp will push the file to the remote over ssh. 6 | * SSH keys don't work, so must use password. 7 | * Create a user `edgerouter-backup` on the target host. 8 | * [Only allow password login for a specific user](https://serverfault.com/questions/307407/ssh-allow-password-for-one-user-rest-only-allow-public-keys) 9 | * [Set Up automatic backups](https://help.ui.com/hc/en-us/articles/204960084-EdgeMAX-Manage-the-configuration-file) 10 | 11 | ``` bash 12 | configure 13 | set system config-management commit-archive location "scp://edgerouter-backup:password@host.lan/path/to/parent" 14 | commit ; save 15 | ``` 16 | 17 | Now you can back up the local files with duplicati. 18 | -------------------------------------------------------------------------------- /docs/images/cfapi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subdavis/selfhosted/36d9968a684d1c437207bef9a7c71d1a4786f719/docs/images/cfapi.png -------------------------------------------------------------------------------- /docs/images/pihole-advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subdavis/selfhosted/36d9968a684d1c437207bef9a7c71d1a4786f719/docs/images/pihole-advanced.png -------------------------------------------------------------------------------- /docs/images/pihole-dns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subdavis/selfhosted/36d9968a684d1c437207bef9a7c71d1a4786f719/docs/images/pihole-dns.png -------------------------------------------------------------------------------- /docs/images/router-dhcp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subdavis/selfhosted/36d9968a684d1c437207bef9a7c71d1a4786f719/docs/images/router-dhcp.png -------------------------------------------------------------------------------- /docs/images/router-dnsmasq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subdavis/selfhosted/36d9968a684d1c437207bef9a7c71d1a4786f719/docs/images/router-dnsmasq.png -------------------------------------------------------------------------------- /docs/images/router-system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subdavis/selfhosted/36d9968a684d1c437207bef9a7c71d1a4786f719/docs/images/router-system.png -------------------------------------------------------------------------------- /docs/lan-only-routes.md: -------------------------------------------------------------------------------- 1 | # LAN-only routes with Traefik 2 | 3 | > **GOAL:** In this guide, you'll learn how to set up portainer with valid SSL and Host-based routing privately on your LAN. 4 | > 5 | > Turn _this_: `http://myserver.lan:8080` 6 | > 7 | > Into this: `https://portainer.mydomain.com` 8 | > 9 | > ...all while keepoing your private services off the internet. 10 | 11 | When running services in traefik, you'll likely want to expose some to the internet (like plex) and keep others accessible only from your local network (like portainer). This document is mostly about IP whitelisting, but I want to first talk about SSL and security. 12 | 13 | ## SSL for private routes 14 | 15 | There are 2 main ways of creating [routing rules](https://docs.traefik.io/routing/routers/#rule) for apps: Host rules and PathPrefix rules. 16 | 17 | * Host rules route based on the hostname of the destination, like `foo.mydomain.com` or `http://192.168.0.10`. 18 | * PathPrefix rules route based on some prefix substring, like `/plex` or `/portainer`. 19 | 20 | You may think that, without rolling out some robust DNS on your home network, you're stuck with `http://myserver.lan/portainer` as your best option for routing. 21 | 22 | This has drawbacks: 23 | 24 | 1. PathPrefix rules are an **enormous pain in the ass** because [nobody understands how they should work](https://github.com/elastic/kibana/issues/6665). 25 | 1. Without a legitimate domain name, you're stuck with self-signed certificates. 26 | 27 | Instead, I like to use my real domain even for local routes. 28 | 29 | 1. Create a CNAME or A record for `portainer.mydomain.com` to point to either your server's private IP or even your network's public IP. **NOTE:** if you're using unbound DNS or a DNS resolver that blocks resolution for private addresses, you'll have to use your public IP and set up port forwarding even if you block all public addresses. Might be possible to do some NAT magic to avoid this. 30 | 1. Set up [Wildcard SSL for your domain](wildcard-certs.md) 31 | 32 | Now, you'd be ready to set up a publicly accessible service, except we're going to restrict access. 33 | 34 | ## IP Whitelisting 35 | 36 | ### The Traefik Part 37 | 38 | With the IPWhitelist middleware, we're going to restrict access to your LAN subnet. You can run traefik exactly the same as in [the wildcard SSL tutorial](wildcard-certs.md). 39 | 40 | * If you choose to use your **Public** IP, you still need to set up port forwarding on your router for this to work correctly. This is because requests to `portainer.mydomain.com` will resolve to your public IP, get routed to your public interface, then directed back into your LAN, so the actual external filtering will happen on your traefik host. 41 | * If you choose to use your **Private** IP in your DNS records, you don't need port forwarding becasue the request will never hit your router. Beware that certain configurations of Unbound DNS will block DNS resolution to private IPs (if you don't know what this is, you're probably not affected). 42 | 43 | ### The Portainer Part 44 | 45 | This part will also be almost the same as the wildcard tutorial, with the addition of 1 middleware. Refer to [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) if you don't know how to represent your subnet as a CIDR block. 46 | 47 | ``` bash 48 | export MY_APP_DOMAIN=mydomain.com 49 | export SUBNET="192.168.0.0/24" 50 | docker run --rm --name portainer \ 51 | --label traefik.enable=true \ 52 | --label traefik.http.services.my-service.loadbalancer.server.port="9000" \ 53 | --label traefik.http.middlewares.middleware-redirect-https.redirectscheme.scheme="https" \ 54 | --label traefik.http.routers.my-route.entrypoints=web \ 55 | --label traefik.http.routers.my-route.rule="Host(`portainer.${MY_APP_DOMAIN}`)" \ 56 | --label traefik.http.routers.my-route.middlewares="middleware-redirect-https@docker" \ 57 | --label traefik.http.routers.my-route-secure.entrypoints=websecure \ 58 | --label traefik.http.routers.my-route-secure.rule="Host(`portainer.${MY_APP_DOMAIN}`)" \ 59 | --label traefik.http.routers.my-route-secure.tls.domains[0].main="*.${MY_APP_DOMAIN}" \ 60 | --label traefik.http.routers.my-route-secure.tls.certresolver="myresolver" \ 61 | --label traefik.http.routers.my-route-secure.tls=true \ 62 | --label traefik.http.middlewares.middleware-ipwhitelist.ipwhitelist.sourcerange="127.0.0.1/32,${SUBNET}" \ 63 | --label traefik.http.routers.%N-secure.middlewares="middleware-ipwhitelist@docker" \ 64 | --volume /tmp/portainer/data/:/data \ 65 | --volume /var/run/docker.sock:/var/run/docker.sock \ 66 | portainer/portainer 67 | ``` 68 | 69 | ## Testing it 70 | 71 | Visit https://portainer.mydomain.com 72 | 73 | It doesn't matter whether your DNS record for `portainer.mydomain.com` points at your network's Public IP or the traefik server's private IP. When the request hits your Firewall or router, you'll get redirected internally and traefik will examine the origin of the request, which will be your host's private IP. 74 | 75 | To verify this, you can: 76 | 77 | * Try the request from a host on the subnet. It will succeed. 78 | * Try the request from a host with a properly configured active VPN running. It will STILL succeed. 79 | * Try the request from a mobile phone with wifi off. u'll get a 401 Unauthorized response! 80 | * Try whatever else you can think of to trick traefik. It won't work :) 81 | 82 | > **NOTE**: If you run traefik behind another proxy that uses X-Forwarded-For header, you may have to configure other settings to [pick the right IP](https://docs.traefik.io/middlewares/ipwhitelist/#configuration-options) 83 | -------------------------------------------------------------------------------- /docs/pihole-dnsmasq.md: -------------------------------------------------------------------------------- 1 | # Configuring Pi-Hole properly with dnsmasq 2 | 3 | This documentation is expanded from [the pihole discourse docs](https://discourse.pi-hole.net/t/how-do-i-configure-my-devices-to-use-pi-hole-as-their-dns-server/245). That document is great, but if you aren't already familiar with [dnsmasq](http://www.thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html) it may be confusing. 4 | 5 | IMO, this is the **most proper** method of configuring a pihole. 6 | 7 | ## Goals 8 | 9 | * to automatically configure DHCP clients to use a local pihole for DNS 10 | * to use our router as a dnsmasq-based dhcp server (instead of using pihole for dhcp) 11 | * to preserve hostname routing for hosts on your LAN, so you don't ever have to touch `/etc/hosts` 12 | * to allow pihole logs to show queries based on hostname rather than IP. 13 | 14 | ## dnsmasq 15 | 16 | dnsmasq is both a DNS *and* a DHCP server. It does both, and we're going to use it for both. What matters is the ordering. 17 | 18 | DHCP servers handle assigning IP addresses to hosts on network. Since dnsmasq is both a DHCP server and a DNS resolver, it can remember what host it assigned what IP, so when you query for `myhost.lan`, dnsmasq will notice that it has a local record for `myhost` and return its local IP. 19 | 20 | Here's how we want the query order to work: 21 | 22 | ``` plain 23 | +------------------------------+ 24 | | | 25 | | Internet (1.1.1.1) | 26 | | | 27 | +-------------+----------------+ 28 | ^ 29 | | 30 | +-------------+----------------+ 31 | | | 32 | | Router (192.168.1.1) | 33 | | | 34 | +-------------+----------------+ 35 | ^ 36 | | 37 | | 38 | +-------------+----------------+ 39 | | | 40 | | Pihole (192.168.1.33) | 41 | | | 42 | +------------------------------+ 43 | ``` 44 | 45 | ### Setting dnsmasq options 46 | 47 | We need to configure the router to tell DHCP clients that the local DNS server is pihole, at `192.168.1.33` (for example). This happens when a client leases an IP, so after you change these settings, you may need to use `dhclient` to refresh your lease. 48 | 49 | I have a Ubiquiti Edgerouter X, so [enabling dnsmasq](https://help.ui.com/hc/en-us/articles/115002673188-EdgeRouter-DHCP-Server-Using-Dnsmasq) is easy enough. 50 | 51 | * Change dnsmasq's DNS forwarding to the public server you choose. I like `1.1.1.2` from [cloudflare](https://blog.cloudflare.com/introducing-1-1-1-1-for-families/) 52 | * Set dnsmasq `dhcp-option` option 6 `dns-server` to the IP of your Pi Hole. `dhcp-option=6,192.168.1.33` is the likely syntax 53 | * Set the system nameserver to be localhost, so all local DNS queries also go through dnsmasq. 54 | * Set a local domain name, like `lan` so that all your hostnames will be accessible as `hostname.lan`. 55 | 56 | ![Edgerouter Dnsmasq GUI](images/router-dnsmasq.png) 57 | 58 | ![Edgerouter System GUI](images/router-system.png) 59 | 60 | My edgerouter config looks like this 61 | 62 | ``` conf 63 | service { 64 | dhcp-server { 65 | disabled false 66 | shared-network-name LAN { 67 | authoritative enable 68 | subnet 192.168.1.0/24 { 69 | default-router 192.168.1.1 70 | domain-name lan 71 | lease 86400 72 | start 192.168.1.38 { 73 | stop 192.168.1.243 74 | } 75 | } 76 | } 77 | static-arp disable 78 | use-dnsmasq enable 79 | } 80 | dns { 81 | forwarding { 82 | cache-size 150 83 | listen-on switch0 84 | name-server 1.1.1.2 85 | name-server 1.0.0.2 86 | options dhcp-option=6,192.168.1.33 87 | } 88 | } 89 | } 90 | system { 91 | name-server 127.0.0.1 92 | } 93 | ``` 94 | 95 | ## Pi Hole Config 96 | 97 | Pihole will direct all un-blocked DNS to your router, the upstream dns server. 98 | 99 | * Set your router IP as the only custom Upstream DNS Server 100 | * Use DNSSEC 101 | * Enable conditional forwarding and specify your local domain, `lan` 102 | 103 | ![Pihole gui](/docs/images/pihole-dns.png) 104 | 105 | ![Pihole advanced](/docs/images/pihole-advanced.png) 106 | 107 | ## Verifying it worked 108 | 109 | https://askubuntu.com/questions/152593/command-line-to-list-dns-servers-used-by-my-system 110 | 111 | ```bash 112 | # List interfaces 113 | nmcli dev 114 | # Show details 115 | nmcli dev show eth0 116 | # Look for "IP4.DNS", it should be your PiHole IP 117 | ``` 118 | 119 | In addition, try renewing a DHCP lease on a client to ensure the new lease is sending your PiHole's IP for the DNS server. If it's not working, check to make sure that the DHCP Server in `Services` has blank entries for both `DNS Server 1` and `DNS Server 2`. 120 | 121 | ![Edgerouter System GUI](images/router-dhcp.png) 122 | 123 | ## DNS over TLS with Unbound 124 | 125 | I followed [this guide from chameth.com](https://chameth.com/dns-over-tls-on-edgerouter-lite/). Unlike Chris, I didn't want to _replace_ dnsmasq with unbound because I still wanted DHCP and automatic hostname resolution features that somehow only dnsmasq seems to provide. 126 | 127 | Instead, I altered dnsmasq to unbind from all interfaces so that I could run dnsmasq only on `switch0` at 192.168.1.1 and 127.0.0.1. 128 | 129 | Now, I run unbound on 127.0.0.2 and forward queries that dnsmasq is unable to resolve on to unbound. Here's my amended config. 130 | 131 | ``` conf 132 | service{ 133 | dns { 134 | forwarding { 135 | cache-size 150 136 | listen-on switch0 137 | name-server 127.0.0.2 138 | options dhcp-option=6,192.168.1.175,192.168.1.112 139 | options bind-interfaces 140 | options listen-address=127.0.0.1 141 | options listen-address=192.168.1.1 142 | } 143 | } 144 | } 145 | ``` 146 | 147 | > About the only time when this is useful is when running another nameserver (or another instance of dnsmasq) on the same machine. Setting this option also enables multiple instances of dnsmasq which provide DHCP service to run in the same machine. 148 | 149 | My unbound config can be found in [/etc/unbound.conf](/etc/unbound.conf) 150 | 151 | ## Redirect hard-coded DNS device queries to PiHole 152 | 153 | The idea is to capture queries coming from stubborn IoT devices like smart speakers and smart TVs. I cobbled this together from [a community thread](https://community.ui.com/questions/Intercepting-and-Re-Directing-DNS-Queries/cd0a248d-ca54-4d16-84c6-a5ade3dc3272) 154 | 155 | ``` conf 156 | service { 157 | nat { 158 | rule 1002 { 159 | description "Redirect DNS" 160 | destination { 161 | port 53 162 | } 163 | inbound-interface switch0 164 | inside-address { 165 | address 192.168.1.175 166 | port 53 167 | } 168 | log disable 169 | protocol tcp_udp 170 | source { 171 | group { 172 | address-group !DNS-Servers 173 | } 174 | } 175 | type destination 176 | } 177 | rule 5002 { 178 | description "Translate DNS to Internal reply" 179 | destination { 180 | group { 181 | address-group DNS-Servers 182 | } 183 | port 53 184 | } 185 | log disable 186 | outbound-interface switch0 187 | protocol tcp_udp 188 | type masquerade 189 | } 190 | ... 191 | } 192 | } 193 | ``` 194 | 195 | Note the address group. You'll have to create a firewall address group: 196 | 197 | ``` conf 198 | firewall { 199 | group { 200 | address-group DNS-Servers { 201 | address 192.168.1.175 202 | address 192.168.1.112 203 | description "Internal DNS servers" 204 | } 205 | } 206 | } 207 | ``` 208 | 209 | ## Adguard Home 210 | 211 | You can specify your router's IP for LAN reverse resolution inside adguard. 212 | -------------------------------------------------------------------------------- /docs/ubuntu-expand-lvm.md: -------------------------------------------------------------------------------- 1 | # Expand LVM to fill disk on ubuntu 2 | 3 | Occasionally I end up with an LVM that doesn't fill the physical disk. [This blog contains the solution](http://ryandoyle.net/posts/expanding-a-lvm-partition-to-fill-remaining-drive-space/) 4 | 5 | In summary: 6 | 7 | ``` bash 8 | df -h 9 | sudo lvresize -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv 10 | sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/wildcard-certs.md: -------------------------------------------------------------------------------- 1 | # ACME (Lets Encrypt) Wildcard SSL in Traefik 2 | 3 | > **GOAL:** In this document, you'll learn how to set up Wildcard SSL with ACME in Traefik and create a service with HTTP to HTTPS forced redirect. In all my docs, the end result is a *working example* rather than bullet points and hand-waving. 4 | 5 | Like much of the Traefik v2 documentation, the [Official ACME Docs](https://docs.traefik.io/https/acme/) aren't very good. Hopefully these snippets help. 6 | 7 | ## Getting Started 8 | 9 | Wildcard SSL is great because it lets you manage fewer certificates and spin up new services and new subdomains without dealing with ACME every time. If you're like me and find ACME (or your own HTTP infrastructure) a little unreliable, then having to interact with it less is a huge positive. 10 | 11 | First, Wildcard SSL can only be done using [DNS Challenge Authentication](https://docs.traefik.io/https/acme/#dnschallenge). To get an SSL certificate for any subdomain, you need to prove ownership of the entire DNS configuration as opposed to any single webserver. 12 | 13 | ### DNS Config 14 | 15 | Because you're doing DNS Auth, you actually don't *need* a dns record to get the certificate, but you do need one to *test* it. 16 | 17 | 1. Create a DNS A record to point to your webserver for `whoami.mydoain.com` 18 | 1. Verify that your router or firewall is configured to allow 80 AND 443 through the firewall to your webserver. 19 | 20 | ### Cloudflare API token 21 | 22 | > **NOTE** This example uses cloudflare as the [DNS provider](https://docs.traefik.io/https/acme/#providers), you may need to replace my env with your own. 23 | 24 | To verify your domain, letsencrypt will use your DNS provider's API to create a `TXT` record with the key `_acme-challenge`. Then, the ACME auth service will query your DNS settings to see if the record is there, proving you own the domain. 25 | 26 | 1. Create an API token by going to your profile then clicking the API token tab. Permissions should look something like this. I use the same token for `DNS` and `ZONE`, and I think it's probably easiest for you to do the same. 27 | 1. If verification fails, look at the traefik logs. **You may need to manually delete the `TXT` record in your DNS console before trying again.** 28 | 29 | ![Cloudflare API SCreenshot](images/cfapi.png) 30 | 31 | ### Running Traefik 32 | 33 | ```bash 34 | export LETSENCRYPT_EMAIL=youremail@domain.com 35 | export CF_API_EMAIL=youremail@domain.com 36 | export CF_TOKEN=your_token 37 | docker run --rm --name traefik \ 38 | --env CF_API_EMAIL=${CF_API_EMAIL} \ 39 | --env CF_DNS_API_TOKEN=${CF_TOKEN} \ 40 | --env CF_ZONE_API_TOKEN=${CF_TOKEN} \ 41 | --volume /var/run/docker.sock:/var/run/docker.sock \ 42 | --volume "/var/lib/traefik/letsencrypt:/letsencrypt" \ 43 | --publis 8080:8080 \ 44 | --publish 80:80 \ 45 | --publish 443:443 \ 46 | traefik:v2.2 \ 47 | --api.insecure=true \ 48 | --providers.docker=true \ 49 | --providers.docker.exposedByDefault=false \ 50 | --entrypoints.web.address=:80 \ 51 | --entrypoints.websecure.address=:443 \ 52 | --certificatesResolvers.myresolver.acme.caServer="https://acme-v02.api.letsencrypt.org/directory" \ 53 | --certificatesresolvers.myresolver.acme.dnschallenge=true \ 54 | --certificatesresolvers.myresolver.acme.dnschallenge.provider=cloudflare \ 55 | --certificatesresolvers.myresolver.acme.email="${LETSENCRYPT_EMAIL}" \ 56 | --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json 57 | ``` 58 | 59 | Let's break down some of these lines. Anything not mentioned below is unrelated to SSL, but leaving it out may break my examples. 60 | 61 | ``` bash 62 | # This provides your API information to traefik through environment variables 63 | --env CF_API_EMAIL=${CF_API_EMAIL} 64 | --env CF_DNS_API_TOKEN=${CF_TOKEN} 65 | --env CF_ZONE_API_TOKEN=${CF_TOKEN} 66 | # Your certificates will be written to a JSON file that need to be stored persistently on your host disk. 67 | --volume "/var/lib/traefik/letsencrypt:/letsencrypt" 68 | # Use both entrypoints, because you'll need to accept connections on 80 to redirect to 443 69 | --entrypoints.web.address=:80 \ 70 | --entrypoints.websecure.address=:443 \ 71 | # Create a resolver named "myresolver" pointed at the production ACME api 72 | --certificatesResolvers.myresolver.acme.caServer="https://acme-v02.api.letsencrypt.org/directory" \ 73 | --certificatesresolvers.myresolver.acme.dnschallenge=true \ 74 | --certificatesresolvers.myresolver.acme.dnschallenge.provider=cloudflare \ 75 | --certificatesresolvers.myresolver.acme.email="${LETSENCRYPT_EMAIL}" \ 76 | --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json 77 | ``` 78 | 79 | At this point, **traefik will not generate a cert** because you don't have any services using the `myresolver` resolver yet. 80 | 81 | ### Running a service 82 | 83 | ```bash 84 | export MY_APP_DOMAIN=mydomain.com 85 | docker run --rm --name whoami \ 86 | --label traefik.enable=true \ 87 | --label traefik.http.services.my-service.loadbalancer.server.port="80" \ 88 | --label traefik.http.middlewares.my-insecure-redirect-middleware.redirectscheme.scheme="https" \ 89 | --label traefik.http.routers.my-route.entrypoints=web \ 90 | --label traefik.http.routers.my-route.rule="Host(`whoami.${MY_APP_DOMAIN}`)" \ 91 | --label traefik.http.routers.my-route.middlewares="my-insecure-redirect-middleware" \ 92 | --label traefik.http.routers.my-secure-route.entrypoints=websecure \ 93 | --label traefik.http.routers.my-secure-route.rule="Host(`whoami.${MY_APP_DOMAIN}`)" \ 94 | --label traefik.http.routers.my-secure-route.tls.domains[0].main="*.${DNS_DOMAIN}" \ 95 | --label traefik.http.routers.my-secure-route.tls.certresolver="myresolver" \ 96 | --label traefik.http.routers.my-secure-route.tls=true \ 97 | containous/whoami 98 | ``` 99 | 100 | Let's break this config down. 101 | 102 | ``` bash 103 | # Create an HTTP (insecure) route that only has redirect middleware 104 | --label traefik.http.services.my-service.loadbalancer.server.port="80" \ 105 | --label traefik.http.middlewares.my-insecure-redirect-middleware.redirectscheme.scheme="https" \ 106 | # This insecure route only listens on the insecure entrypoint 107 | --label traefik.http.routers.my-route.entrypoints=web \ 108 | # Both secure and insecure need the routing rule 109 | --label traefik.http.routers.my-route.rule="Host(`whoami.${MY_APP_DOMAIN}`)" \ 110 | # Very important to register the middleware on the route 111 | --label traefik.http.routers.my-route.middlewares="my-insecure-redirect-middleware" \ 112 | # Create an HTTPS (secure) route that references "myresolver" from traefik config 113 | --label traefik.http.routers.my-secure-route.entrypoints=websecure \ 114 | --label traefik.http.routers.my-secure-route.rule="Host(`whoami.${MY_APP_DOMAIN}`)" \ 115 | # Use your wildcard and NOT the actual domain for domains[0].main. 116 | --label traefik.http.routers.my-secure-route.tls.domains[0].main="*.${MY_APP_DOMAIN}" \ 117 | # If you ARENT hosting any routes for the bare domain (mydomain.com), you can REMOVE the SAN (Subject Alternative Name) 118 | # However, leaving it here doesn't hurt anything! 119 | --label traefik.http.routers.my-secure-route.tls.domains[0].san="${MY_APP_DOMAIN}" \ 120 | --label traefik.http.routers.my-secure-route.tls.certresolver="myresolver" \ 121 | --label traefik.http.routers.my-secure-route.tls=true \ 122 | ``` 123 | 124 | Why is https redirect so complicated? [Nobody seems to know](https://github.com/containous/traefik/issues/4863). 125 | 126 | ## Summary 127 | 128 | You should now be able to access https://whoami.mydomain.com in your browser! 129 | -------------------------------------------------------------------------------- /docs/wireguard-question.md: -------------------------------------------------------------------------------- 1 | # Sharing WireGuard network with other containers via docker 2 | 3 | I'm trying to do something a little "backwards". Most WireGuard tutorials show you how to share a WireGuard container's internet connection with peers, forwarding traffic from `wg0` to `eth0`. For example, the VPN Server configuration [described on ArchWiki](https://wiki.archlinux.org/index.php/WireGuard) 4 | 5 | **I want to do the opposite**. I want containers that share a network with WireGuard to be able to access its peers. 6 | 7 | ### wg0.conf 8 | 9 | My current config is lifted straight from archwiki, and works in the traditional VPN server way. 10 | 11 | ``` conf 12 | [Interface] 13 | Address = 10.200.200.1/24 14 | ListenPort = 51820 15 | PrivateKey = SERVER_PRIVATE_KEY 16 | PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE 17 | PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE 18 | 19 | [Peer] 20 | # foo 21 | PublicKey = PEER_FOO_PUBLIC_KEY 22 | PresharedKey = PRE-SHARED_KEY 23 | AllowedIPs = 10.200.200.2/32 24 | 25 | [Peer] 26 | # bar 27 | PublicKey = PEER_BAR_PUBLIC_KEY 28 | PresharedKey = PRE-SHARED_KEY 29 | AllowedIPs = 10.200.200.3/32 30 | ``` 31 | 32 | ### docker-compose.yml 33 | 34 | ``` yml 35 | version: '3.8' 36 | services: 37 | wireguard: 38 | image: linuxserver/wireguard 39 | container_name: wireguard 40 | cap_add: 41 | - NET_ADMIN 42 | - SYS_MODULE 43 | environment: 44 | - PUID=1000 45 | - PGID=1000 46 | - TZ=${TIME_ZONE} 47 | - SERVERURL="wireguard.${DNS_DOMAIN}" 48 | - SERVERPORT=51820 49 | - PEERS=2 50 | - PEERDNS=192.168.1.10 # DNS on my LAN, not important. 51 | - INTERNAL_SUBNET=10.200.200.0 52 | ports: 53 | - "51820:51820/udp" 54 | volumes: 55 | - wireguardconfig:/config 56 | - /lib/modules:/lib/modules 57 | sysctls: 58 | - "net.ipv4.ip_forward=1" 59 | - "net.ipv4.conf.all.src_valid_mark=1" 60 | networks: 61 | - wireguard-net 62 | 63 | ubuntu: 64 | image: ubuntu 65 | container_name: ubuntu 66 | entrypoint: /bin/sleep 67 | command: 10000000 68 | networks: 69 | - wireguard-net 70 | 71 | volumes: 72 | wireguardconfig: 73 | 74 | wireguard-net: 75 | name: wireguard-net 76 | ipam: 77 | config: 78 | - subnet: 10.200.0.0/16 79 | ``` 80 | 81 | ## Goal 82 | 83 | I want to be able to `docker exec` into ubunut and run `ping 10.200.200.2` when peer 2 is active. I don't think I'm far off. What am I missing? -------------------------------------------------------------------------------- /etc/authconfig.ini: -------------------------------------------------------------------------------- 1 | # Configuration example 2 | rule.webtop.action = auth 3 | rule.webtop.rule = Host(`webtop.subdavis.com`) 4 | rule.webtop.whitelist = davis.a.brandon@gmail.com 5 | -------------------------------------------------------------------------------- /etc/clickhouse/clickhouse-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | warning 4 | true 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /etc/clickhouse/clickhouse-user-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0 5 | 0 6 | 7 | 8 | -------------------------------------------------------------------------------- /etc/mdadm.conf: -------------------------------------------------------------------------------- 1 | ARRAY /dev/md/0 metadata=1.2 name=localhost:0 UUID=86102d98:fe537e8c:a1bb0f9e:88341045 2 | -------------------------------------------------------------------------------- /etc/sshd_config: -------------------------------------------------------------------------------- 1 | ChallengeResponseAuthentication no 2 | UsePAM no 3 | X11Forwarding no 4 | PasswordAuthentication no 5 | PrintMotd yes 6 | AcceptEnv LANG LC_* 7 | Subsystem sftp /usr/lib/openssh/sftp-server 8 | Match User edgerouter-backup 9 | PasswordAuthentication yes 10 | -------------------------------------------------------------------------------- /etc/traefik-logrotate.conf: -------------------------------------------------------------------------------- 1 | /media/local/traefik/logs/*.log { 2 | daily 3 | rotate 30 4 | missingok 5 | notifempty 6 | compress 7 | dateext 8 | dateformat .%Y-%m-%d 9 | postrotate 10 | "$HOME/bin/docker" -H unix:///run/user/1000/docker.sock kill --signal="USR1" $($HOME/bin/docker ps | grep traefik | awk '{print $1}') 11 | endscript 12 | } 13 | -------------------------------------------------------------------------------- /etc/traefik/rules-fail2ban.yml: -------------------------------------------------------------------------------- 1 | http: 2 | middlewares: 3 | fail2ban-mddl: 4 | plugin: 5 | fail2ban: 6 | whitelist: 7 | ip: 8 | - "192.168.1.1" 9 | rules: 10 | bantime: "12h" 11 | findtime: "10m" 12 | maxretry: 2000 13 | enabled: true 14 | urlregexp: "" 15 | ports: "80:443" 16 | -------------------------------------------------------------------------------- /etc/unbound.conf: -------------------------------------------------------------------------------- 1 | 2 | server: 3 | verbosity: 1 4 | interface: 127.0.0.2 5 | port: 53 6 | do-ip4: yes 7 | do-ip6: no 8 | do-udp: yes 9 | do-tcp: yes 10 | 11 | access-control: 127.0.0.0/8 allow 12 | 13 | hide-identity: yes 14 | hide-version: yes 15 | harden-glue: yes 16 | harden-dnssec-stripped: yes 17 | 18 | cache-min-ttl: 900 19 | cache-max-ttl: 14400 20 | prefetch: yes 21 | rrset-roundrobin: yes 22 | ssl-upstream: yes 23 | use-caps-for-id: yes 24 | 25 | # These are addresses on your private network, 26 | # and are not allowed to be returned for public 27 | # internet names. 28 | private-address: 192.168.0.0/16 29 | private-address: 172.16.0.0/12 30 | private-address: 10.0.0.0/8 31 | 32 | logfile: "/var/lib/unbound/unbound.log" 33 | verbosity: 0 34 | val-log-level: 3 35 | 36 | forward-zone: 37 | name: "." 38 | forward-addr: 1.1.1.1@853 39 | forward-addr: 1.0.0.1@853 40 | -------------------------------------------------------------------------------- /logrotate.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Rotate log files 3 | Documentation=man:logrotate(8) man:logrotate.conf(5) 4 | ConditionACPower=true 5 | 6 | [Service] 7 | Type=oneshot 8 | ExecStart=/usr/sbin/logrotate -s %h/logrotate.state %h/selfhosted/etc/traefik-logrotate.conf 9 | 10 | # performance options 11 | #Nice=19 12 | #IOSchedulingClass=best-effort 13 | #IOSchedulingPriority=7 14 | 15 | # hardening options 16 | # details: https://www.freedesktop.org/software/systemd/man/systemd.exec.html 17 | # no ProtectHome for userdir logs 18 | # no PrivateNetwork for mail deliviery 19 | # no ProtectKernelTunables for working SELinux with systemd older than 235 20 | # no MemoryDenyWriteExecute for gzip on i686 21 | #PrivateDevices=true 22 | #PrivateTmp=true 23 | #ProtectControlGroups=true 24 | #ProtectKernelModules=true 25 | #ProtectSystem=full 26 | #RestrictRealtime=true 27 | -------------------------------------------------------------------------------- /logrotate.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Daily rotation of log files 3 | Documentation=man:logrotate(8) man:logrotate.conf(5) 4 | 5 | [Timer] 6 | OnCalendar=daily 7 | AccuracySec=12h 8 | Persistent=true 9 | 10 | [Install] 11 | WantedBy=timers.target 12 | -------------------------------------------------------------------------------- /media-4tb.mount: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=4TB Disk 3 | 4 | [Mount] 5 | What=UUID=98d3794b-3f70-4bbc-8d39-ffd0847c621b 6 | Where=/media/4tb 7 | Type=ext4 8 | Options=defaults,auto,noatime,exec 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /media-primary.mount: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=RAID1 3 | 4 | [Mount] 5 | What=/dev/md0 6 | Where=/media/primary 7 | Type=ext4 8 | Options=defaults,auto,noatime,exec 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /media-secondary.mount: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=General data 3 | 4 | [Mount] 5 | What=UUID=7de2e82b-e282-468f-976a-8a2e6c9722a8 6 | Where=/media/secondary 7 | Type=ext4 8 | Options=defaults,auto,noatime,exec 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /media-wildy.mount: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Wildy 3 | 4 | [Mount] 5 | What=/dev/sdb1 6 | Where=/media/wildy 7 | #Type=ext2 8 | Options=defaults,auto,noatime,exec 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /wildy.compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | 4 | transmission: 5 | image: haugene/transmission-openvpn:latest 6 | container_name: transmission 7 | restart: always 8 | dns: 1.1.1.1 9 | user: ${XID:-1000} 10 | cap_add: 11 | - NET_ADMIN 12 | devices: 13 | - /dev/net/tun 14 | volumes: 15 | - ${PRIMARY_MOUNT}/data:/data 16 | - /etc/localtime:/etc/localtime:ro 17 | environment: 18 | - CREATE_TUN_DEVICE=true 19 | - "OPENVPN_PROVIDER=${OPENVPN_PROVIDER}" 20 | - "OPENVPN_USERNAME=${TORRENT_USERNAME}" 21 | - "OPENVPN_PASSWORD=${TORRENT_PASSWORD}" 22 | - "TRANSMISSION_UMASK=0" 23 | - "OPENVPN_OPTS=--inactive 3600 --ping 10 --ping-exit 60" 24 | - LOCAL_NETWORK=${LOCAL_NETWORK} 25 | 26 | transmission_proxy: 27 | image: haugene/transmission-openvpn-proxy:latest 28 | container_name: transmission_proxy 29 | restart: always 30 | ports: 31 | - "9091:8080" 32 | depends_on: 33 | - transmission 34 | 35 | watchtower: 36 | image: containrrr/watchtower:latest 37 | container_name: watchtower 38 | restart: always 39 | command: --schedule "0 0 */6 * * *" 40 | volumes: 41 | - ${SOCK_PATH}:/var/run/docker.sock 42 | - "${PRIMARY_MOUNT}/watchtower/config/:/config" 43 | - "${PRIMARY_MOUNT}/watchtower/docker-config.json:/config.json" 44 | --------------------------------------------------------------------------------