homeserver initial commit
- ansible - docker-compose - Kubernetes_deployments
This commit is contained in:
2
Kubernetes_deployments/.gitignore
vendored
Normal file
2
Kubernetes_deployments/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.secrets/
|
||||
.env
|
||||
393
Kubernetes_deployments/README.md
Normal file
393
Kubernetes_deployments/README.md
Normal file
@ -0,0 +1,393 @@
|
||||
Setup K3s Kubernetes Cluster
|
||||
===================================
|
||||
|
||||
# Configure Cert Manager for automating SSL certificate handling
|
||||
|
||||
Cert manager handles SSL certificate creation and renewal from Let's Encrypt.
|
||||
|
||||
```bash
|
||||
helm repo add jetstack https://charts.jetstack.io --force-update
|
||||
helm repo update
|
||||
|
||||
helm install \
|
||||
cert-manager jetstack/cert-manager \
|
||||
--namespace cert-manager \
|
||||
--create-namespace \
|
||||
--version v1.15.3 \
|
||||
--set crds.enabled=true \
|
||||
--set prometheus.enabled=false \
|
||||
--set webhook.timeoutSeconds=4 \
|
||||
```
|
||||
|
||||
Next, deploy the certificate Issuer. Issuers, and ClusterIssuers,
|
||||
are Kubernetes resources that represent certificate authorities (CAs) that are
|
||||
able to generate signed certificates by honoring certificate signing requests.
|
||||
All cert-manager certificates require a referenced issuer that is in a ready
|
||||
condition to attempt to honor the request.
|
||||
[Ref](https://cert-manager.io/docs/concepts/issuer/).
|
||||
|
||||
The template for ClusterIssuer is in the cert-manager directory. A single wildcard-cert
|
||||
will be created and used for all ingress subdomains.
|
||||
Create a new certificate and cert in cert directory
|
||||
and copy the secret manually to all the namespaces.
|
||||
|
||||
First add the DNS servers to the coreDNS config:
|
||||
|
||||
```bash
|
||||
export KUBE_EDITOR=nvim
|
||||
# Change the forward section with . 1.1.1.1 1.0.0.1
|
||||
kubectl -n kube-system edit configmap coredns
|
||||
```
|
||||
Next, deploy the ClusterIssuer, WildcardCert, and secrets using helm
|
||||
|
||||
```bash
|
||||
source .env
|
||||
helm install cert-handler cert-manager-helm-chart \
|
||||
--atomic --set secret.apiToken=$CLOUDFLARE_TOKEN \
|
||||
--set clusterIssuer.email=$EMAIL \
|
||||
--set wildcardCert.dnsNames[0]=$DNSNAME
|
||||
|
||||
# Copy the wildcard certificate to other namespaces
|
||||
kubectl get secret wildcard-cert-secret --namespace=cert-manager -o yaml \
|
||||
| sed 's/namespace: cert-manager/namespace: <namespace>/' | kubectl apply -f -
|
||||
```
|
||||
|
||||
If for some reason certificate secret `wildcard-cert-secret` is not generated,
|
||||
the issue can be related to cloudflare API token is wrong, the token secret is
|
||||
missing, the Issuer or ClusterIssuer is not ready etc.
|
||||
|
||||
Here are some troubleshoot commands to test:
|
||||
|
||||
```bash
|
||||
kubectl get clusterissuer
|
||||
kubectl describe clusterissuer
|
||||
kubectl get certificate -n nginx-test
|
||||
kubectl get certificateRequest -n nginx-test
|
||||
kubectl describe challenges -n cert-manager
|
||||
kubectl describe orders -n cert-manager
|
||||
```
|
||||
|
||||
# Deploy Private Docker Registry
|
||||
|
||||
Create a new namespace called docker-registry and deploy the private
|
||||
docker-registry.
|
||||
|
||||
First create docker credentials with htpasswd:
|
||||
|
||||
```bash
|
||||
htpasswd -cB registry-passwords USERNAME
|
||||
|
||||
kubectl create namespace docker-registry
|
||||
kubectl create secret generic registry-credentials \
|
||||
--from-file=.secrets/registry-passwords \
|
||||
-n docker-registry
|
||||
```
|
||||
|
||||
Next, deploy the docker registry with helm chart:
|
||||
|
||||
```bash
|
||||
source .env
|
||||
kubectl get secret wildcard-cert-secret --namespace=cert-manager -o yaml \
|
||||
| sed 's/namespace: cert-manager/namespace: docker-registry/' \
|
||||
| kubectl apply -f -
|
||||
|
||||
helm install registry docker-registry-helm-chart/ \
|
||||
--set host=$DOCKER_REGISTRY_HOST \
|
||||
--set ingress.tls.host=$DNSNAME \
|
||||
--atomic
|
||||
```
|
||||
|
||||
|
||||
# Deploy Portfolio Website from Private Docker Registry
|
||||
|
||||
First, create a secret to access the private docker registry. Then copy the
|
||||
wildcard CA cert and deploy the portfolio webapp.
|
||||
|
||||
```bash
|
||||
kubectl create namespace my-portfolio
|
||||
kubectl get secret wildcard-cert-secret --namespace=cert -o yaml \
|
||||
| sed 's/namespace: cert/namespace: my-portfolio/' | kubectl apply -f -
|
||||
|
||||
source .env
|
||||
kubectl create secret docker-registry my-registry-secret \
|
||||
--docker-server="${DOCKER_REGISTRY_HOST}" \
|
||||
--docker-username="${DOCKER_USER}" \
|
||||
--docker-password="${DOCKER_PASSWORD}" \
|
||||
-n my-portfolio
|
||||
|
||||
# use envsubst to substitute the environment variables in the manifest
|
||||
envsubst < my-portfolio/portfolioManifest.yaml | \
|
||||
kubectl apply -n my-portfolio -f -
|
||||
```
|
||||
|
||||
|
||||
# Expose External Services via Traefik Ingress Controller
|
||||
|
||||
External services hosted outside the kubernetes cluster can be exposed using
|
||||
the kubernetes traefik reverse proxy.
|
||||
|
||||
A nginx http server is deployed as a proxy that listens on port 80
|
||||
and redirects requests to the proxmox local IP address. The server has an
|
||||
associated clusterIP service which is exposed via ingress. The nginx proxy can
|
||||
be configured to listen to other ports and forward traffic to other external
|
||||
services running locally or remotely.
|
||||
|
||||
```bash
|
||||
source .env
|
||||
kubectl create namespace external-services
|
||||
kubectl get secret wildcard-cert-secret --namespace=cert -o yaml \
|
||||
| sed 's/namespace: cert/namespace: external-services/' | kubectl apply -f -
|
||||
envsubst < external-service/proxmox.yaml | kubectl apply -n external-services -f -
|
||||
```
|
||||
|
||||
|
||||
# Create Shared NFS Storage for Plex and Jellyfin
|
||||
|
||||
A 1TB NVME SSD is mounted to one of the original homelab VMs. This serves as an
|
||||
NFS mount for all k3s nodes to use as shared storage for plex and jellyfin
|
||||
containers.
|
||||
|
||||
## On the host VM:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install nfs-kernel-server
|
||||
sudo chown nobody:nogroup /media/flexdrive
|
||||
|
||||
# Configure mount on /etc/fstab to persist across reboot
|
||||
sudo vim /etc/fstab
|
||||
# Add the following line. Change the filsystem if other than ntfs
|
||||
# /dev/sdb2 /media/flexdrive ntfs defaults 0 2
|
||||
|
||||
# Configure NFS exports by editing the NFS exports file
|
||||
sudo vim /etc/exports
|
||||
# Add the following line to the file
|
||||
# /media/flexdrive 192.168.1.113/24(rw,sync,no_subtree_check,no_root_squash)
|
||||
|
||||
# Apply the exports config
|
||||
sudo exportfs -ra
|
||||
|
||||
# Start and enable NFS Server
|
||||
sudo systemctl start nfs-kernel-server
|
||||
sudo systemctl enable nfs-kernel-server
|
||||
```
|
||||
|
||||
## On all the K3s VMs:
|
||||
```
|
||||
sudo apt install nfs-common
|
||||
sudo mkdir /mnt/media
|
||||
sudo mount 192.168.1.113:/media/flexdrive /mnt/media
|
||||
# And test if the contents are visible
|
||||
# After that unmount with the following command as mounting will be taken care
|
||||
# by k8s
|
||||
sudo umount /mnt/media
|
||||
```
|
||||
|
||||
|
||||
# Deploy Jellyfin Container in K3s
|
||||
|
||||
Jellyfin is a media server that can be used to organize, play, and stream
|
||||
audio and video files. The Jellyfin container is deployed in the k3s cluster
|
||||
using the NFS shared storage for media files. Due to segregated nature of the
|
||||
media manifest files, it has not been helm charted.
|
||||
|
||||
```bash
|
||||
source .env
|
||||
kubectl create namespace media
|
||||
kubectl get secret wildcard-cert-secret --namespace=cert-manager -o yaml \
|
||||
| sed 's/namespace: cert-manager/namespace: media/' | kubectl apply -f -
|
||||
|
||||
# Create a new storageclass called manual to not use longhorn storageclass
|
||||
kubectl apply -f media/storageclass-nfs.yaml
|
||||
|
||||
# Create NFS PV and PVC
|
||||
envsubst < media/pv.yaml | kubectl apply -n media -f -
|
||||
kubectl apply -f media/pvc.yaml -n media
|
||||
|
||||
# Deploy Jellyfin
|
||||
envsubst < media/jellyfin-deploy.yaml | kubectl apply -n media -f -
|
||||
```
|
||||
|
||||
## Transfer media files from one PVC to another (Optional)
|
||||
|
||||
To transfer media files from one PVC to another, create a temporary pod to copy
|
||||
files from one PVC to another. The following command will create a temporary
|
||||
pod in the media namespace to copy files from one PVC to another.
|
||||
|
||||
```bash
|
||||
# Create a temporary pod to copy files from one PVC to another
|
||||
k apply -f temp-deploy.yaml -n media
|
||||
# Copy files from one PVC to another
|
||||
kubectl exec -it temp-pod -n media -- bash
|
||||
cp -r /mnt/source/* /mnt/destination/
|
||||
```
|
||||
|
||||
|
||||
# Create Storage Solution
|
||||
|
||||
```bash
|
||||
# On each VM
|
||||
sudo mkfs.ext4 /dev/sda4
|
||||
sudo mkdir /mnt/longhorn
|
||||
sudo mount /dev/sda4 /mnt/longhorn
|
||||
|
||||
helm repo add longhorn https://charts.longhorn.io
|
||||
helm repo update
|
||||
|
||||
kubectl create namespace longhorn-system
|
||||
helm install longhorn longhorn/longhorn --namespace longhorn-system
|
||||
|
||||
kubectl -n longhorn-system get pods
|
||||
# Access longhorn UI
|
||||
kubectl -n longhorn-system port-forward svc/longhorn-frontend 8080:80
|
||||
|
||||
# If the /mnt/longhorn is not shown
|
||||
kubectl -n longhorn-system get nodes.longhorn.io
|
||||
kubectl -n longhorn-system edit nodes.longhorn.io <node-name>
|
||||
```
|
||||
Add the following block under disks for all nodes:
|
||||
|
||||
```bash
|
||||
custom-disk-mnt-longhorn: # New disk for /mnt/longhorn
|
||||
allowScheduling: true
|
||||
diskDriver: ""
|
||||
diskType: filesystem
|
||||
evictionRequested: false
|
||||
path: /mnt/longhorn # Specify the new mount path
|
||||
storageReserved: 0 # Adjust storageReserved if needed
|
||||
tags: []
|
||||
|
||||
# Set number of replica count to 1
|
||||
kubectl edit configmap -n longhorn-system longhorn-storageclass
|
||||
set the numberOfReplicas: "1"
|
||||
```
|
||||
|
||||
|
||||
# Configure AdGuard Adblocker
|
||||
|
||||
AdGuard is deployed in the K3S cluster for network ad protection.
|
||||
A loadbalancer service is used for DNS resolution and clusterIP
|
||||
and ingress for the WEBUI.
|
||||
|
||||
The adguard initial admin port is 3000 which is bound to the loadbalancer IP
|
||||
from the local network. The AdGuard UI is accessible from the ingress
|
||||
domain on the internet.
|
||||
|
||||
```bash
|
||||
kubectl create namespace adguard
|
||||
kubectl get secret wildcard-cert-secret --namespace=cert -o yaml \
|
||||
| sed 's/namespace: cert/namespace: adguard/' | kubectl apply -f -
|
||||
|
||||
source .env
|
||||
helm install adguard \
|
||||
--set host=$ADGUARD_HOST \
|
||||
--atomic adguard-helm-chart
|
||||
```
|
||||
|
||||
|
||||
# Pocketbase Database and Authentication Backend
|
||||
|
||||
Pocketbase serves as the database and authentication backend for
|
||||
various side projects.
|
||||
|
||||
```bash
|
||||
# Create namespace and copy the wildcard cert secret
|
||||
kubectl create namespace pocketbase
|
||||
kubectl get secret wildcard-cert-secret --namespace=cert-manager -o yaml \
|
||||
| sed 's/namespace: cert-manager/namespace: pocketbase/' | kubectl apply -f -
|
||||
|
||||
# Deploy pocketbase using helm chart
|
||||
helm install pocketbase \
|
||||
--set ingress.host=$POCKETBASE_HOST \
|
||||
--set ingress.tls.hosts[0]=$DNSNAME \
|
||||
--atomic pocketbase-helm-chart
|
||||
```
|
||||
|
||||
It may be required to create initial user and password for the superuser.
|
||||
To do that, exec into the pod and run the following command:
|
||||
|
||||
```bash
|
||||
pocketbase superuser create email password
|
||||
```
|
||||
|
||||
# qBittorrent with Wireguard
|
||||
|
||||
qBittorrent is deployed with wireguard to route traffic through a VPN tunnel.
|
||||
The following packages must be installed on each node:
|
||||
|
||||
```bash
|
||||
# On each k3s node
|
||||
sudo apt update
|
||||
sudo apt install -y wireguard wireguard-tools linux-headers-$(uname -r)
|
||||
```
|
||||
|
||||
The qBittorrent is deplyoyed via helm chart. The qBittorrent deployment uses
|
||||
the `media-nfs-pv` common NFS PVC for downloads. The helm chart contains both
|
||||
qBittorrent and wireguard. For security, qBittorrent is not exposed outside the
|
||||
network via ingress. It is accessible locally via loadbalancer IP address.
|
||||
|
||||
```bash
|
||||
helm install qbittorrent qbittorrent-helm-chart/ --atomic
|
||||
```
|
||||
|
||||
After deployment, verify qBittorrent is accessible on the loadbalancer IP and
|
||||
port. Login to the qBittorrent UI with default credentials from the deployment log.
|
||||
Change the user settings under settings/WebUI.
|
||||
Configure the network interface (wg0) in settings/Advanced and
|
||||
set download/upload speeds in settings/speed.
|
||||
|
||||
Also verify the VPM is working by executing the following command on the qBittorrent pod:
|
||||
|
||||
```bash
|
||||
curl ipinfo.io
|
||||
```
|
||||
|
||||
|
||||
# PostgreSQL Database
|
||||
|
||||
The PostgreSQL database uses the bitnami postgres helm chart with one primary
|
||||
and one replica statefulset, totaling 2 postgres pods.
|
||||
|
||||
```bash
|
||||
# Add the Bitnami repo if not already added
|
||||
helm repo add bitnami https://charts.bitnami.com/bitnami
|
||||
helm repo update
|
||||
|
||||
# Install PostgreSQL with these values
|
||||
source .env
|
||||
helm install my-postgres \
|
||||
bitnami/postgresql -f values.yaml \
|
||||
--set global.postgresql.auth.username=$POSTGRES_USER \
|
||||
--set global.postgresql.auth.password=$POSTGRES_PASSWORD \
|
||||
--set global.postgresql.auth.postgresPassword=$POSTGRES_PASSWORD \
|
||||
--atomic \
|
||||
-n postgres
|
||||
```
|
||||
|
||||
## Connect to the Database
|
||||
|
||||
```bash
|
||||
psql -U $POSTGRES_USER -d postgres --host 192.168.1.145 -p 5432
|
||||
```
|
||||
|
||||
## Backup and Restore PostgreSQL Database
|
||||
|
||||
```bash
|
||||
# To backup
|
||||
# Dump format is compressed and allows parallel restore
|
||||
pg_dump -U $POSTGRES_USER -h 192.168.1.145 -p 5432 -F c -f db_backup.dump postgres
|
||||
|
||||
# To restore
|
||||
pg_restore -U $POSTGRES_USER -h 192.168.1.145 -p 5432 -d postgres db_backup.dump
|
||||
```
|
||||
|
||||
## pgAdmin
|
||||
|
||||
pgAdmin provides GUI support for PostgreSQL database management. Deploy using
|
||||
pgadmin.yaml manifest under postgres directory. The environment variables are
|
||||
substituted from the .env file.
|
||||
|
||||
```bash
|
||||
source .env
|
||||
envsubst < postgres/pgadmin.yaml | kubectl apply -n postgres -f -
|
||||
```
|
||||
6
Kubernetes_deployments/adguard-helm-chart/Chart.yaml
Normal file
6
Kubernetes_deployments/adguard-helm-chart/Chart.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: adguard
|
||||
description: A Helm chart for deploying AdGuard Home
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "latest"
|
||||
@ -0,0 +1,37 @@
|
||||
{{/*
|
||||
Expand the name of the release
|
||||
*/}}
|
||||
{{- define "adguard.fullname" -}}
|
||||
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Common labels for all resources
|
||||
*/}}
|
||||
{{- define "adguard.labels" -}}
|
||||
app: {{ include "adguard.fullname" . }}
|
||||
chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Generate a name for the PVC
|
||||
*/}}
|
||||
{{- define "adguard.pvcName" -}}
|
||||
{{ printf "%s-pvc" (include "adguard.fullname" .) }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Generate a name for the service
|
||||
*/}}
|
||||
{{- define "adguard.serviceName" -}}
|
||||
{{ printf "%s-service" (include "adguard.fullname" .) }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Generate a name for the ingress
|
||||
*/}}
|
||||
{{- define "adguard.ingressName" -}}
|
||||
{{ printf "%s-ingress" (include "adguard.fullname" .) }}
|
||||
{{- end -}}
|
||||
@ -0,0 +1,44 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-home
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
app: {{ .Values.deployment.labels.app }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ .Values.deployment.labels.app }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ .Values.deployment.labels.app }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ .Values.deployment.labels.app }}
|
||||
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.deployment.adminContainerPort }}
|
||||
protocol: TCP
|
||||
name: admin
|
||||
- containerPort: {{ .Values.deployment.httpContainerPort }}
|
||||
name: http
|
||||
protocol: TCP
|
||||
- containerPort: {{ .Values.deployment.dns.tcp }}
|
||||
name: dns-tcp
|
||||
protocol: TCP
|
||||
- containerPort: {{ .Values.deployment.dns.udp }}
|
||||
name: dns-udp
|
||||
protocol: UDP
|
||||
volumeMounts:
|
||||
- name: adguard-config
|
||||
mountPath: /opt/adguardhome/conf
|
||||
- name: adguard-work
|
||||
mountPath: /opt/adguardhome/work
|
||||
volumes:
|
||||
- name: adguard-config
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.pvc.claimName }}
|
||||
- name: adguard-work
|
||||
emptyDir: {}
|
||||
@ -0,0 +1,27 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-ingress
|
||||
namespace: {{ .Values.namespace }}
|
||||
annotations:
|
||||
{{- toYaml .Values.ingress.annotations | nindent 4 }}
|
||||
spec:
|
||||
rules:
|
||||
- host: {{ .Values.host }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ .Release.Name }}-web-ui
|
||||
port:
|
||||
number: {{ .Values.service.port }}
|
||||
{{- if .Values.ingress.tls.enabled }}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .Values.host }}
|
||||
secretName: {{ .Values.ingress.tls.secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
12
Kubernetes_deployments/adguard-helm-chart/templates/pvc.yaml
Normal file
12
Kubernetes_deployments/adguard-helm-chart/templates/pvc.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-pvc
|
||||
namespace: {{ .Values.namespace }}
|
||||
spec:
|
||||
accessModes:
|
||||
- {{ .Values.pvc.accessModes }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.pvc.size }}
|
||||
storageClassName: {{ .Values.pvc.storageClass }}
|
||||
@ -0,0 +1,37 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-service
|
||||
namespace: {{ .Values.namespace }}
|
||||
spec:
|
||||
selector:
|
||||
app: {{ .Release.Name }}-home
|
||||
ports:
|
||||
- port: {{ .Values.service.dnsPort.udp }}
|
||||
targetPort: {{ .Values.service.dnsPort.udp }}
|
||||
protocol: UDP
|
||||
name: dns-udp
|
||||
- port: {{ .Values.service.dnsPort.tcp }}
|
||||
targetPort: {{ .Values.service.dnsPort.tcp }}
|
||||
protocol: TCP
|
||||
name: dns-tcp
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: {{ .Values.service.adminTargetPort }}
|
||||
protocol: TCP
|
||||
name: admin
|
||||
type: {{ .Values.service.type }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-web-ui
|
||||
namespace: {{ .Values.namespace }}
|
||||
spec:
|
||||
selector:
|
||||
app: {{ .Release.Name }}-home
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: {{ .Values.service.webUiPort }}
|
||||
protocol: TCP
|
||||
name: http
|
||||
58
Kubernetes_deployments/adguard-helm-chart/values.yaml
Normal file
58
Kubernetes_deployments/adguard-helm-chart/values.yaml
Normal file
@ -0,0 +1,58 @@
|
||||
apiVersion: v1
|
||||
appVersion: "latest"
|
||||
description: A Helm chart for deploying AdGuard Home
|
||||
name: adguard-home
|
||||
namespace: adguard
|
||||
version: 0.1.0
|
||||
|
||||
replicaCount: 1
|
||||
host: adguard.example.com # Change this to your domain
|
||||
|
||||
deployment:
|
||||
adminContainerPort: 3000
|
||||
httpContainerPort: 80
|
||||
dns:
|
||||
tcp: 53
|
||||
udp: 53
|
||||
labels:
|
||||
app: adguard-home
|
||||
|
||||
image:
|
||||
repository: adguard/adguardhome
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
service:
|
||||
type: LoadBalancer
|
||||
port: 80
|
||||
adminTargetPort: 3000
|
||||
webUiPort: 80
|
||||
dnsPort:
|
||||
udp: 53
|
||||
tcp: 53
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
traefik.ingress.kubernetes.io/affinity: "true"
|
||||
traefik.ingress.kubernetes.io/session-cookie-name: "SESSIONID"
|
||||
hosts:
|
||||
- host:
|
||||
paths:
|
||||
- /
|
||||
tls:
|
||||
enabled: true
|
||||
secretName: wildcard-cert-secret
|
||||
|
||||
pvc:
|
||||
claimName: adguard-pvc
|
||||
enabled: true
|
||||
storageClass: longhorn
|
||||
accessModes: ReadWriteOnce
|
||||
size: 1Gi
|
||||
|
||||
resources: {}
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
@ -0,0 +1,5 @@
|
||||
apiVersion: v2
|
||||
name: cert-manager
|
||||
description: A Helm chart for cert-manager
|
||||
version: 0.1.0
|
||||
appVersion: "v1.11.0"
|
||||
@ -0,0 +1,18 @@
|
||||
# filepath: /home/taqi/homeserver/k3s-infra/cert-manager/templates/clusterIssuer.yaml
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: {{ .Values.clusterIssuer.name }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
spec:
|
||||
acme:
|
||||
server: {{ .Values.clusterIssuer.server }}
|
||||
privateKeySecretRef:
|
||||
name: {{ .Values.clusterIssuer.privateKeySecretRef }}
|
||||
solvers:
|
||||
- dns01:
|
||||
cloudflare:
|
||||
email: {{ .Values.clusterIssuer.email }}
|
||||
apiTokenSecretRef:
|
||||
name: {{ .Values.clusterIssuer.apiTokenSecretRef.name }}
|
||||
key: {{ .Values.clusterIssuer.apiTokenSecretRef.key }}
|
||||
@ -0,0 +1,8 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ .Values.secret.name }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
type: Opaque
|
||||
data:
|
||||
api-token: {{ .Values.secret.apiToken }}
|
||||
@ -0,0 +1,14 @@
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: {{ .Values.wildcardCert.name }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
spec:
|
||||
secretName: {{ .Values.wildcardCert.secretName }}
|
||||
issuerRef:
|
||||
name: {{ .Values.clusterIssuer.name }}
|
||||
kind: ClusterIssuer
|
||||
dnsNames:
|
||||
{{- range .Values.wildcardCert.dnsNames }}
|
||||
- "{{ . }}"
|
||||
{{- end }}
|
||||
21
Kubernetes_deployments/cert-manager-helm-chart/values.yaml
Normal file
21
Kubernetes_deployments/cert-manager-helm-chart/values.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
namespace: cert-manager
|
||||
|
||||
clusterIssuer:
|
||||
name: acme-issuer
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
privateKeySecretRef: example-issuer-account-key
|
||||
email: EMAIL
|
||||
apiTokenSecretRef:
|
||||
name: cloudflare-api-token-secret
|
||||
key: api-token
|
||||
|
||||
wildcardCert:
|
||||
name: wildcard-cert
|
||||
secretName: wildcard-cert-secret
|
||||
dnsNames:
|
||||
- ".example.com"
|
||||
|
||||
secret:
|
||||
type: Opaque
|
||||
name: cloudflare-api-token-secret
|
||||
apiToken: base64encodedtoken
|
||||
@ -0,0 +1,5 @@
|
||||
apiVersion: v2
|
||||
name: docker-registry-helm-chart
|
||||
description: A Helm chart for deploying a Docker registry
|
||||
version: 0.1.0
|
||||
appVersion: "latest"
|
||||
@ -0,0 +1,49 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ .Values.name }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
spec:
|
||||
replicas: {{ .Values.deployment.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ .Values.name }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ .Values.name }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ .Values.name }}
|
||||
image: {{ .Values.deployment.image }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.deployment.containerPort }}
|
||||
env:
|
||||
- name: REGISTRY_AUTH
|
||||
value: "htpasswd"
|
||||
- name: REGISTRY_AUTH_HTPASSWD_REALM
|
||||
value: "Registry Realm"
|
||||
- name: REGISTRY_AUTH_HTPASSWD_PATH
|
||||
value: "/auth/registry-passwords"
|
||||
- name: REGISTRY_AUTH_HTPASSWD_FILE
|
||||
value: "/auth/registry-passwords"
|
||||
- name: REGISTRY_HTTP_HEADERS
|
||||
value: |
|
||||
Access-Control-Allow-Origin: ['{{ .Values.uiDomain }}']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE', 'POST', 'PUT']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept', 'Content-Type', 'X-Requested-With', 'Cache-Control']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
volumeMounts:
|
||||
- name: {{ .Values.deployment.registryStorageVolumeName }}
|
||||
mountPath: /var/lib/registry
|
||||
- name: {{ .Values.deployment.authStorageVolumeName }}
|
||||
mountPath: /auth
|
||||
volumes:
|
||||
- name: {{ .Values.deployment.registryStorageVolumeName }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.pvc.claimName }}
|
||||
- name: {{ .Values.deployment.authStorageVolumeName }}
|
||||
secret:
|
||||
secretName: {{ .Values.credentialSecret.name }}
|
||||
@ -0,0 +1,29 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ .Values.name }}-ingress
|
||||
namespace: {{ .Values.namespace }}
|
||||
annotations:
|
||||
{{- range $key, $value := .Values.ingress.annotations }}
|
||||
{{ $key }}: {{ $value | quote }}
|
||||
{{- end }}
|
||||
spec:
|
||||
rules:
|
||||
- host: "{{ .Values.host }}"
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ .Values.name }}-service
|
||||
port:
|
||||
number: 5000
|
||||
{{- if .Values.ingress.tls.enabled }}
|
||||
tls:
|
||||
- hosts:
|
||||
- "{{ .Values.ingress.tls.host }}"
|
||||
secretName: {{ .Values.ingress.tls.secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ .Values.pvc.claimName }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
spec:
|
||||
accessModes:
|
||||
- {{ .Values.pvc.accessMode }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.pvc.size }}
|
||||
@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Values.name }}-service
|
||||
namespace: {{ .Values.namespace }}
|
||||
spec:
|
||||
selector:
|
||||
app: {{ .Values.name }}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: {{ .Values.deployment.containerPort }}
|
||||
type: {{ .Values.service.type }}
|
||||
@ -0,0 +1,34 @@
|
||||
name: registry
|
||||
namespace: docker-registry
|
||||
storage: 5Gi
|
||||
host: registry.example.com
|
||||
|
||||
deployment:
|
||||
replicas: 1
|
||||
containerPort: 5000
|
||||
image: registry:2
|
||||
registryStorageVolumeName: registry-storage
|
||||
authStorageVolumeName: auth-storage
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
tls:
|
||||
enabled: true
|
||||
host: "*.example.com"
|
||||
secretName: wildcard-cert-secret
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 5000
|
||||
|
||||
pvc:
|
||||
claimName: registry-pvc
|
||||
enabled: true
|
||||
storageClass: longhorn
|
||||
accessMode: ReadWriteOnce
|
||||
size: 5Gi
|
||||
|
||||
credentialSecret:
|
||||
name: registry-credentials
|
||||
79
Kubernetes_deployments/external-service/proxmox.yaml
Normal file
79
Kubernetes_deployments/external-service/proxmox.yaml
Normal file
@ -0,0 +1,79 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: proxmox-proxy
|
||||
namespace: external-services
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: proxmox-proxy
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: proxmox-proxy
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- containerPort: 80
|
||||
volumeMounts:
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/nginx.conf
|
||||
subPath: nginx.conf
|
||||
volumes:
|
||||
- name: nginx-config
|
||||
configMap:
|
||||
name: proxmox-proxy-config
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: proxmox-proxy
|
||||
namespace: external-services
|
||||
spec:
|
||||
selector:
|
||||
app: proxmox-proxy
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: proxmox-proxy-config
|
||||
namespace: external-services
|
||||
data:
|
||||
nginx.conf: |
|
||||
events {}
|
||||
http {
|
||||
server {
|
||||
listen 80;
|
||||
location / {
|
||||
proxy_pass https://${PROXMOX_IP};
|
||||
proxy_ssl_verify off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
---
|
||||
apiVersion: traefik.containo.us/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: proxmox-route
|
||||
namespace: external-services
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`${PROXMOX_HOST}`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: proxmox-proxy
|
||||
port: 80
|
||||
tls:
|
||||
secretName: wildcard-cert-secret
|
||||
103
Kubernetes_deployments/media/jellyfin-deploy.yaml
Normal file
103
Kubernetes_deployments/media/jellyfin-deploy.yaml
Normal file
@ -0,0 +1,103 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: jellyfin-network-config
|
||||
data:
|
||||
network.xml: |
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<NetworkConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<BaseUrl>/</BaseUrl>
|
||||
<EnableHttps>true</EnableHttps>
|
||||
<RequireHttps>true</RequireHttps>
|
||||
<InternalHttpPort>8096</InternalHttpPort>
|
||||
<InternalHttpsPort>8920</InternalHttpsPort>
|
||||
<PublicHttpPort>80</PublicHttpPort>
|
||||
<PublicHttpsPort>443</PublicHttpsPort>
|
||||
<EnableRemoteAccess>true</EnableRemoteAccess>
|
||||
<EnablePublishedServerUriByRequest>true</EnablePublishedServerUriByRequest>
|
||||
<PublishedServerUri>https://${JELLYFIN_HOST}</PublishedServerUri>
|
||||
</NetworkConfiguration>
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: jellyfin
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: jellyfin
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: jellyfin
|
||||
spec:
|
||||
containers:
|
||||
- name: jellyfin
|
||||
image: jellyfin/jellyfin:latest
|
||||
ports:
|
||||
- containerPort: 8096
|
||||
volumeMounts:
|
||||
- name: plex-media
|
||||
mountPath: /media
|
||||
- name: config
|
||||
mountPath: /config
|
||||
- name: network-config
|
||||
mountPath: /config/config/ network.xml
|
||||
subPath: network.xml
|
||||
volumes:
|
||||
- name: plex-media
|
||||
persistentVolumeClaim:
|
||||
claimName: media-nfs-pvc
|
||||
- name: config
|
||||
persistentVolumeClaim:
|
||||
claimName: plex-config-pvc
|
||||
- name: network-config
|
||||
configMap:
|
||||
name: jellyfin-network-config
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: jellyfin-service
|
||||
spec:
|
||||
selector:
|
||||
app: jellyfin
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8096
|
||||
targetPort: 8096
|
||||
type: ClusterIP
|
||||
|
||||
---
|
||||
apiVersion: traefik.containo.us/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: jellyfin-ingress
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.middlewares: jellyfin-headers@kubernetescrd
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`${JELLYFIN_HOST}`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: jellyfin-service
|
||||
port: 8096
|
||||
tls:
|
||||
secretName: wildcard-cert-secret
|
||||
|
||||
---
|
||||
apiVersion: traefik.containo.us/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: jellyfin-headers
|
||||
spec:
|
||||
headers:
|
||||
customRequestHeaders:
|
||||
X-Forwarded-Proto: "https"
|
||||
customResponseHeaders:
|
||||
X-Frame-Options: "SAMEORIGIN"
|
||||
16
Kubernetes_deployments/media/pv.yaml
Normal file
16
Kubernetes_deployments/media/pv.yaml
Normal file
@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: media-nfs-pv
|
||||
labels:
|
||||
app: media
|
||||
spec:
|
||||
capacity:
|
||||
storage: 900Gi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
nfs:
|
||||
path: /media/flexdrive # Path to your NFS share
|
||||
server: "${NFS_SERVER}" # IP of your NFS server (replace with correct IP)
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: manual
|
||||
11
Kubernetes_deployments/media/pvc-longhorn.yaml
Normal file
11
Kubernetes_deployments/media/pvc-longhorn.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: plex-config-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi # Storage for Jellyfin config files
|
||||
storageClassName: longhorn # Make sure this matches your Longhorn storage class
|
||||
14
Kubernetes_deployments/media/pvc.yaml
Normal file
14
Kubernetes_deployments/media/pvc.yaml
Normal file
@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: media-nfs-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 900Gi
|
||||
storageClassName: manual
|
||||
selector:
|
||||
matchLabels:
|
||||
app: media
|
||||
6
Kubernetes_deployments/media/storageclass-nfs.yaml
Normal file
6
Kubernetes_deployments/media/storageclass-nfs.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
name: manual
|
||||
provisioner: kubernetes.io/no-provisioner
|
||||
volumeBindingMode: WaitForFirstConsumer
|
||||
22
Kubernetes_deployments/media/temp-deploy.yaml
Normal file
22
Kubernetes_deployments/media/temp-deploy.yaml
Normal file
@ -0,0 +1,22 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: media-transfer-pod
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: media-transfer
|
||||
image: alpine # Use a lightweight image
|
||||
command: ["/bin/sh", "-c", "sleep 3600"] # Keep the pod alive
|
||||
volumeMounts:
|
||||
- name: plex-media
|
||||
mountPath: /mnt/longhorn
|
||||
- name: existing-media
|
||||
mountPath: /mnt/existing
|
||||
volumes:
|
||||
- name: plex-media
|
||||
persistentVolumeClaim:
|
||||
claimName: plex-media-longhorn
|
||||
- name: existing-media
|
||||
persistentVolumeClaim:
|
||||
claimName: plex-media-pvc
|
||||
14
Kubernetes_deployments/metallb/metallbConfig.yaml
Normal file
14
Kubernetes_deployments/metallb/metallbConfig.yaml
Normal file
@ -0,0 +1,14 @@
|
||||
apiVersion: metallb.io/v1beta1
|
||||
kind: IPAddressPool
|
||||
metadata:
|
||||
namespace: metallb-system
|
||||
name: my-ip-pool
|
||||
spec:
|
||||
addresses:
|
||||
- 192.168.1.141-192.168.1.150
|
||||
---
|
||||
apiVersion: metallb.io/v1beta1
|
||||
kind: L2Advertisement
|
||||
metadata:
|
||||
namespace: metallb-system
|
||||
name: my-l2-advertisement
|
||||
83
Kubernetes_deployments/my-portfolio/portfolioManifest.yaml
Normal file
83
Kubernetes_deployments/my-portfolio/portfolioManifest.yaml
Normal file
@ -0,0 +1,83 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: portfolio-app
|
||||
labels:
|
||||
app: portfolio-app
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: portfolio-app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: portfolio-app
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: my-registry-secret
|
||||
containers:
|
||||
- name: portfolio-app
|
||||
image: "${DOCKER_REGISTRY_HOST}/my-portfolio-app:latest"
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 80
|
||||
restartPolicy: Always
|
||||
terminationGracePeriodSeconds: 30
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: portfolio-app-svc
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
selector:
|
||||
app: portfolio-app
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: portfolio
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- "${DNSNAME}"
|
||||
secretName: wildcard-cert-secret
|
||||
rules:
|
||||
- host: "${PORTFOLIO_HOST}"
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: portfolio-app-svc
|
||||
port:
|
||||
number: 80
|
||||
- path: /experience
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: react-app-service
|
||||
port:
|
||||
number: 80
|
||||
- path: /interest
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: react-app-service
|
||||
port:
|
||||
number: 80
|
||||
- path: /project
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: react-app-service
|
||||
port:
|
||||
number: 80
|
||||
5
Kubernetes_deployments/pocketbase-helm-chart/Chart.yaml
Normal file
5
Kubernetes_deployments/pocketbase-helm-chart/Chart.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
apiVersion: v2
|
||||
name: pocketbase
|
||||
description: A Helm chart for deploying PocketBase
|
||||
version: 0.1.0
|
||||
appVersion: "latest"
|
||||
@ -0,0 +1,29 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ .Values.deployment.name }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
app: {{ .Values.deployment.labels.app }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ .Values.deployment.labels.app }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ .Values.deployment.labels.app }}
|
||||
spec:
|
||||
containers:
|
||||
- name: pocketbase
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
ports:
|
||||
- containerPort: {{ .Values.deployment.containerPort }}
|
||||
volumeMounts:
|
||||
- name: {{ .Values.pvc.name }}
|
||||
mountPath: {{ index .Values.deployment.volumeMounts 0 "mountPath" }}
|
||||
volumes:
|
||||
- name: {{ .Values.pvc.name }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.persistence.name }}
|
||||
@ -0,0 +1,32 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ .Values.ingress.name }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
annotations:
|
||||
{{- range $key, $value := .Values.ingress.annotations }}
|
||||
{{ $key }}: {{ $value | quote }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
rules:
|
||||
- host: {{ .Values.ingress.host }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ .Values.ingress.path }}
|
||||
pathType: {{ .Values.ingress.pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ .Values.service.name }}
|
||||
port:
|
||||
number: {{ .Values.service.port }}
|
||||
{{- if .Values.ingress.tls.enabled }}
|
||||
tls:
|
||||
- hosts:
|
||||
{{- range .Values.ingress.tls.hosts }}
|
||||
- "{{ . }}"
|
||||
{{- end }}
|
||||
secretName: {{ .Values.ingress.tls.secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ .Values.persistence.name }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
spec:
|
||||
accessModes:
|
||||
- {{ .Values.persistence.accessMode }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.size }}
|
||||
@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Values.service.name }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
spec:
|
||||
selector:
|
||||
app: {{ .Values.deployment.labels.app }}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: {{ .Values.deployment.containerPort }}
|
||||
type: {{ .Values.service.type }}
|
||||
43
Kubernetes_deployments/pocketbase-helm-chart/values.yaml
Normal file
43
Kubernetes_deployments/pocketbase-helm-chart/values.yaml
Normal file
@ -0,0 +1,43 @@
|
||||
replicaCount: 1
|
||||
namespace: pocketbase
|
||||
|
||||
deployment:
|
||||
name: pocketbase
|
||||
containerPort: 8090
|
||||
labels:
|
||||
app: pocketbase
|
||||
volumeMounts:
|
||||
- mountPath: /pb_data
|
||||
|
||||
image:
|
||||
repository: ghcr.io/muchobien/pocketbase
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
service:
|
||||
name: pocketbase
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
name: pocketbase-ingress
|
||||
className: traefik
|
||||
annotations: {}
|
||||
host: pocketbase.example.com
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
enabled: true
|
||||
secretName: wildcard-cert-secret
|
||||
hosts:
|
||||
- "*.example.com"
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
name: pocketbase-pvc
|
||||
accessMode: ReadWriteOnce
|
||||
size: 5Gi
|
||||
|
||||
pvc:
|
||||
name: pocketbase-data
|
||||
105
Kubernetes_deployments/postgres/pgadmin.yaml
Normal file
105
Kubernetes_deployments/postgres/pgadmin.yaml
Normal file
@ -0,0 +1,105 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: pgadmin-secret
|
||||
type: Opaque
|
||||
stringData:
|
||||
pgadmin-password: "${PGADMIN_PASSWORD}"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: pgadmin-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: pgadmin
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: pgadmin
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: pgadmin
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 5050 # pgAdmin group ID
|
||||
runAsUser: 5050 # pgAdmin user ID
|
||||
initContainers:
|
||||
- name: init-chmod
|
||||
image: busybox
|
||||
command: ["sh", "-c", "chown -R 5050:5050 /var/lib/pgadmin"]
|
||||
volumeMounts:
|
||||
- name: pgadmin-data
|
||||
mountPath: /var/lib/pgadmin
|
||||
securityContext:
|
||||
runAsUser: 0 # Run as root for chmod
|
||||
containers:
|
||||
- name: pgadmin
|
||||
image: dpage/pgadmin4:latest
|
||||
env:
|
||||
- name: SCRIPT_NAME
|
||||
value: /console
|
||||
- name: PGADMIN_DEFAULT_EMAIL
|
||||
value: "${PGADMIN_EMAIL}"
|
||||
- name: PGADMIN_DEFAULT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: pgadmin-secret
|
||||
key: pgadmin-password
|
||||
ports:
|
||||
- containerPort: 80
|
||||
volumeMounts:
|
||||
- name: pgadmin-data
|
||||
mountPath: /var/lib/pgadmin
|
||||
securityContext:
|
||||
runAsUser: 5050 # pgAdmin user ID
|
||||
runAsGroup: 5050 # pgAdmin group ID
|
||||
volumes:
|
||||
- name: pgadmin-data
|
||||
persistentVolumeClaim:
|
||||
claimName: pgadmin-pvc
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: pgadmin-service
|
||||
spec:
|
||||
type: LoadBalancer # or NodePort based on your setup
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
selector:
|
||||
app: pgadmin
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: pgadmin-ingress
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- "${DNSNAME}"
|
||||
secretName: wildcard-cert-secret
|
||||
rules:
|
||||
- host: "${PGADMIN_HOST}"
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: pgadmin-service
|
||||
port:
|
||||
number: 80
|
||||
31
Kubernetes_deployments/postgres/values.yaml
Normal file
31
Kubernetes_deployments/postgres/values.yaml
Normal file
@ -0,0 +1,31 @@
|
||||
image:
|
||||
registry: docker.io
|
||||
repository: bitnami/postgresql
|
||||
tag: "16.2.0-debian-11-r0"
|
||||
global:
|
||||
postgresql:
|
||||
auth:
|
||||
postgresPassword: "placeholder"
|
||||
username: "placeholder"
|
||||
password: "placeholder"
|
||||
database: "postgres"
|
||||
|
||||
primary:
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 5Gi
|
||||
service:
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
postgresql: 5432
|
||||
|
||||
readReplicas:
|
||||
replicaCount: 1 # This plus primary makes 2 total
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 5Gi
|
||||
|
||||
architecture: replication
|
||||
|
||||
volumePermissions:
|
||||
enabled: true
|
||||
6
Kubernetes_deployments/qbittorrent-helm-chart/Chart.yaml
Normal file
6
Kubernetes_deployments/qbittorrent-helm-chart/Chart.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: qbittorrent
|
||||
description: A Helm chart for deploying qBittorrent with WireGuard
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "latest"
|
||||
@ -0,0 +1,19 @@
|
||||
{{/*
|
||||
Expand the helper functions for the qBittorrent Helm chart
|
||||
*/}}
|
||||
|
||||
{{- define "qbittorrent.fullname" -}}
|
||||
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "qbittorrent.serviceName" -}}
|
||||
{{- printf "%s-service" (include "qbittorrent.fullname" .) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "qbittorrent.deploymentName" -}}
|
||||
{{- printf "%s-deployment" (include "qbittorrent.fullname" .) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "qbittorrent.configMapName" -}}
|
||||
{{- printf "%s-config" (include "qbittorrent.fullname" .) -}}
|
||||
{{- end -}}
|
||||
@ -0,0 +1,20 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ .Values.persistence.configMap.name }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
data:
|
||||
wg0.conf: |
|
||||
[Interface]
|
||||
Address = {{ .Values.wireguard.address }}
|
||||
PrivateKey = {{ .Values.wireguard.privateKey }}
|
||||
MTU = {{ .Values.wireguard.mtu }}
|
||||
DNS = {{ .Values.wireguard.dns }}
|
||||
ListenPort = {{ .Values.wireguard.listenPort }}
|
||||
|
||||
[Peer]
|
||||
PublicKey = {{ .Values.wireguard.peerPublicKey }}
|
||||
PresharedKey = {{ .Values.wireguard.presharedKey }}
|
||||
AllowedIPs = {{ .Values.wireguard.allowedIPs }}
|
||||
Endpoint = {{ .Values.wireguard.endpoint }}
|
||||
PersistentKeepalive = {{ .Values.wireguard.persistentKeepalive }}
|
||||
@ -0,0 +1,125 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ .Release.Name }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ .Release.Name }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ .Release.Name }}
|
||||
spec:
|
||||
initContainers:
|
||||
- name: wireguard-init
|
||||
image: {{ .Values.wireguardImage.repository }}:{{ .Values.wireguardImage.tag }}
|
||||
imagePullPolicy: {{ .Values.wireguardImage.pullPolicy }}
|
||||
securityContext:
|
||||
privileged: true
|
||||
capabilities:
|
||||
add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
set -x
|
||||
echo "Starting WireGuard initialization..."
|
||||
mkdir -p /etc/wireguard
|
||||
cp /config/wg_confs/wg0.conf /etc/wireguard/wg0.conf
|
||||
chmod 600 /etc/wireguard/wg0.conf
|
||||
|
||||
if ! lsmod | grep -q wireguard; then
|
||||
modprobe wireguard || echo "Failed to load wireguard module"
|
||||
fi
|
||||
|
||||
wg-quick up wg0 || echo "Failed to bring up WireGuard interface"
|
||||
|
||||
ip link show wg0
|
||||
wg show
|
||||
volumeMounts:
|
||||
- name: wireguard-config
|
||||
mountPath: /config/wg_confs
|
||||
- name: modules
|
||||
mountPath: /lib/modules
|
||||
containers:
|
||||
- name: wireguard
|
||||
image: {{ .Values.wireguardImage.repository }}:{{ .Values.wireguardImage.tag }}
|
||||
imagePullPolicy: {{ .Values.wireguardImage.pullPolicy }}
|
||||
securityContext:
|
||||
privileged: true
|
||||
capabilities:
|
||||
add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
env:
|
||||
- name: PUID
|
||||
value: "{{ .Values.config.puid }}"
|
||||
- name: PGID
|
||||
value: "{{ .Values.config.pgid }}"
|
||||
- name: UMASK_SET
|
||||
value: "{{ .Values.config.umask }}"
|
||||
- name: TZ
|
||||
value: "{{ .Values.config.timezone }}"
|
||||
volumeMounts:
|
||||
- name: wireguard-config
|
||||
mountPath: /config/wg_confs
|
||||
- name: modules
|
||||
mountPath: /lib/modules
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
while true; do
|
||||
if ! ip link show wg0 > /dev/null 2>&1; then
|
||||
wg-quick up wg0
|
||||
fi
|
||||
sleep 30
|
||||
done
|
||||
ports:
|
||||
- containerPort: {{ .Values.service.wireguardPort }}
|
||||
protocol: UDP
|
||||
- name: qbittorrent
|
||||
image: {{ .Values.qbittorrentImage.repository }}:{{ .Values.qbittorrentImage.tag }}
|
||||
imagePullPolicy: {{ .Values.qbittorrentImage.pullPolicy }}
|
||||
env:
|
||||
- name: PUID
|
||||
value: "{{ .Values.config.puid }}"
|
||||
- name: PGID
|
||||
value: "{{ .Values.config.pgid }}"
|
||||
- name: TZ
|
||||
value: "{{ .Values.config.timezone }}"
|
||||
- name: WEBUI_PORT
|
||||
value: "{{ .Values.config.webuiPort }}"
|
||||
volumeMounts:
|
||||
- name: qbittorrent-config
|
||||
mountPath: /config
|
||||
- name: downloads
|
||||
mountPath: /downloads
|
||||
ports:
|
||||
- containerPort: {{ .Values.deployment.containerPort }}
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: {{ .Values.deployment.containerPort }}
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
volumes:
|
||||
- name: qbittorrent-config
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.persistence.config.name }}
|
||||
- name: wireguard-config
|
||||
configMap:
|
||||
name: {{ .Values.persistence.configMap.name }}
|
||||
- name: downloads
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.persistence.downloads.existingClaim }}
|
||||
- name: modules
|
||||
hostPath:
|
||||
path: /lib/modules
|
||||
@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ .Values.persistence.config.name }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
spec:
|
||||
accessModes:
|
||||
- {{ .Values.persistence.config.accessMode }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.config.size }}
|
||||
storageClassName: {{ .Values.persistence.config.storageClass }}
|
||||
@ -0,0 +1,18 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Values.service.name }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
spec:
|
||||
selector:
|
||||
app: {{ .Release.Name }}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
name: qbittorrent
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: {{ .Values.deployment.containerPort }}
|
||||
- protocol: UDP
|
||||
name: wireguard
|
||||
port: {{ .Values.service.wireguardPort }}
|
||||
targetPort: {{ .Values.service.wireguardPort }}
|
||||
type: {{ .Values.service.type }}
|
||||
60
Kubernetes_deployments/qbittorrent-helm-chart/values.yaml
Normal file
60
Kubernetes_deployments/qbittorrent-helm-chart/values.yaml
Normal file
@ -0,0 +1,60 @@
|
||||
replicaCount: 1
|
||||
namespace: media
|
||||
|
||||
deployment:
|
||||
labels:
|
||||
app: qbittorrent
|
||||
containerPort: 8080
|
||||
image:
|
||||
repository: linuxserver/qbittorrent
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
|
||||
qbittorrentImage:
|
||||
repository: linuxserver/qbittorrent
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
|
||||
wireguardImage:
|
||||
repository: linuxserver/wireguard
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
|
||||
service:
|
||||
name: qbittorrent-service
|
||||
type: LoadBalancer
|
||||
port: 8080
|
||||
wireguardPort: 51820
|
||||
|
||||
persistence:
|
||||
config:
|
||||
enabled: true
|
||||
name: qbittorrent-config-pvc
|
||||
accessMode: ReadWriteOnce
|
||||
size: 1Gi
|
||||
storageClass: longhorn
|
||||
downloads:
|
||||
enabled: true
|
||||
existingClaim: media-nfs-pvc
|
||||
configMap:
|
||||
enabled: true
|
||||
name: wireguard-config
|
||||
|
||||
config:
|
||||
puid: 1000
|
||||
pgid: 1000
|
||||
timezone: Europe/Helsinki
|
||||
umask: 022
|
||||
webuiPort: 8080
|
||||
|
||||
wireguard:
|
||||
address: 10.182.199.210/32
|
||||
privateKey: WNDT2JsSZWw4q5EgsUKkBEX1hpWlpJGUTV/ibfJZOVo=
|
||||
mtu: 1329
|
||||
dns: 10.128.0.1
|
||||
listenPort: 51820
|
||||
peerPublicKey: PyLCXAQT8KkM4T+dUsOQfn+Ub3pGxfGlxkIApuig+hk=
|
||||
presharedKey: jSEf0xVUv/LwLmybp+LSM21Q2VOPbWPGcI/Dc4LLGkM=
|
||||
endpoint: europe3.vpn.airdns.org:1637
|
||||
allowedIPs: 0.0.0.0/0
|
||||
persistentKeepalive: 15
|
||||
46
Kubernetes_deployments/stress-test/hog.yaml
Normal file
46
Kubernetes_deployments/stress-test/hog.yaml
Normal file
@ -0,0 +1,46 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
deployment.kubernetes.io/revision: "1"
|
||||
generation: 1
|
||||
labels:
|
||||
app: hog
|
||||
name: hog
|
||||
namespace: test
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: hog
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app: hog
|
||||
spec:
|
||||
containers:
|
||||
- image: vish/stress
|
||||
imagePullPolicy: Always
|
||||
name: stress
|
||||
resources:
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
requests:
|
||||
memory: "500Mi"
|
||||
args:
|
||||
- -cpus
|
||||
- "2"
|
||||
- -mem-total
|
||||
- "1250Mi"
|
||||
- -mem-alloc-size
|
||||
- "100Mi"
|
||||
- -mem-alloc-sleep
|
||||
- "1s"
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
dnsPolicy: ClusterFirst
|
||||
restartPolicy: Always
|
||||
schedulerName: default-scheduler
|
||||
securityContext: {}
|
||||
terminationGracePeriodSeconds: 30
|
||||
173
README.md
Normal file
173
README.md
Normal file
@ -0,0 +1,173 @@
|
||||
# Homeserver Setup
|
||||
|
||||
```
|
||||
© 2023 Taqi Tahmid
|
||||
```
|
||||
|
||||
This is the top level directory for the homeserver setup. It contains the
|
||||
following directories:
|
||||
|
||||
1. `ansible`: Contains the ansible playbooks and roles for setting up the
|
||||
homeserver. Proxmox is used as the hypervisor, so the playbooks are written with
|
||||
that in mind. The Kubernetes cluster is set up using K3s. This has not been
|
||||
automated yet and is a manual process.
|
||||
|
||||
2. `docker`: Contains the docker-compose files for setting up the different
|
||||
services. Right now Kubernetes is preferred over docker-compose, so this
|
||||
directory is not actively maintained.
|
||||
|
||||
3. `kubernetes`: Contains the kubernetes manifests and helm charts for setting
|
||||
up the different services.
|
||||
|
||||
|
||||
# Services
|
||||
|
||||
The following services are set up on the homeserver:
|
||||
1. AdGuard Home
|
||||
2. Private Docker Registry
|
||||
3. Jellyfin
|
||||
4. My Portfolio Website
|
||||
5. Postgres Database
|
||||
6. Pocketbase Backend
|
||||
|
||||
In future the following services are planned to be set up:
|
||||
1. Nextcloud
|
||||
2. Gitea
|
||||
3. Monitoring Stack
|
||||
|
||||
|
||||
# Homeserver Setup
|
||||
|
||||
I have two mini PCs with Intel N1000 CPUs and 16GB of RAM each and 500GB SSDs.
|
||||
Both of them are running Proxmox and are connected to a 1Gbps network. The
|
||||
Proxmox is set up in a cluster configuration.
|
||||
|
||||
There are four VMs dedicated for the Kubernetes cluster. The VMs are running
|
||||
Ubuntu 22.04 and have 4GB of RAM and 2 CPUs each. The VMs are connected to a
|
||||
bridge network so that they can communicate with each other. Two VMs are
|
||||
confiugred as dual control plane and worker nodes and two VMs are configured as
|
||||
worker nodes. The Kubernetes cluster is set up using K3s.
|
||||
|
||||
## Proxmox Installation
|
||||
|
||||
The Proxmox installation is done on the mini PCs. The installation is done by
|
||||
booting from a USB drive with the Proxmox ISO. The installation is done on the
|
||||
SSD and the network is configured during the installation. The Proxmox is
|
||||
configured in a cluster configuration.
|
||||
|
||||
Ref: [proxmox-installation](https://pve.proxmox.com/wiki/Installation)
|
||||
|
||||
## Promox VM increase disk size
|
||||
|
||||
Access the Proxmox Web Interface:
|
||||
|
||||
1. Log in to the Proxmox web interface.
|
||||
Select the VM:
|
||||
2. In the left sidebar, click on the VM to resize.
|
||||
Resize the Disk:
|
||||
3. Go to the Hardware tab.
|
||||
4. Select the disk to resize (e.g., scsi0, ide0, etc.).
|
||||
5. Click on the Resize button in the toolbar.
|
||||
6. Enter 50G (or 50000 for 50GB) in the size field.
|
||||
|
||||
After that login to the VM and create a new partition or add to existing one
|
||||
```
|
||||
sudo fdisk /dev/sda
|
||||
Press p to print the partition table.
|
||||
Press d to delete the existing partition (note: ensure data is safe).
|
||||
Press n to create a new partition and follow the prompts. Make sure to use the
|
||||
same starting sector as the previous partition to avoid data loss. Press w to
|
||||
write changes and exit.
|
||||
sudo mkfs.ext4 /dev/sdaX
|
||||
```
|
||||
|
||||
## Proxmox Passthrough Physical Disk to VM
|
||||
|
||||
Ref: [proxmox-pve](https://pve.proxmox.com/wiki/Passthrough_Physical_Disk_to_Virtual_Machine_(VM))
|
||||
It is possible to pass through a physical disk attached to the hardware to
|
||||
the VM. This implementation passes through a NVME storage to the
|
||||
dockerhost VM.
|
||||
|
||||
```bash
|
||||
# List the disk by-id with lsblk
|
||||
lsblk |awk 'NR==1{print $0" DEVICE-ID(S)"}NR>1{dev=$1;printf $0" \
|
||||
";system("find /dev/disk/by-id -lname \"*"dev"\" -printf \" %p\"");\
|
||||
print "";}'|grep -v -E 'part|lvm'
|
||||
|
||||
# Hot plug or add the physical device as a new SCSI disk
|
||||
qm set 103 -scsi2 /dev/disk/by-id/usb-WD_BLACK_SN770_1TB_012938055C4B-0:0
|
||||
|
||||
# Check with the following command
|
||||
grep 5C4B /etc/pve/qemu-server/103.conf
|
||||
|
||||
# After that reboot the VM and verify with lsblk command
|
||||
lsblk
|
||||
```
|
||||
|
||||
## Setup Master and worker Nodes for K3s
|
||||
|
||||
The cluster configuration consists of 4 VMs configured as 2 master and 2 worker k3s nodes.
|
||||
The master nodes also function as worker nodes.
|
||||
|
||||
```bash
|
||||
# On the first master run the following command
|
||||
curl -sfL https://get.k3s.io | sh -s - server --cluster-init --disable servicelb
|
||||
|
||||
# This will generate a token under /var/lib/rancher/k3s/server/node-token
|
||||
# Which will be required to for adding nodes to the cluster
|
||||
|
||||
# On the second master run the following command
|
||||
export TOKEN=<token>
|
||||
export MASTER1_IP=<ip>
|
||||
curl -sfL https://get.k3s.io | \
|
||||
sh -s - server --server https://${MASTER1_IP}:6443 \
|
||||
--token ${TOKEN} --disable servicelb
|
||||
|
||||
# Similarly on the worker nodes run the following command
|
||||
export TOKEN=<token>
|
||||
export MASTER1_IP=<ip>
|
||||
curl -sfL https://get.k3s.io | \
|
||||
K3S_URL=https://${MASTER1_IP}:6443 K3S_TOKEN=${TOKEN} sh -
|
||||
```
|
||||
|
||||
## Configure Metallb load balancer for k3s
|
||||
|
||||
The metallb loadbalancer is used for services instead of k3s default
|
||||
servicelb as it offers advanced features and supports IP address pools for load
|
||||
balancer configuration.
|
||||
|
||||
```bash
|
||||
# On any of the master nodes run the following command
|
||||
kubectl apply -f \
|
||||
https://raw.githubusercontent.com/metallb/metallb/v0.13.7/config/manifests/metallb-native.yaml
|
||||
|
||||
# Ensure that metallb is installed with the following command
|
||||
kubectl get pods -n metallb-system
|
||||
|
||||
# On the host machine apply the metallb cofigmap under metallb directory
|
||||
kubectl apply -f /home/taqi/homeserver/k3s-infra/metallb/metallbConfig.yaml
|
||||
|
||||
# Test that the loadbalancer is working with nginx deployment
|
||||
kubectl create namespace nginx
|
||||
kubectl create deployment nginx --image=nginx -n nginx
|
||||
kubectl expose deployment nginx --port=80 --type=LoadBalancer -n nginx
|
||||
|
||||
# If nginx service gets an external IP and it is accessible from browser then
|
||||
the configuration is complete
|
||||
kubectl delete namespace nginx
|
||||
```
|
||||
|
||||
## Cloud Image for VMs
|
||||
|
||||
A cloud image is a pre-configured disk image that is optimized for use in cloud
|
||||
environments. These images typically include a minimal set of software and
|
||||
configurations that are necessary to run in a virtualized environment,
|
||||
such as a cloud or a hypervisor like Proxmox. Cloud images are designed to be
|
||||
lightweight and can be quickly deployed to create new virtual machines. They
|
||||
often come with cloud-init support, which allows for easy customization and
|
||||
initialization of instances at boot time.
|
||||
|
||||
The cloud iamges are used for setting up the VMs in Proxmox. No traditional
|
||||
template is used for setting up the VMs. The cloud images are available for
|
||||
download from the following link:
|
||||
[Ubuntu 22.04](https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img)
|
||||
1
ansible/.gitignore
vendored
Normal file
1
ansible/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
secrets/
|
||||
86
ansible/README.md
Normal file
86
ansible/README.md
Normal file
@ -0,0 +1,86 @@
|
||||
# Ansible Playbook for Proxmox VM Management
|
||||
|
||||
This Ansible playbook automates the creation, deletion, and configuration of
|
||||
virtual machines (VMs) on a Proxmox server.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Ansible installed on the local machine
|
||||
- Ansible community.general.proxmox_kvm module
|
||||
- Access to a Proxmox server with API access enabled
|
||||
- Python `proxmoxer` library installed (`pip install proxmoxer`)
|
||||
|
||||
## Setup
|
||||
|
||||
1. Clone this repository:
|
||||
```sh
|
||||
git clone https://github.com/TheTaqiTahmid/proxmox_ansible_automation
|
||||
```
|
||||
|
||||
2. Update the `inventory` file with your Proxmox server details:
|
||||
```yaml
|
||||
all:
|
||||
hosts:
|
||||
proxmox:
|
||||
ansible_host: your_proxmox_ip
|
||||
ansible_user: your_proxmox_user
|
||||
ansible_password: your_proxmox_password
|
||||
```
|
||||
In the current example implementation in `inventories/hosts.yaml`, there are
|
||||
multiple groups depending on the types of hosts.
|
||||
|
||||
3. Add group-related variables to the group file under the `group_vars` directory
|
||||
and individual host-related variables to the files under the `host_vars`
|
||||
directory. Ansible will automatically pick up these variables.
|
||||
|
||||
## Playbooks
|
||||
|
||||
### Create VM
|
||||
|
||||
To create the VMs, run the following command:
|
||||
```sh
|
||||
ansible-playbook playbooks/create-vms.yaml
|
||||
```
|
||||
The playbook can be run against specific Proxmox instance using:
|
||||
```sh
|
||||
ansible-playbook playbooks/create-vms.yaml --limit proxmox1
|
||||
```
|
||||
|
||||
### Delete VM
|
||||
|
||||
To delete existing VMs, run the following command:
|
||||
```sh
|
||||
ansible-playbook playbooks/destroy-vms.yaml
|
||||
```
|
||||
|
||||
Similarly the destory playbook can be run against specific Proxmox instance using:
|
||||
```sh
|
||||
ansible-playbook playbooks/destroy-vms.yaml --limit proxmox1
|
||||
```
|
||||
|
||||
### Configure VM
|
||||
|
||||
To configure an existing VM, run the following command:
|
||||
```sh
|
||||
ansible-playbook playbooks/configure-vms.yaml
|
||||
```
|
||||
|
||||
The configuration can be limited to individual VMs using limits:
|
||||
```sh
|
||||
ansible-playbook playbooks/configure-vms.yaml --limit vm6
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
The playbooks use the following variables, which can be customized in the
|
||||
`group_vars/proxmox.yml` file:
|
||||
|
||||
- `vm_id`: The ID of the VM
|
||||
- `vm_name`: The name of the VM
|
||||
- `vm_memory`: The amount of memory for the VM
|
||||
- `vm_cores`: The number of CPU cores for the VM
|
||||
- `vm_disk_size`: The size of the VM disk
|
||||
|
||||
## Author
|
||||
|
||||
- Taqi Tahmid (mdtaqitahmid@gmail.com)
|
||||
5
ansible/ansible.cfg
Normal file
5
ansible/ansible.cfg
Normal file
@ -0,0 +1,5 @@
|
||||
[defaults]
|
||||
inventory = ./inventory/hosts.yaml
|
||||
roles_path = ./roles
|
||||
host_key_checking = False
|
||||
vault_password_file = ~/.ansible_vault_pass
|
||||
11
ansible/inventory/group_vars/all.yaml
Normal file
11
ansible/inventory/group_vars/all.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
# Proxmox access related variables
|
||||
proxmox_api_url: "192.168.1.121"
|
||||
|
||||
# Cloud-init image related variables
|
||||
image_url: "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"
|
||||
image_dest: "/tmp/cloud-image.img"
|
||||
image_format: "qcow2"
|
||||
storage_name: "local"
|
||||
|
||||
# ansible venv
|
||||
ansible_venv: "/home/taqi/.venv/ansible/bin/python"
|
||||
4
ansible/inventory/group_vars/vms.yaml
Normal file
4
ansible/inventory/group_vars/vms.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
apt_packages:
|
||||
- curl
|
||||
- vim
|
||||
- htop
|
||||
29
ansible/inventory/host_vars/proxmox1.yaml
Normal file
29
ansible/inventory/host_vars/proxmox1.yaml
Normal file
@ -0,0 +1,29 @@
|
||||
# VM related variables
|
||||
vm_list:
|
||||
- id: 106
|
||||
name: "vm6"
|
||||
memory: 4096
|
||||
cores: 2
|
||||
disk_size: 30G
|
||||
ip: "192.168.1.151/24"
|
||||
gateway: "192.168.1.1"
|
||||
nameserver1: "1.1.1.1"
|
||||
nameserver2: "8.8.8.8"
|
||||
- id: 107
|
||||
name: "vm7"
|
||||
memory: 4096
|
||||
cores: 2
|
||||
disk_size: 30G
|
||||
ip: "192.168.1.152/24"
|
||||
gateway: "192.168.1.1"
|
||||
nameserver1: "1.1.1.1"
|
||||
nameserver2: "8.8.8.8"
|
||||
|
||||
# cloud-init variables
|
||||
node: "homeserver1"
|
||||
net0: "virtio,bridge=vmbr0"
|
||||
# disk_name: "local:1000/vm-1000-disk-0.raw,discard=on"
|
||||
disk_path: "/var/lib/vz/images/1000"
|
||||
ide2: "local:cloudinit,format=qcow2"
|
||||
boot_order: "order=scsi0"
|
||||
scsi_hw: "virtio-scsi-pci"
|
||||
30
ansible/inventory/host_vars/proxmox2.yaml
Normal file
30
ansible/inventory/host_vars/proxmox2.yaml
Normal file
@ -0,0 +1,30 @@
|
||||
# VM related variables
|
||||
vm_list:
|
||||
- id: 206
|
||||
name: "vm8"
|
||||
memory: 4096
|
||||
cores: 2
|
||||
disk_size: 30G
|
||||
ip: "192.168.1.161/24"
|
||||
gateway: "192.168.1.1"
|
||||
nameserver1: "1.1.1.1"
|
||||
nameserver2: "8.8.8.8"
|
||||
- id: 207
|
||||
name: "vm9"
|
||||
memory: 4096
|
||||
cores: 2
|
||||
disk_size: 30G
|
||||
ip: "192.168.1.162/24"
|
||||
gateway: "192.168.1.1"
|
||||
nameserver1: "1.1.1.1"
|
||||
nameserver2: "8.8.8.8"
|
||||
|
||||
|
||||
# cloud-init template variables
|
||||
node: "homeserver2"
|
||||
net0: "virtio,bridge=vmbr0"
|
||||
# disk_name: "local:2000/vm-2000-disk-0.raw,discard=on"
|
||||
disk_path: "/var/lib/vz/images/2000"
|
||||
ide2: "local:cloudinit,format=qcow2"
|
||||
boot_order: "order=scsi0"
|
||||
scsi_hw: "virtio-scsi-pci"
|
||||
51
ansible/inventory/hosts.yaml
Normal file
51
ansible/inventory/hosts.yaml
Normal file
@ -0,0 +1,51 @@
|
||||
all:
|
||||
children:
|
||||
hypervisors:
|
||||
vms:
|
||||
|
||||
hypervisors:
|
||||
children:
|
||||
server1:
|
||||
server2:
|
||||
|
||||
server1:
|
||||
hosts:
|
||||
proxmox1:
|
||||
ansible_host: 192.168.1.121
|
||||
ansible_user: "{{ ansible_proxmox_user }}"
|
||||
ansible_ssh_private_key_file: "{{ ansible_ssh_private_key_file }}"
|
||||
|
||||
server2:
|
||||
hosts:
|
||||
proxmox2:
|
||||
ansible_host: 192.168.1.122
|
||||
ansible_user: "{{ ansible_proxmox_user }}"
|
||||
ansible_ssh_private_key_file: "{{ ansible_ssh_private_key_file }}"
|
||||
|
||||
vms:
|
||||
children:
|
||||
vm_group_1:
|
||||
vm_group_2:
|
||||
|
||||
vm_group_1:
|
||||
hosts:
|
||||
vm6:
|
||||
ansible_host: 192.168.1.151
|
||||
ansible_user: "{{ ansible_vm_user }}"
|
||||
ansible_ssh_private_key_file: "{{ ansible_ssh_private_key_file }}"
|
||||
vm7:
|
||||
ansible_host: 192.168.1.152
|
||||
ansible_user: "{{ ansible_vm_user }}"
|
||||
ansible_ssh_private_key_file: "{{ ansible_ssh_private_key_file }}"
|
||||
|
||||
vm_group_2:
|
||||
hosts:
|
||||
vm8:
|
||||
ansible_host: 192.168.1.161
|
||||
ansible_user: "{{ ansible_vm_user }}"
|
||||
ansible_ssh_private_key_file: "{{ ansible_ssh_private_key_file }}"
|
||||
vm9:
|
||||
ansible_host: 192.168.1.162
|
||||
ansible_user: "{{ ansible_vm_user }}"
|
||||
ansible_ssh_private_key_file: "{{ ansible_ssh_private_key_file }}"
|
||||
|
||||
6
ansible/playbooks/configure-vms.yaml
Normal file
6
ansible/playbooks/configure-vms.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
- name: Create Proxmox VMs
|
||||
hosts: vms
|
||||
vars_files:
|
||||
- ../secrets/vault.yaml # Load the encrypted vault file
|
||||
roles:
|
||||
- configure-vms
|
||||
6
ansible/playbooks/create-vms.yaml
Normal file
6
ansible/playbooks/create-vms.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
- name: Create Proxmox VMs
|
||||
hosts: hypervisors
|
||||
vars_files:
|
||||
- ../secrets/vault.yaml # Load the encrypted vault file
|
||||
roles:
|
||||
- create-vms
|
||||
6
ansible/playbooks/destroy-vms.yaml
Normal file
6
ansible/playbooks/destroy-vms.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
- name: Destroy Proxmox VMs
|
||||
hosts: hypervisors
|
||||
vars_files:
|
||||
- ../secrets/vault.yaml # Load the encrypted vault file
|
||||
roles:
|
||||
- destroy-vms
|
||||
11
ansible/roles/configure-vms/tasks/main.yaml
Normal file
11
ansible/roles/configure-vms/tasks/main.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
- name: Update apt cache
|
||||
ansible.builtin.apt:
|
||||
update_cache: yes
|
||||
become: true
|
||||
|
||||
- name: Install necessary packages
|
||||
ansible.builtin.apt:
|
||||
name: "{{ apt_packages }}"
|
||||
state: present
|
||||
become: true
|
||||
70
ansible/roles/create-vms/tasks/main.yaml
Normal file
70
ansible/roles/create-vms/tasks/main.yaml
Normal file
@ -0,0 +1,70 @@
|
||||
---
|
||||
- name: Download cloud image
|
||||
get_url:
|
||||
url: "{{ image_url }}"
|
||||
dest: "{{ image_dest }}"
|
||||
use_netrc: yes
|
||||
|
||||
- name: create VMs
|
||||
delegate_to: localhost
|
||||
vars:
|
||||
ansible_python_interpreter: /home/taqi/.venv/ansible/bin/python
|
||||
community.general.proxmox_kvm:
|
||||
api_host: "{{ proxmox_api_url }}"
|
||||
api_user: "{{ proxmox_user }}"
|
||||
api_token_id: "{{ proxmox_api_token_id }}"
|
||||
api_token_secret: "{{ proxmox_api_token }}"
|
||||
node: "{{ node }}"
|
||||
vmid: "{{ item.id }}"
|
||||
name: "{{ item.name }}"
|
||||
memory: "{{ item.memory }}"
|
||||
cores: "{{ item.cores }}"
|
||||
scsihw: "{{ scsi_hw }}"
|
||||
boot: "{{ boot_order }}"
|
||||
net:
|
||||
net0: "{{ net0 }}"
|
||||
ipconfig:
|
||||
ipconfig0: "ip={{ item.ip }},gw={{ item.gateway }}"
|
||||
ide:
|
||||
ide2: "{{ ide2 }}"
|
||||
nameservers: "{{ item.nameserver1 }},{{ item.nameserver2 }}"
|
||||
ciuser: "{{ ciuser }}"
|
||||
cipassword: "{{ cipassword }}"
|
||||
sshkeys: "{{ lookup('file', '/home/taqi/.ssh/homeserver.pub') }}"
|
||||
loop: "{{ vm_list }}"
|
||||
|
||||
- name: Import disk image
|
||||
ansible.builtin.shell: |
|
||||
qm importdisk "{{ item.id }}" "{{ image_dest }}" "{{ storage_name }}" --format "{{ image_format }}"
|
||||
loop: "{{ vm_list }}"
|
||||
|
||||
- name: Attach disk to VM
|
||||
ansible.builtin.shell: |
|
||||
qm set "{{ item.id }}" --scsi0 "{{ storage_name }}:{{ item.id }}/vm-{{ item.id }}-disk-0.{{ image_format }},discard=on"
|
||||
loop: "{{ vm_list }}"
|
||||
|
||||
- name: Resize disk
|
||||
ansible.builtin.shell: |
|
||||
qm resize {{ item.id }} scsi0 {{ item.disk_size }}
|
||||
loop: "{{ vm_list }}"
|
||||
|
||||
- name: Start VMs
|
||||
delegate_to: localhost
|
||||
vars:
|
||||
ansible_python_interpreter: /home/taqi/.venv/ansible/bin/python
|
||||
community.general.proxmox_kvm:
|
||||
api_host: "{{ proxmox_api_url }}"
|
||||
api_user: "{{ proxmox_user }}"
|
||||
api_token_id: "{{ proxmox_api_token_id }}"
|
||||
api_token_secret: "{{ proxmox_api_token }}"
|
||||
node: "{{ node }}"
|
||||
name: "{{ item.name }}"
|
||||
state: started
|
||||
loop: "{{ vm_list }}"
|
||||
tags:
|
||||
- start_vms
|
||||
|
||||
- name: Clean up downloaded image
|
||||
file:
|
||||
path: "{{ image_dest }}"
|
||||
state: absent
|
||||
59
ansible/roles/destroy-vms/tasks/main.yaml
Normal file
59
ansible/roles/destroy-vms/tasks/main.yaml
Normal file
@ -0,0 +1,59 @@
|
||||
---
|
||||
- name: Get VM current state
|
||||
delegate_to: localhost
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ ansible_venv }}"
|
||||
community.general.proxmox_kvm:
|
||||
api_host: "{{ proxmox_api_url }}"
|
||||
api_user: "{{ proxmox_user }}"
|
||||
api_token_id: "{{ proxmox_api_token_id }}"
|
||||
api_token_secret: "{{ proxmox_api_token }}"
|
||||
name: "{{ item.name }}"
|
||||
node: "{{ node }}"
|
||||
state: current
|
||||
register: vm_state
|
||||
ignore_errors: yes
|
||||
loop: "{{ vm_list }}"
|
||||
tags:
|
||||
- vm_delete
|
||||
|
||||
- name: Debug VM state
|
||||
debug:
|
||||
msg: "VM state: {{ vm_state.results[0].status }}"
|
||||
when: vm_state is succeeded
|
||||
loop: "{{ vm_list }}"
|
||||
|
||||
- name: Stop VM
|
||||
delegate_to: localhost
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ ansible_venv }}"
|
||||
community.general.proxmox_kvm:
|
||||
api_host: "{{ proxmox_api_url }}"
|
||||
api_user: "{{ proxmox_user }}"
|
||||
api_token_id: "{{ proxmox_api_token_id }}"
|
||||
api_token_secret: "{{ proxmox_api_token }}"
|
||||
name: "{{ item.name }}"
|
||||
node: "{{ node }}"
|
||||
state: stopped
|
||||
force: true
|
||||
when: vm_state is succeeded and vm_state.results[0].status != 'absent'
|
||||
loop: "{{ vm_list }}"
|
||||
tags:
|
||||
- vm_delete
|
||||
|
||||
- name: Delete VM
|
||||
delegate_to: localhost
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ ansible_venv }}"
|
||||
community.general.proxmox_kvm:
|
||||
api_host: "{{ proxmox_api_url }}"
|
||||
api_user: "{{ proxmox_user }}"
|
||||
api_token_id: "{{ proxmox_api_token_id }}"
|
||||
api_token_secret: "{{ proxmox_api_token }}"
|
||||
name: "{{ item.name }}"
|
||||
node: "{{ node }}"
|
||||
state: absent
|
||||
when: vm_state is succeeded and vm_state.results[0].status != 'absent'
|
||||
loop: "{{ vm_list }}"
|
||||
tags:
|
||||
- vm_delete
|
||||
1
docker_compose/.gitignore
vendored
Normal file
1
docker_compose/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.env
|
||||
246
docker_compose/README.md
Normal file
246
docker_compose/README.md
Normal file
@ -0,0 +1,246 @@
|
||||
Homeserver Notes
|
||||
================
|
||||
|
||||
# Future Plan
|
||||
|
||||
- Add authentication frontend like Authentik which will handle the authentication
|
||||
- Add Nextcloud
|
||||
- Add Gitea
|
||||
|
||||
# List of Service Running on Homeserver
|
||||
|
||||
- Adguard
|
||||
- Plex
|
||||
- Sonarr
|
||||
- Radarr
|
||||
- qbittorrent
|
||||
- Portainer
|
||||
- Jackett
|
||||
- Jellyfin
|
||||
- Wireguard
|
||||
|
||||
# List of Basic CLI tools installed on server
|
||||
|
||||
- ca-certificates
|
||||
- curl
|
||||
- gnupg
|
||||
- lsb-release
|
||||
- ntp
|
||||
- ncdu
|
||||
- net-tools
|
||||
- apache2-utils
|
||||
- apt-transport-https
|
||||
- htop
|
||||
|
||||
|
||||
# Firewall Rules (Currently Disabled)
|
||||
|
||||
I am using ufw to set different firewall rules. As I go I will update the rules
|
||||
|
||||
```
|
||||
sudo ufw default allow outgoing
|
||||
sudo ufw default allow incoming
|
||||
sudo ufw allow from 192.168.1.0/24
|
||||
sudo ufw allow 443
|
||||
sudo ufw allow 80
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
# Hardware Transcoding for Jellyfin
|
||||
|
||||
The media stream applications such as Jellyfin and Plex uses transcoding to
|
||||
convert video format which might be necessary if the end user device does not
|
||||
support some video formats or resolution. If hardware transcoding is not enabled
|
||||
Plex/Jellyfin uses software based transcoding which is resource intensive.
|
||||
|
||||
Most of the new CPU/GPU support HW based transcoding. For our Ryzen 5 2500U
|
||||
processor, we have HW transcoding. Here is the process to enable it:
|
||||
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get install vainfo mesa-va-drivers libva2 libva-utils
|
||||
|
||||
# Run the following command to make sure VA API is working properly
|
||||
vainfo
|
||||
|
||||
# Add the following to services to the Jellyfin container compose file
|
||||
devices:
|
||||
- /dev/dri/renderD128:/dev/dri/renderD128 # VA-API device for hardware acceleration
|
||||
group_add:
|
||||
- video # Add the container to the 'video' group
|
||||
|
||||
```
|
||||
|
||||
|
||||
# Traefik Reverse proxy
|
||||
|
||||
- Traefik is modern HTTP reverse proxy and load balancerthat can be used to route
|
||||
traffic to different internal containers or ports based on subdomain name.
|
||||
|
||||
- In addition to that It can also automatically handle SSL certificate genertion
|
||||
and renewal for HTTPS automatically handle SSL certificate genertion
|
||||
and renewal for HTTPS.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
In order to get wildcard certificates from LetsEncrypt, I will be using DNS challange
|
||||
method. DNS challange method is one of the methods provided by LetsEncrypt to verify
|
||||
the ownership of the domain by adding specific DNS records.
|
||||
|
||||
To do that with cloudflare, I have created a new API token with name _CF_DNS_API_TOKEN_
|
||||
and saved it as docker secret under ~/docker/secrets directory
|
||||
```
|
||||
# To save the appdata for traefik3, created the following folders
|
||||
mkdir -p ~/docker/appdata/traefik3/acme
|
||||
mkdir -p ~/docker/appdata/traefik3/rules/udms
|
||||
|
||||
# To save teh LetsEncrypt certificate, created the following file
|
||||
touch acme.json
|
||||
chmod 600 acme.json # without 600, Traefik will not start
|
||||
|
||||
# To save logs, created following files
|
||||
touch traefik.log
|
||||
touch access.log
|
||||
```
|
||||
After creating the Docker Compose file, add these TLS options like this:
|
||||
```
|
||||
# Under DOCKERDIR/appdata/traefik3/rules/udms/tls-opts.yml
|
||||
tls:
|
||||
options:
|
||||
tls-opts:
|
||||
minVersion: VersionTLS12
|
||||
cipherSuites:
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
||||
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
|
||||
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
|
||||
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
|
||||
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
|
||||
- TLS_AES_128_GCM_SHA256
|
||||
- TLS_AES_256_GCM_SHA384
|
||||
- TLS_CHACHA20_POLY1305_SHA256
|
||||
- TLS_FALLBACK_SCSV # Client is doing version fallback. See RFC 7507
|
||||
curvePreferences:
|
||||
- CurveP521
|
||||
- CurveP384
|
||||
sniStrict: true
|
||||
```
|
||||
Add the middleware Basic Auth:
|
||||
```
|
||||
# Under DOCKERDIR/appdata/traefik3/rules/udms/middlewares-basic-auth.yml
|
||||
http:
|
||||
middlewares:
|
||||
middlewares-basic-auth:
|
||||
basicAuth:
|
||||
# users:
|
||||
# - "user:password"
|
||||
usersFile: "/run/secrets/basic_auth_credentials"
|
||||
realm: "Traefik 3 Basic Auth"
|
||||
|
||||
```
|
||||
Add middleware rate limited to prevent DDoS attack
|
||||
```
|
||||
# Under DOCKERDIR/appdata/traefik3/rules/udms/middlewares-rate-limit.yaml
|
||||
http:
|
||||
middlewares:
|
||||
middlewares-rate-limit:
|
||||
rateLimit:
|
||||
average: 100
|
||||
burst: 50
|
||||
```
|
||||
Add secure headers middleware
|
||||
```
|
||||
# Under DOCKERDIR/appdata/traefik3/rules/udms/middlewares-secure-headers.yaml
|
||||
http:
|
||||
middlewares:
|
||||
middlewares-secure-headers:
|
||||
headers:
|
||||
accessControlAllowMethods:
|
||||
- GET
|
||||
- OPTIONS
|
||||
- PUT
|
||||
accessControlMaxAge: 100
|
||||
hostsProxyHeaders:
|
||||
- "X-Forwarded-Host"
|
||||
stsSeconds: 63072000
|
||||
stsIncludeSubdomains: true
|
||||
stsPreload: true
|
||||
# forceSTSHeader: true # This is a good thing but it can be tricky. Enable after everything works.
|
||||
customFrameOptionsValue: SAMEORIGIN # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
|
||||
contentTypeNosniff: true
|
||||
browserXssFilter: true
|
||||
referrerPolicy: "same-origin"
|
||||
permissionsPolicy: "camera=(), microphone=(), geolocation=(), payment=(), usb=(), vr=()"
|
||||
customResponseHeaders:
|
||||
X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex," # disable search engines from indexing home server
|
||||
server: "" # hide server info from visitors
|
||||
```
|
||||
|
||||
## Networking
|
||||
|
||||
Create a default Bridge network for the Traefik
|
||||
|
||||
# Wireguard VPN setup
|
||||
|
||||
In order for qbittorrent container to use the wireguard VPN tunnel
|
||||
wireguard container has been added to the qbittorrent docker compose
|
||||
file.
|
||||
- qbittorrent container depends on the wireguard container. If
|
||||
wireguard container is down, qbittorrent network will not work.
|
||||
|
||||
- Since, qbittorrent is using the wireguard container, port 9500
|
||||
has been forwared to the host 9500 port from the wireguard container
|
||||
|
||||
- qbittorrent is using wireguard network interface. So, to access
|
||||
the qbittorrent GUI, iptables rules had to be setup. Also, when the pc restarts
|
||||
the wireguard container IP might change.
|
||||
|
||||
```
|
||||
# Forward traffic coming to port 9500 on the host to port 9500 on the WireGuard container
|
||||
sudo iptables -t nat -A PREROUTING -p tcp --dport 9500 -j DNAT --to-destination 172.18.0.6:9500
|
||||
|
||||
# Forward traffic from the WireGuard container back to the host's port 9500
|
||||
sudo iptables -t nat -A POSTROUTING -p tcp -d 172.18.0.6 --dport 9500 -j MASQUERADE
|
||||
```
|
||||
- We can check the host ip geolocation by the following command. In that way
|
||||
we can verify VPN is working.
|
||||
```
|
||||
docker exec -it qbittorrent curl ipinfo.io
|
||||
|
||||
{
|
||||
"ip": "1.2.3.4",
|
||||
"hostname": "1.2.3.4.in-addr.arpa",
|
||||
"city": "Amsterdam",
|
||||
"region": "North Holland",
|
||||
"country": "NL",
|
||||
"loc": "55.3740,41.8897",
|
||||
"org": "Some Company",
|
||||
"postal": "1234",
|
||||
"timezone": "Europe/Amsterdam",
|
||||
"readme": "https://ipinfo.io/missingauth"
|
||||
}
|
||||
```
|
||||
- We can check the wireguard VPN connection status with the following command
|
||||
```
|
||||
docker exec -it wireguard wg
|
||||
|
||||
interface: wg0
|
||||
public key: <public key>
|
||||
private key: (hidden)
|
||||
listening port: 56791
|
||||
fwmark: 0xca6c
|
||||
|
||||
peer: <public key>
|
||||
preshared key: (hidden)
|
||||
endpoint: <ip>:51820
|
||||
allowed ips: 0.0.0.0/0, ::/0
|
||||
latest handshake: 1 minute, 47 seconds ago
|
||||
transfer: 12.69 MiB received, 822.64 KiB sent
|
||||
persistent keepalive: every 15 seconds
|
||||
```
|
||||
|
||||
# FAQ
|
||||
|
||||
1. How to get the plex claim?
|
||||
-> Go the the url and login: https://www.plex.tv/claim/
|
||||
18
docker_compose/jackett.yaml
Normal file
18
docker_compose/jackett.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
version: "3"
|
||||
services:
|
||||
jackett:
|
||||
image: "linuxserver/jackett"
|
||||
container_name: "jackett"
|
||||
env_file:
|
||||
./.env
|
||||
volumes:
|
||||
- ${DOCKERDIR}/appdata/jackett:/config
|
||||
- ${DATADIR}/downloads:/downloads
|
||||
- "/etc/localtime:/etc/localtime:ro"
|
||||
ports:
|
||||
- "9117:9117"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PUID=${PUID}
|
||||
- PGID=${PGID}
|
||||
- TZ=${TZ}
|
||||
38
docker_compose/jellyfin.yaml
Normal file
38
docker_compose/jellyfin.yaml
Normal file
@ -0,0 +1,38 @@
|
||||
services:
|
||||
jellyfin:
|
||||
image: jellyfin/jellyfin
|
||||
container_name: jellyfin
|
||||
env_file:
|
||||
- ./.env
|
||||
volumes:
|
||||
- ${DOCKERDIR}/appdata/jellyfin:/config
|
||||
- ${DATADIR}/downloads:/downloads
|
||||
- type: bind
|
||||
source: ${DATADIR}
|
||||
target: /media
|
||||
read_only: true
|
||||
ports:
|
||||
- "8096:8096" # Optional, if you rely on Traefik for routing, this can be removed.
|
||||
restart: 'unless-stopped'
|
||||
devices:
|
||||
- /dev/dri/renderD128:/dev/dri/renderD128 # VA-API device for hardware acceleration
|
||||
group_add:
|
||||
- video # Add the container to the video group necessary for accesing /dev/dri
|
||||
environment:
|
||||
- PUID=${PUID}
|
||||
- PGID=${PGID}
|
||||
- TZ=${TZ}
|
||||
- UMASK_SET=002
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.jellyfin-rtr.rule=Host(`jellyfin.${DOMAINNAME}`)"
|
||||
- "traefik.http.routers.jellyfin-rtr.entrypoints=websecure"
|
||||
- "traefik.http.routers.jellyfin-rtr.service=jellyfin-svc"
|
||||
- "traefik.http.services.jellyfin-svc.loadbalancer.server.port=8096"
|
||||
- "traefik.http.routers.traefik-rtr.middlewares=middlewares-rate-limit@file,middlewares-secure-headers@file"
|
||||
networks:
|
||||
- t3_proxy
|
||||
|
||||
networks:
|
||||
t3_proxy:
|
||||
external: true
|
||||
40
docker_compose/plex.yaml
Normal file
40
docker_compose/plex.yaml
Normal file
@ -0,0 +1,40 @@
|
||||
version: '3.5'
|
||||
services:
|
||||
plex:
|
||||
image: plexinc/pms-docker
|
||||
container_name: plex
|
||||
env_file:
|
||||
./.env
|
||||
environment:
|
||||
- PLEX_UID=${PUID}
|
||||
- PLEX_GID=${PGID}
|
||||
- TZ=${TZ}
|
||||
- VERSION=docker
|
||||
- PLEX_CLAIM=${PLEX_CLAIM}
|
||||
ports:
|
||||
- "32400:32400/tcp"
|
||||
- "3005:3005/tcp"
|
||||
- "8324:8324/tcp"
|
||||
- "32469:32469/tcp"
|
||||
- "1899:1900/udp"
|
||||
- "32410:32410/udp"
|
||||
- "32412:32412/udp"
|
||||
- "32413:32413/udp"
|
||||
- "32414:32414/udp"
|
||||
volumes:
|
||||
- ${DOCKERDIR}/appdata/plex:/config
|
||||
- ${DATADIR}/tvshows:/tv
|
||||
- ${DATADIR}/movies:/movies
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.plex-rtr.rule=Host(`plex.${DOMAINNAME}`)"
|
||||
- "traefik.http.routers.plex-rtr.entrypoints=websecure"
|
||||
- "traefik.http.routers.plex-rtr.service=plex-svc"
|
||||
- "traefik.http.services.plex-svc.loadbalancer.server.port=32400"
|
||||
- "traefik.http.routers.traefik-rtr.middlewares=middlewares-rate-limit@file,middlewares-secure-headers@file"
|
||||
networks:
|
||||
- t3_proxy
|
||||
networks:
|
||||
t3_proxy:
|
||||
external: true
|
||||
35
docker_compose/portainer.yaml
Normal file
35
docker_compose/portainer.yaml
Normal file
@ -0,0 +1,35 @@
|
||||
version: "3"
|
||||
services:
|
||||
portainer:
|
||||
image: portainer/portainer-ce:latest
|
||||
ports:
|
||||
- 9000:9000
|
||||
volumes:
|
||||
- /home/taqi/docker/portainer/data:/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ./.env
|
||||
networks:
|
||||
- t3_proxy
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# HTTP Routers
|
||||
- "traefik.http.routers.portainer-rtr.entrypoints=websecure"
|
||||
- "traefik.http.routers.portainer-rtr.rule=Host(`portainer.${DOMAINNAME}`)"
|
||||
# HTTP Services
|
||||
- "traefik.http.routers.portainer-rtr.tls=true"
|
||||
- "traefik.http.routers.portainer-rtr.service=portainer-svc"
|
||||
- "traefik.http.services.portainer-svc.loadbalancer.server.port=9000"
|
||||
- "traefik.http.routers.traefik-rtr.middlewares=middlewares-rate-limit@file,middlewares-secure-headers@file"
|
||||
command:
|
||||
--http-enabled
|
||||
environment:
|
||||
- TZ=${TZ}
|
||||
- DOMAINNAME=${DOMAINNAME}
|
||||
volumes:
|
||||
data:
|
||||
|
||||
networks:
|
||||
t3_proxy:
|
||||
external: true
|
||||
52
docker_compose/qbittorrent.yaml
Normal file
52
docker_compose/qbittorrent.yaml
Normal file
@ -0,0 +1,52 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
wireguard:
|
||||
image: linuxserver/wireguard:latest
|
||||
container_name: wireguard
|
||||
env_file:
|
||||
./.env
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
environment:
|
||||
- PUID=${PUID}
|
||||
- PGID=${PGID}
|
||||
- TZ=Europe/Helsinki
|
||||
volumes:
|
||||
- ${WIREGUARD_CONFIG}:/config/wg0.conf
|
||||
- /lib/modules:/lib/modules
|
||||
ports:
|
||||
- 51820:51820/udp
|
||||
- 9500:9500 # qbittorrent
|
||||
sysctls:
|
||||
- net.ipv4.conf.all.src_valid_mark=1
|
||||
- net.ipv6.conf.all.disable_ipv6=0
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
dockercompose_default:
|
||||
ipv4_address: 172.18.0.100
|
||||
|
||||
|
||||
qbittorrent:
|
||||
image: "linuxserver/qbittorrent"
|
||||
container_name: "qbittorrent"
|
||||
env_file:
|
||||
./.env
|
||||
volumes:
|
||||
- ${DOCKERDIR}/appdata/qbittorrent:/config
|
||||
- ${DATADIR}/downloads:/downloads
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PUID=${PUID}
|
||||
- PGID=${PGID}
|
||||
- TZ=${TZ}
|
||||
- UMASK_SET=002
|
||||
- WEBUI_PORT=9500
|
||||
network_mode: service:wireguard
|
||||
depends_on:
|
||||
- wireguard
|
||||
|
||||
networks:
|
||||
dockercompose_default:
|
||||
external: true
|
||||
21
docker_compose/radarr.yaml
Normal file
21
docker_compose/radarr.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
version: "3"
|
||||
services:
|
||||
radarr:
|
||||
image: "linuxserver/radarr"
|
||||
container_name: "radarr"
|
||||
env_file:
|
||||
./.env
|
||||
volumes:
|
||||
- ${DOCKERDIR}/appdata/radarr:/config
|
||||
- ${DATADIR}/downloads:/downloads
|
||||
- ${DATADIR}/movies:/movies
|
||||
- "/etc/localtime:/etc/localtime:ro"
|
||||
ports:
|
||||
- "7878:7878"
|
||||
restart: always
|
||||
environment:
|
||||
- PUID=${PUID}
|
||||
- PGID=${PGID}
|
||||
- TZ=${TZ}
|
||||
networks:
|
||||
- bridge
|
||||
19
docker_compose/sonarr.yaml
Normal file
19
docker_compose/sonarr.yaml
Normal file
@ -0,0 +1,19 @@
|
||||
version: "3"
|
||||
services:
|
||||
sonarr:
|
||||
image: "linuxserver/sonarr"
|
||||
container_name: "sonarr"
|
||||
env_file:
|
||||
./.env
|
||||
volumes:
|
||||
- ${DOCKERDIR}/appdata/sonarr:/config
|
||||
- ${DATADIR}/downloads:/downloads
|
||||
- ${DATADIR}/tvshows:/tvshows
|
||||
- "/etc/localtime:/etc/localtime:ro"
|
||||
ports:
|
||||
- "8989:8989"
|
||||
restart: always
|
||||
environment:
|
||||
- PUID=${PUID}
|
||||
- PGID=${PGID}
|
||||
- TZ=${TZ}
|
||||
96
docker_compose/traefikv3.yaml
Normal file
96
docker_compose/traefikv3.yaml
Normal file
@ -0,0 +1,96 @@
|
||||
version: '3.8'
|
||||
|
||||
networks:
|
||||
t3_proxy:
|
||||
name: t3_proxy
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 192.168.90.0/24
|
||||
|
||||
secrets:
|
||||
basic_auth_credentials:
|
||||
file: $DOCKERDIR/secrets/basic_auth_credentials
|
||||
cf_dns_api_token:
|
||||
file: $DOCKERDIR/secrets/cf_dns_api_token
|
||||
|
||||
services:
|
||||
traefik:
|
||||
container_name: traefik
|
||||
image: traefik:3.0
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ./.env
|
||||
networks:
|
||||
t3_proxy:
|
||||
ipv4_address: 192.168.90.254
|
||||
command:
|
||||
- --entrypoints.web.address=:80
|
||||
- --entrypoints.websecure.address=:443
|
||||
- --entrypoints.traefik.address=:8080
|
||||
- --entrypoints.websecure.http.tls=true
|
||||
# The following two options redirects http request at port 80 to https
|
||||
- --entrypoints.web.http.redirections.entrypoint.to=websecure
|
||||
- --entrypoints.web.http.redirections.entrypoint.scheme=https
|
||||
- --entrypoints.web.http.redirections.entrypoint.permanent=true
|
||||
- --api=true
|
||||
- --api.dashboard=true
|
||||
# - --api.insecure=true
|
||||
- --entrypoints.websecure.forwardedHeaders.trustedIPs=$CLOUDFLARE_IPS,$LOCAL_IPS
|
||||
- --log=true
|
||||
- --log.filePath=/logs/traefik.log
|
||||
- --log.level=DEBUG
|
||||
- --accessLog=true
|
||||
- --accessLog.filePath=/logs/access.log
|
||||
- --accessLog.bufferingSize=100
|
||||
- --accessLog.filters.statusCodes=204-299,400-499,500-599
|
||||
- --providers.docker=true
|
||||
- --providers.docker.network=t3_proxy
|
||||
- --entrypoints.websecure.http.tls.options=tls-opts@file
|
||||
- --entrypoints.websecure.http.tls.certresolver=dns-cloudflare
|
||||
- --entrypoints.websecure.http.tls.domains[0].main=$DOMAINNAME
|
||||
- --entrypoints.websecure.http.tls.domains[0].sans=*.$DOMAINNAME
|
||||
- --providers.file.directory=/rules
|
||||
- --providers.file.watch=true
|
||||
- --certificatesResolvers.dns-cloudflare.acme.storage=/acme.json
|
||||
- --certificatesResolvers.dns-cloudflare.acme.dnsChallenge.provider=cloudflare
|
||||
- --certificatesResolvers.dns-cloudflare.acme.dnsChallenge.resolvers=1.1.1.1:53,1.0.0.1:53
|
||||
ports:
|
||||
# - 80:80
|
||||
- 443:443
|
||||
- 8080:8080
|
||||
# - target: 80
|
||||
# published: 80
|
||||
# protocol: tcp
|
||||
# mode: host
|
||||
# - target: 443
|
||||
# published: 443
|
||||
# protocol: tcp
|
||||
# mode: host
|
||||
# - target: 8080
|
||||
# published: 8585
|
||||
# protocol: tcp
|
||||
# mode: host
|
||||
volumes:
|
||||
- $DOCKERDIR/appdata/traefik3/rules/$HOSTNAME:/rules
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- $DOCKERDIR/appdata/traefik3/acme/acme.json:/acme.json
|
||||
- $DOCKERDIR/logs/$HOSTNAME/traefik:/logs
|
||||
environment:
|
||||
- PUID=${PUID}
|
||||
- PGID=${PGID}
|
||||
- TZ=$TZ
|
||||
- CF_DNS_API_TOKEN_FILE=/run/secrets/cf_dns_api_token
|
||||
- HTPASSWD_FILE=/run/secrets/basic_auth_credentials
|
||||
- DOMAINNAME=${DOMAINNAME}
|
||||
secrets:
|
||||
- cf_dns_api_token
|
||||
- basic_auth_credentials
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.dashboard.tls=true"
|
||||
- "traefik.http.routers.traefik-rtr.entrypoints=websecure"
|
||||
- "traefik.http.routers.traefik-rtr.rule=Host(`traefik.${DOMAINNAME}`)"
|
||||
- "traefik.http.routers.traefik-rtr.service=api@internal"
|
||||
# Middlewares
|
||||
- "traefik.http.routers.traefik-rtr.middlewares=middlewares-rate-limit@file,middlewares-secure-headers@file,middlewares-basic-auth@file"
|
||||
Reference in New Issue
Block a user