Compare commits

..

1 Commits

Author SHA1 Message Date
forust 1dec4de708 nya~ 2025-12-09 21:56:13 +01:00
172 changed files with 491 additions and 6459 deletions
-39
View File
@@ -1,39 +0,0 @@
name: Deploy to Server
run-name: Deploying to ${{ runner.os}} server on ${{ gitea.ref }}
on:
push:
branches:
- main
- ci/gitea-actions
jobs:
deploy:
runs-on: prod
steps:
- name: Fetch and Diff Analysis
id: diff
run: |
cd ${{ secrets.PROD_DIR }}
git fetch origin main
CHANGES=$(git diff --name-only HEAD origin/main | cut -d/ -f1 | sort -u | tr '\n' ' ')
echo "dirs=$CHANGES" >> $GITHUB_OUTPUT
echo "Changed dirs: $CHANGES"
- name: Sync Server Files
run: |
cd ${{ secrets.PROD_DIR }}
git reset --hard origin/main
echo "Server files synced with origin/main"
- name: Deploy Services
run: |
cd ${{ secrets.PROD_DIR }}
for dir in ${{ steps.diff.outputs.dirs }}; do
if [ -d "$dir" ] && ([ -f "$dir/compose.yaml" ] || [ -f "$dir/docker-compose.yaml" ]); then
echo ">>> Deploying $dir"
cd "$dir"
DOCKER_BUILDKIT=1 BUILDKIT_PROGRESS=plain docker compose up -d --build --no-color
cd ..
else
echo ">>> Skipping $dir: no compose file found"
fi
done
-41
View File
@@ -1,41 +0,0 @@
## BINARY MODE, USE WITH THE GITHUB RUNNER BINARY INSTALLED ON THE SERVER
name: Deploy to Server
run-name: Deploying onto server on ${{ github.ref }}
on:
push:
branches:
- main
- ci/actions
jobs:
deploy:
runs-on: [prod, self-hosted]
steps:
- name: Fetch and Diff Analysis
id: diff
run: |
cd ${{ secrets.PROD_DIR }}
git fetch origin main
CHANGES=$(git diff --name-only HEAD origin/main | cut -d/ -f1 | sort -u | tr '\n' ' ')
echo "dirs=$CHANGES" >> $GITHUB_OUTPUT
echo "Changed dirs: $CHANGES"
- name: Sync Server Files
run: |
cd ${{ secrets.PROD_DIR }}
git reset --hard origin/main
echo "Server files synced with origin/main"
- name: Deploy Services
run: |
cd ${{ secrets.PROD_DIR }}
for dir in ${{ steps.diff.outputs.dirs }}; do
if [ -d "$dir" ] && ([ -f "$dir/compose.yaml" ] || [ -f "$dir/docker-compose.yaml" ]); then
echo ">>> Deploying $dir"
cd "$dir"
DOCKER_BUILDKIT=1 BUILDKIT_PROGRESS=plain docker compose up -d --build --no-color
cd ..
else
echo ">>> Skipping $dir: no compose file found"
fi
done
+10 -34
View File
@@ -2,15 +2,19 @@
sync.ffs_lock
.sync.ffs_db
# Copyparty
*.hist/
# Environment
.env
.env.anna
.env.forust
.env.*
!.env.*example
# Volumes, configs and data directories
# Volumes and data directories
gitea/gitea-db/
gitea/gitea-data/*
n8n/n8n-data/*
n8n/n8n-node-data/*
adguardhome/conf/*
adguardhome/data/*
dockmon/data/*
portainer/portainer_data/*
metube/MeTube_downloads
@@ -18,10 +22,6 @@ uptime-kuma/data/
termix/termix-data/*
cfddns/config.json
checkmk/checkmk/*
downtify/Downtify_downloads
headscale/config/*
headscale/data/*
searxng/core-config/*
# Steaming services files
streaming/jellyfin/*
@@ -33,21 +33,13 @@ streaming/qbittorrent/*
streaming/prowlarr/*
# Homepage
homepages/forust_files/.well-known/*
homepages/forust_files/assets/images/team/*
# Traefik files
traefik/letsencrypt/acme.json
traefik/dynamic/fileservers.yml
traefik/dynamic/*.local.y*ml.*
traefik/dynamic/*.external.y*ml
traefik/logs/*
# SSL Certificates
adguardhome/certs/*
traefik/certs/*
certs/
# Monitoring
monitoring/prometheus.yml
@@ -82,8 +74,6 @@ replacements.txt
# Git
.gitattributes
# Gitea/github Runners
.runner
# Misc
.DS_Store
@@ -91,17 +81,3 @@ replacements.txt
# Temp files
edu_master/temp/
temp/*
# Environment
.env
.env.anna
.env.forust
.env.*
!*example
# kubernetes
*/k8s/*secret*
!*/k8s/*secret*.example
traefik/k8s/local-tls.yaml
convertx/k8s/config.yaml
+27 -25
View File
@@ -3,46 +3,48 @@ services:
image: adguard/adguardhome:latest
container_name: adguardhome
restart: unless-stopped
environment:
- TZ=${TZ}
ports:
- "53:53/tcp"
- "53:53/udp"
- "853:853/tcp" # DNS over TLS
# - "67:67/udp" # DHCP
# - "68:68/tcp" # DHCP
# - "3000:3000/tcp"
- "3000:3000/tcp"
volumes:
- data:/opt/adguardhome/work
- ./conf:/opt/adguardhome/conf
- ./certs:/certs:ro
- ./data/work:/opt/adguardhome/work
- ./data/conf:/opt/adguardhome/conf
networks:
- traefik-proxy
labels:
- "traefik.enable=true"
- "traefik.http.services.adguard.loadbalancer.server.port=3000"
- "traefik.docker.network=traefik-proxy"
# Prod Router
- "traefik.http.routers.adguard.rule=Host(`dns.forust.xyz`) || Host(`adguard.forust.xyz`)"
- "traefik.http.routers.adguard.rule=Host(`adguard.forust.xyz`)"
- "traefik.http.routers.adguard.entrypoints=websecure"
- "traefik.http.routers.adguard.middlewares=security-headers@file"
- "traefik.http.routers.adguard.service=adguard"
- "traefik.http.routers.adguard.tls=true"
# Local Router
- "traefik.http.routers.adguard-local.rule=Host(`adguard.workstation.internal`) || Host(`dns.workstation.internal`)"
- "traefik.http.routers.adguard-local.entrypoints=websecure"
- "traefik.http.routers.adguard-local.tls=true"
# Dev Router
- "traefik.http.routers.adguard-dev.rule=Host(`adguard.gigaforust.internal`) || Host(`dns.gigaforust.internal`)"
- "traefik.http.routers.adguard-dev.entrypoints=websecure"
- "traefik.http.routers.adguard-dev.tls=true"
# DoH Router
- "traefik.http.routers.dns.rule=(Host(`dns.forust.xyz`) && PathPrefix(`/dns-query`))"
- "traefik.http.routers.dns.entrypoints=websecure"
- "traefik.http.routers.dns.tls.certresolver=letsencrypt"
- "traefik.http.services.adguard.loadbalancer.server.port=3000"
# Local Router
- "traefik.http.routers.adguard-local.rule=Host(`adguard.workstation.internal`) || Host(`adguard.internal`)"
- "traefik.http.routers.adguard-local.entrypoints=websecure"
- "traefik.http.routers.adguard-local.middlewares=security-headers@file"
- "traefik.http.routers.adguard-local.service=adguard"
- "traefik.http.routers.adguard-local.tls=true"
# Dev Router
- "traefik.http.routers.adguard-dev.rule=Host(`adguard.gigaforust.internal`)"
- "traefik.http.routers.adguard-dev.entrypoints=websecure"
- "traefik.http.routers.adguard-dev.middlewares=security-headers@file"
- "traefik.http.routers.adguard-dev.service=adguard"
- "traefik.http.routers.adguard-dev.tls=true"
# Glance Metadata
- glance.name=adguard
- glance.url=https://adguard.forust.xyz/
- glance.description=AdGuard Home is a network-wide software for blocking ads.
networks:
- proxy
volumes:
data:
networks:
proxy:
traefik-proxy:
external: true
-90
View File
@@ -1,90 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: adguard-service
namespace: adguard
spec:
selector:
app: adguard
ports:
- port: 3000
name: webui
targetPort: 3000
- port: 53
name: dns
targetPort: 53
protocol: UDP
- port: 53
name: dns-tcp
targetPort: 53
protocol: TCP
- port: 853
name: dot
targetPort: 853
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: adguard-deployment
namespace: adguard
spec:
replicas: 1
selector:
matchLabels:
app: adguard
template:
metadata:
labels:
app: adguard
spec:
containers:
- name: adguard
image: adguard/adguardhome:latest
resources:
limits:
memory: "1.5Gi"
cpu: "300m"
requests:
memory: "500Mi"
cpu: "50m"
ports:
- containerPort: 3000
name: webui
- containerPort: 53
name: dns
- containerPort: 853
name: dot
volumeMounts:
- name: adguard-data
mountPath: /opt/adguardhome/work
subPath: work
- name: adguard-data
mountPath: /opt/adguardhome/conf
subPath: conf
- name: adguard-certs
mountPath: /certs
readOnly: true
volumes:
- name: adguard-data
persistentVolumeClaim:
claimName: adguard-pvc
- name: adguard-certs
secret:
secretName: adguard-certs
items:
- key: tls.crt
path: fullchain.pem
- key: tls.key
path: privkey.pem
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: adguard-pvc
namespace: adguard
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
-62
View File
@@ -1,62 +0,0 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: adguard-prod
namespace: adguard
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^(adguard|dns)\.forust\.xyz$`)
kind: Rule
services:
- name: adguard-service
port: 3000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: adguard-local
namespace: adguard
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^(adguard|dns)\.(workstation|gigaforust)\.internal$`)
kind: Rule
services:
- name: adguard-service
port: 3000
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: adguard-doh
namespace: adguard
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^(adguard|dns)\.forust\.xyz$`) && PathPrefix(`/dns-query`)
kind: Rule
services:
- name: adguard-service
port: 3000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: adguard-dot
namespace: adguard
spec:
entryPoints:
- dot
routes:
- match: HostSNI(`*`)
services:
- name: adguard-service
port: 853
-4
View File
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: adguard
-10
View File
@@ -1,10 +0,0 @@
kubectl apply -f k8s/namespace.yaml && \
kubectl create secret tls adguard-certs -n adguard \
--cert=certs/fullchain.pem \
--key=certs/privkey.pem --dry-run=client -o yaml > \
k8s/secrets.yaml
# OR WITH NO FILE CREATION:
kubectl create secret tls adguard-certs -n adguard \
--cert=certs/fullchain.pem --key=certs/privkey.pem \
--save-config
+20 -9
View File
@@ -26,9 +26,9 @@ services:
command: server
container_name: authentik-server
restart: unless-stopped
# ports:
# - ${PORT_HTTP:-9000}:9000
# - ${PORT_HTTPS:-9443}:9443
ports:
- ${PORT_HTTP:-9000}:9000
- ${PORT_HTTPS:-9443}:9443
env_file:
- .env
environment:
@@ -37,28 +37,39 @@ services:
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
volumes:
- ./media:/media
- ./custom-templates:/templates
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik-proxy"
# Services
# - "traefik.http.services.authentik-server.loadbalancer.server.port=9443"
- "traefik.http.services.authentik-server.loadbalancer.server.port=9000"
# Prod Router
- "traefik.http.routers.authentik-server.rule=Host(`auth.forust.xyz`)"
- "traefik.http.routers.authentik-server.entrypoints=websecure"
- "traefik.http.routers.authentik-server.middlewares=security-headers@file"
- "traefik.http.routers.authentik-server.service=authentik-server"
- "traefik.http.routers.authentik-server.tls=true"
# Local Router
- "traefik.http.routers.authentik-server-local.rule=Host(`auth.workstation.internal`)"
- "traefik.http.routers.authentik-server-local.rule=Host(`auth.workstation.internal`) || Host(`auth-dashboard.internal`)"
- "traefik.http.routers.authentik-server-local.entrypoints=websecure"
- "traefik.http.routers.authentik-server-local.middlewares=security-headers@file"
- "traefik.http.routers.authentik-server-local.service=authentik-server"
- "traefik.http.routers.authentik-server-local.tls=true"
# Dev Router
- "traefik.http.routers.authentik-server-dev.rule=Host(`auth.gigaforust.internal`)"
- "traefik.http.routers.authentik-server-dev.entrypoints=websecure"
- "traefik.http.routers.authentik-server-dev.middlewares=security-headers@file"
- "traefik.http.routers.authentik-server-dev.service=authentik-server"
- "traefik.http.routers.authentik-server-dev.tls=true"
volumes:
- ./media:/media
- ./custom-templates:/templates
networks:
- proxy
- traefik-proxy
- authentik
depends_on:
postgresql:
@@ -91,5 +102,5 @@ volumes:
driver: local
networks:
authentik:
proxy:
traefik-proxy:
external: true
-93
View File
@@ -1,93 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: authentik-server-service
namespace: authentik
spec:
type: ClusterIP
selector:
app: authentik-server
ports:
- port: 9000
targetPort: 9000
---
apiVersion: v1
kind: Service
metadata:
name: authentik-worker-service
namespace: authentik
spec:
type: ClusterIP
selector:
app: authentik-worker
ports:
- port: 9000
targetPort: 9000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: authentik-server-deployment
namespace: authentik
spec:
replicas: 1
selector:
matchLabels:
app: authentik-server
template:
metadata:
labels:
app: authentik-server
spec:
containers:
- name: authentik-server
image: ghcr.io/goauthentik/server:2025.10.2
args: ["server"]
envFrom:
- configMapRef:
name: authentik-config
- secretRef:
name: authentik-secrets
ports:
- containerPort: 9000
resources:
requests:
memory: "700Mi"
cpu: "300m"
limits:
memory: "1.5Gi"
cpu: "1000m"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: authentik-worker-deployment
namespace: authentik
spec:
replicas: 1
selector:
matchLabels:
app: authentik-worker
template:
metadata:
labels:
app: authentik-worker
spec:
containers:
- name: authentik-worker
image: ghcr.io/goauthentik/server:2025.10.2
args: ["worker"]
securityContext:
runAsUser: 0
envFrom:
- configMapRef:
name: authentik-config
- secretRef:
name: authentik-secrets
resources:
requests:
memory: "512Mi"
cpu: "300m"
limits:
memory: "1Gi"
cpu: "700m"
-11
View File
@@ -1,11 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: authentik-config
namespace: authentik
data:
AUTHENTIK_IMAGE: ghcr.io/goauthentik/server
AUTHENTIK_TAG: "2025.10.2"
AUTHENTIK_POSTGRESQL__HOST: authentik-postgres-service
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_ERROR_REPORTING__ENABLED: "true"
-32
View File
@@ -1,32 +0,0 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: authentik-prod
namespace: authentik
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^auth\.forust\.xyz$`)
kind: Rule
services:
- name: authentik-server-service
port: 9000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: authentik-local
namespace: authentik
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^auth\.(workstation|gigaforust)\.internal$`)
kind: Rule
services:
- name: authentik-server-service
port: 9000
-4
View File
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: authentik
-66
View File
@@ -1,66 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: authentik-postgres-service
namespace: authentik
spec:
clusterIP: None
selector:
app: authentik-postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: authentik-postgres-statefulset
namespace: authentik
spec:
selector:
matchLabels:
app: authentik-postgres
serviceName: authentik-postgres-service
replicas: 1
template:
metadata:
labels:
app: authentik-postgres
spec:
containers:
- name: postgres
image: docker.io/library/postgres:15-alpine
env:
- name: POSTGRES_DB
value: authentik
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: authentik-secrets
key: AUTHENTIK_POSTGRESQL__USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: authentik-secrets
key: AUTHENTIK_POSTGRESQL__PASSWORD
ports:
- containerPort: 5432
name: postgres
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "1Gi"
cpu: "500m"
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
-11
View File
@@ -1,11 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: authentik-secrets
namespace: authentik
type: Opaque
stringData:
AUTHENTIK_SECRET_KEY: ""
AUTHENTIK_POSTGRESQL__PASSWORD: ""
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_BOOTSTRAP_PASSWORD: authentik
-15
View File
@@ -1,15 +0,0 @@
CLOUDFLARE_API_TOKEN=YOUR_CLOUDFLARE_API_TOKEN
DOMAINS=example.com,dns.example.com,mc.example.com,auth.example.com,ssh.example.com
IP4_DOMAINS=
IP6_DOMAINS=
IP4_PROVIDER=cloudflare.trace
IP6_PROVIDER=none # change if you want to update AAAA
UPDATE_CRON=@every 5m
UPDATE_ON_START=true
DELETE_ON_STOP=false
DELETE_ON_FAILURE=true
TTL=1
PROXIED=!is(dns.example.com) && !is(mc.example.com) && !is(ssh.example.com)
EMOJI=true
UPTIMEKUMA=https://uptime-kuma.example.com/api/push/AsaSDFGFkfklaFALSKffkfFKfkfkfkFK?status=up&msg=OK&ping=
REJECT_CLOUDFLARE_IPS=true
+5 -22
View File
@@ -2,29 +2,12 @@ services:
cloudflare-ddns:
image: timothyjmiller/cloudflare-ddns:latest
container_name: cloudflare-ddns
restart: unless-stopped
security_opt:
- no-new-privileges:true
network_mode: 'host'
# https://github.com/timothymiller/cloudflare-ddns#-quick-start
environment:
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN:?Cloudflare API token is required}
- DOMAINS=${DOMAINS:-}
- IP4_DOMAINS=${IP4_DOMAINS:-}
- IP6_DOMAINS=${IP6_DOMAINS:-}
- IP4_PROVIDER=${IP4_PROVIDER:-cloudflare.trace}
- IP6_PROVIDER=${IP6_PROVIDER:-none}
- UPDATE_CRON=${UPDATE_CRON:-@every 5m}
- UPDATE_ON_START=${UPDATE_ON_START:-true}
- DELETE_ON_STOP=${DELETE_ON_STOP:-false}
- DELETE_ON_FAILURE=${DELETE_ON_FAILURE:-true}
- TTL=${TTL:-1} # 1=auto
# to proxy only "dns.example.com" and "wfs.example.com" use "!is(dns.domain.com) && !is (wfs.domain.com)"
- PROXIED=${PROXIED:-true}
- EMOJI=${EMOJI:-true}
- UPTIMEKUMA=${UPTIMEKUMA:-}
- HEALTHCHECKS=${HEALTHCHECKS:-}
- REJECT_CLOUDFLARE_IPS=${REJECT_CLOUDFLARE_IPS:-true}
# volumes:
# Prefer using environment variables for configuration, config.json legacy support
# - ./config.json:/config.json
- PUID=1000
- PGID=1000
volumes:
- ./config.json:/config.json
restart: unless-stopped
-1
View File
@@ -1 +0,0 @@
secret.yaml
-32
View File
@@ -1,32 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: cfddns
labels:
app: cfddns
spec:
replicas: 1
selector:
matchLabels:
app: cfddns
template:
metadata:
labels:
app: cfddns
spec:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
containers:
- name: cloudflare-ddns
image: timothyjmiller/cloudflare-ddns:latest
imagePullPolicy: Always
resources:
requests:
memory: "20Mi"
cpu: "30m"
limits:
memory: "64Mi"
cpu: "50m"
envFrom:
- secretRef:
name: cfddns-secrets
-18
View File
@@ -1,18 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: cfddns-secrets
type: Opaque
stringData:
CLOUDFLARE_API_TOKEN: your_token
DOMAINS: "example.com,www.example.com"
IP4_PROVIDER: cloudflare.trace
IP6_PROVIDER: none
UPDATE_CRON: "@every 5m"
UPDATE_ON_START: "true"
DELETE_ON_STOP: "false"
DELETE_ON_FAILURE: "true"
TTL: "1"
PROXIED: "true"
EMOJI: "true"
REJECT_CLOUDFLARE_IPS: "true"
-2
View File
@@ -1,2 +0,0 @@
CMK_PASSWORD=password
TZ=Europe/Berlin
-39
View File
@@ -1,39 +0,0 @@
services:
checkmk:
image: "checkmk/check-mk-raw:2.4.0-latest"
container_name: "checkmk"
restart: unless-stopped
# ports:
# - 5000:5000
# - 6776:8000
volumes:
- sites:/omd/sites
tmpfs:
- /opt/omd/sites/cmk/tmp:uid=1000,gid=1000
environment:
- CMK_PASSWORD=${CMK_PASSWORD:-password}
- CMK_SITE_ID=cmk
- TZ=${TZ:-Etc/UTC}
labels:
- "traefik.enable=true"
- "traefik.http.services.checkmk.loadbalancer.server.port=5000"
# Prod Router
- "traefik.http.routers.checkmk.rule=Host(`cmk.forust.xyz`)"
- "traefik.http.routers.checkmk.entrypoints=websecure"
- "traefik.http.routers.checkmk.tls=true"
# Local Router
- "traefik.http.routers.checkmk-local.rule=Host(`cmk.workstation.internal`)"
- "traefik.http.routers.checkmk-local.entrypoints=websecure"
- "traefik.http.routers.checkmk-local.tls=true"
# Dev Router
- "traefik.http.routers.checkmk-dev.rule=Host(`cmk.gigaforust.internal`)"
- "traefik.http.routers.checkmk-dev.entrypoints=websecure"
- "traefik.http.routers.checkmk-dev.tls=true"
networks:
- proxy
networks:
proxy:
external: true
volumes:
sites:
-68
View File
@@ -1,68 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: checkmk-service
namespace: checkmk
spec:
selector:
app: checkmk
ports:
- port: 5000
targetPort: 5000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkmk-deployment
namespace: checkmk
spec:
replicas: 1
selector:
matchLabels:
app: checkmk
template:
metadata:
labels:
app: checkmk
spec:
containers:
- name: checkmk
image: checkmk/check-mk-raw:2.4.0-latest
envFrom:
- secretRef:
name: checkmk-secrets
- configMapRef:
name: checkmk-config
ports:
- containerPort: 5000
volumeMounts:
- name: sites
mountPath: /omd/sites
- name: tmp
mountPath: /opt/omd/sites/cmk/tmp
resources:
requests:
memory: "2Gi"
cpu: "600m"
limits:
memory: "5Gi"
cpu: "4"
volumes:
- name: sites
persistentVolumeClaim:
claimName: checkmk-sites-pvc
- name: tmp
emptyDir:
medium: Memory
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: checkmk-sites-pvc
namespace: checkmk
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
-8
View File
@@ -1,8 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: checkmk-config
namespace: checkmk
data:
TZ: Europe/Bratislava
CMK_SITE_ID: cmk
-32
View File
@@ -1,32 +0,0 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: checkmk-prod
namespace: checkmk
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^cmk\.forust\.xyz$`)
kind: Rule
services:
- name: checkmk-service
port: 5000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: checkmk-local
namespace: checkmk
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^cmk\.(workstation|gigaforust)\.internal$`)
kind: Rule
services:
- name: checkmk-service
port: 5000
-4
View File
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: checkmk
-8
View File
@@ -1,8 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: checkmk-secrets
namespace: checkmk
type: Opaque
stringData:
CMK_PASSWORD: "password"
-43
View File
@@ -1,43 +0,0 @@
services:
convertx:
container_name: convertx
image: ghcr.io/c4illin/convertx:latest
restart: unless-stopped
ports:
- "9992:3000"
# https://github.com/C4illin/ConvertX#environment-variables
environment:
- JWT_SECRET=$(JWT_SECRET)
- ACCOUNT_REGISTRATION=$(ACCOUNT_REGISTRATION:-false)
- HTTP_ALLOWED=$(HTTP_ALLOWED:-false)
- ALLOW_UNAUTHENTICATED=$(ALLOW_UNAUTHENTICATED:-false)
- AUTO_DELETE_EVERY_N_HOURS=$(AUTO_DELETE_EVERY_N_HOURS:-24)
- WEBROOT=$(WEBROOT)
- HIDE_HISTORY=$(HIDE_HISTORY:-false)
- LANGUAGE=$(LANGUAGE:-en)
- UNAUTHENTICATED_USER_SHARING=$(UNAUTHENTICATED_USER_SHARING:-false)
- MAX_CONVERT_PROCESS=$(MAX_CONVERT_PROCESS:-0)
labels:
- "traefik.enable=true"
- "traefik.http.services.convertx.loadbalancer.server.port=3000"
# Prod Router
- "traefik.http.routers.convertx.rule=Host(`convert.forust.xyz`)"
- "traefik.http.routers.convertx.entrypoints=websecure"
- "traefik.http.routers.convertx.tls=true"
# Local Router
- "traefik.http.routers.convertx-local.rule=Host(`convert.workstation.internal`)"
- "traefik.http.routers.convertx-local.entrypoints=websecure"
- "traefik.http.routers.convertx-local.tls=true"
# Dev Router
- "traefik.http.routers.convertx-dev.rule=Host(`convertx.gigaforust.internal`)"
- "traefik.http.routers.convertx-dev.entrypoints=websecure"
- "traefik.http.routers.convertx-dev.tls=true"
networks:
- proxy
volumes:
- data:/app/data
networks:
proxy:
external: true
volumes:
data:
-16
View File
@@ -1,16 +0,0 @@
# test manifest with docker and k8s config keys mismatch
apiVersion: v1
kind: ConfigMap
metadata:
name: convertx-config
namespace: convertx
data:
ACCOUNT_REGISTRATION: "false"
HTTP_ALLOWED: "false"
ALLOW_UNAUTHENTICAED: "false"
AUTO_DELETE_EVERY_N_HOURS: "24"
WEBROOT: "/convert"
HIDE_HISTORY: "false"
LANGUAGE: "en"
UNAUTHED_USER_SHARING: "false"
MAX_CONVERT_PROCESS: "0"
-63
View File
@@ -1,63 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: convertx-service
namespace: convertx
spec:
selector:
app: convertx
ports:
- port: 3000
targetPort: 3000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: convertx-deployment
namespace: convertx
spec:
replicas: 1
selector:
matchLabels:
app: convertx
template:
metadata:
labels:
app: convertx
spec:
containers:
- image: ghcr.io/c4illin/convertx:latest
name: convertx
envFrom:
- configMapRef:
name: convertx-config
- secretRef:
name: convertx-secrets
ports:
- containerPort: 3000
volumeMounts:
- mountPath: /data
name: data
resources:
requests:
memory: "250Mi"
cpu: "100m"
limits:
cpu: "1500m"
memory: "1.5Gi"
volumes:
- name: data
persistentVolumeClaim:
claimName: convertx-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: convertx-pvc
namespace: convertx
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
-32
View File
@@ -1,32 +0,0 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: convertx-prod
namespace: convertx
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^forust\.xyz$`) && PathPrefix(`/convert`)
kind: Rule
services:
- name: convertx-service
port: 3000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: convertx-local
namespace: convertx
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^(workstation|gigaforust)\.internal$`) && PathPrefix(`/convert`)
kind: Rule
services:
- name: convertx-service
port: 3000
-4
View File
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: convertx
-7
View File
@@ -1,7 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: convertx-secrets
type: Opaque
stringData:
jwt-secret: ""
+22 -14
View File
@@ -3,44 +3,52 @@ services:
image: darthnorse/dockmon:latest
container_name: dockmon
restart: unless-stopped
# ports:
# - 8000:443
ports:
- 8000:443
environment:
- TZ=Europe/Bratislava
volumes:
- data:/app/data
- ./data:/app/data
- /var/run/docker.sock:/var/run/docker.sock
healthcheck:
test: [ "CMD", "curl", "-k", "-f", "https://localhost:443/health" ]
interval: 30s
timeout: 10s
retries: 3
networks:
- traefik-proxy
labels:
- "traefik.enable=true"
- "traefik.http.services.dockmon.loadbalancer.server.port=443"
- "traefik.http.services.dockmon.loadbalancer.server.scheme=https"
- "traefik.http.services.dockmon.loadbalancer.serverstransport=insecureTransport@file"
- "traefik.docker.network=traefik-proxy"
# Prod Router
- "traefik.http.routers.dockmon.rule=Host(`dockmon.forust.xyz`)"
- "traefik.http.routers.dockmon.entrypoints=websecure"
- "traefik.http.routers.dockmon.middlewares=security-headers@file"
- "traefik.http.routers.dockmon.middlewares=security-chain@file"
- "traefik.http.routers.dockmon.service=dockmon"
- "traefik.http.routers.dockmon.tls=true"
- "traefik.http.services.dockmon.loadbalancer.server.port=443"
- "traefik.http.services.dockmon.loadbalancer.server.scheme=https"
- "traefik.http.services.dockmon.loadbalancer.serverstransport=insecureTransport@file"
# Local Router
- "traefik.http.routers.dockmon-local.rule=Host(`dockmon.workstation.internal`)"
- "traefik.http.routers.dockmon-local.rule=Host(`dockmon.workstation.internal`) || Host(`dockmon.internal`)"
- "traefik.http.routers.dockmon-local.entrypoints=websecure"
- "traefik.http.routers.dockmon-local.middlewares=security-headers@file"
- "traefik.http.routers.dockmon-local.service=dockmon"
- "traefik.http.routers.dockmon-local.tls=true"
# Dev Router
- "traefik.http.routers.dockmon-dev.rule=Host(`dockmon.gigaforust.internal`)"
- "traefik.http.routers.dockmon-dev.entrypoints=websecure"
- "traefik.http.routers.dockmon-dev.middlewares=security-chain@file"
- "traefik.http.routers.dockmon-dev.service=dockmon"
- "traefik.http.routers.dockmon-dev.tls=true"
# Glance Metadata
- glance.name=dockmon
- glance.url=https://dockmon.forust.xyz/
- glance.description=Dockmon is a lightweight Docker container monitoring and management tool with a user-friendly web interface.
networks:
- proxy
volumes:
data:
networks:
proxy:
traefik-proxy:
external: true
-68
View File
@@ -1,68 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: dockmon-service
namespace: dockmon
spec:
clusterIP: None
selector:
app: dockmon
ports:
- port: 443
targetPort: 443
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: dockmon-statefulset
namespace: dockmon
spec:
serviceName: dockmon-service
replicas: 1
selector:
matchLabels:
app: dockmon
template:
metadata:
labels:
app: dockmon
spec:
containers:
- name: dockmon
image: darthnorse/dockmon:latest
ports:
- containerPort: 443
volumeMounts:
- name: data
mountPath: /app/data
- name: docker-sock
mountPath: /var/run/docker.sock
livenessProbe:
httpGet:
path: /health
port: 443
scheme: HTTPS
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
resources:
requests:
memory: "512Mi"
cpu: "200m"
limits:
memory: "1.5Gi"
cpu: "700m "
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
type: Socket
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
-44
View File
@@ -1,44 +0,0 @@
apiVersion: traefik.io/v1alpha1
kind: ServersTransport
metadata:
name: dockmon-transport
namespace: dockmon
spec:
insecureSkipVerify: true
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: dockmon-prod
namespace: dockmon
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^dockmon\.forust\.xyz$`)
kind: Rule
middlewares:
- name: security-headers@file
services:
- name: dockmon-service
port: 443
serversTransport: dockmon-transport
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: dockmon-local
namespace: dockmon
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^dockmon\.(workstation|gigaforust)\.internal$`)
kind: Rule
services:
- name: dockmon-service
port: 443
serversTransport: dockmon-transport
-4
View File
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: dockmon
-31
View File
@@ -1,31 +0,0 @@
services:
downtify:
container_name: downtify
image: ghcr.io/henriquesebastiao/downtify:latest
restart: unless-stopped
# ports:
# - '7077:8000'
volumes:
- ./Downtify_downloads:/downloads
labels:
- "traefik.enable=true"
- "traefik.http.services.downtify.loadbalancer.server.port=8000"
# Prod Router
- "traefik.http.routers.downtify.rule=Host(`downtify.forust.xyz`)"
- "traefik.http.routers.downtify.entrypoints=websecure"
- "traefik.http.routers.downtify.middlewares=security-chain@file"
- "traefik.http.routers.downtify.tls=true"
# Local Router
- "traefik.http.routers.downtify-local.rule=Host(`downtify.workstation.internal`)"
- "traefik.http.routers.downtify-local.entrypoints=websecure"
- "traefik.http.routers.downtify-local.tls=true"
# Dev Router
- "traefik.http.routers.downtify-dev.rule=Host(`downtify.gigaforust.internal`)"
- "traefik.http.routers.downtify-dev.entrypoints=websecure"
- "traefik.http.routers.downtify-dev.tls=true"
networks:
- proxy
networks:
proxy:
external: true
-58
View File
@@ -1,58 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: downtify-service
namespace: downtify
spec:
selector:
app: downtify
ports:
- port: 8000
targetPort: 8000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: downtify-deployment
namespace: downtify
spec:
replicas: 1
selector:
matchLabels:
app: downtify
template:
metadata:
labels:
app: downtify
spec:
containers:
- name: downtify
image: ghcr.io/henriquesebastiao/downtify:latest
ports:
- containerPort: 8000
volumeMounts:
- name: downloads
mountPath: /downloads
resources:
requests:
memory: "128Mi"
cpu: "200m"
limits:
memory: "1Gi"
cpu: "1"
volumes:
- name: downloads
persistentVolumeClaim:
claimName: downtify-downloads-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: downtify-downloads-pvc
namespace: downtify
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
-34
View File
@@ -1,34 +0,0 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: downtify-prod
namespace: downtify
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^downtify\.forust\.xyz$`)
kind: Rule
middlewares:
- name: security-chain@file
services:
- name: downtify-service
port: 8000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: downtify-local
namespace: downtify
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^downtify\.(workstation|gigaforust)\.internal$`)
kind: Rule
services:
- name: downtify-service
port: 8000
-4
View File
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: downtify
+1 -2
View File
@@ -3,11 +3,10 @@ services:
build:
context: .
dockerfile: Dockerfile
image: gcr.forust.xyz/forust/dtek-notif:latest
pull_policy: build
restart: unless-stopped
environment:
- TZ=Europe/Kyiv
dns:
- 1.1.1.1
- 8.8.8.8
-4
View File
@@ -17,8 +17,6 @@ services:
session-keeper:
build: ./phpsessid-bot
image: gcr.forust.xyz/forust/session-keeper:latest
pull_policy: build
env_file: .env
restart: unless-stopped
depends_on:
@@ -33,8 +31,6 @@ services:
webinar-checker:
build: ./webinar-checker
image: gcr.forust.xyz/forust/webinar-checker:latest
pull_policy: build
env_file: .env
restart: unless-stopped
depends_on:
+9 -20
View File
@@ -12,26 +12,15 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
# Load configuration (adapted to .env keys)
def _env(key, default=None):
v = os.getenv(key, default)
if isinstance(v, str) and len(v) >= 2 and ((v[0] == '"' and v[-1] == '"') or (v[0] == "'" and v[-1] == "'")):
return v[1:-1]
return v
LOGIN = _env('KEEPER_LOGIN')
PASSWORD = _env('KEEPER_PASSWORD')
EDU_BASE = _env('EDU_URL_BASE', 'https://edu.edu.vn.ua')
EDU_LOGIN_PATH = _env('EDU_URL_LOGIN', '/user/login')
EDU_COURSES_PATH = _env('EDU_URL_COURSES', '/course/userlist')
URL_LOGIN = f"{EDU_BASE.rstrip('/')}/{EDU_LOGIN_PATH.lstrip('/')}"
URL_VERIFY = f"{EDU_BASE.rstrip('/')}/{EDU_COURSES_PATH.lstrip('/')}"
INTERVAL = int(_env('KEEPER_INTERVAL', 10))
USER_AGENT = _env('USER_AGENT', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36')
REDIS_HOST = _env('REDIS_HOST', 'redis')
REDIS_PORT = int(_env('REDIS_PORT', 6379))
# Load configuration
LOGIN = os.getenv('EDU_LOGIN')
PASSWORD = os.getenv('EDU_PASSWORD')
URL_LOGIN = os.getenv('EDU_URL_LOGIN', 'https://edu.edu.vn.ua/user/login')
URL_VERIFY = os.getenv('EDU_URL_VERIFY', 'https://edu.edu.vn.ua/course/userlist')
INTERVAL = int(os.getenv('PHPSESSID_INTERVAL', 10))
USER_AGENT = os.getenv('USER_AGENT', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36')
REDIS_HOST = os.getenv('REDIS_HOST', 'redis')
REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))
SUCCESS_FILE = '/tmp/last_success'
+32 -381
View File
@@ -1,13 +1,9 @@
import os
import re
import logging
import redis
import json
import time
from datetime import datetime, timedelta
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ChatMember
from telegram.constants import ChatType
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes
from playwright.async_api import async_playwright
@@ -18,37 +14,22 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
# Suppress HTTP request logs
logging.getLogger('urllib3').setLevel(logging.WARNING)
logging.getLogger('httpx').setLevel(logging.WARNING)
logging.getLogger('telegram.ext._application').setLevel(logging.WARNING)
# Load environment variables
def _env(key, default=None):
v = os.getenv(key, default)
if isinstance(v, str) and len(v) >= 2 and ((v[0] == '"' and v[-1] == '"') or (v[0] == "'" and v[-1] == "'")):
return v[1:-1]
return v
EDU_BASE = _env('EDU_URL_BASE', 'https://edu.edu.vn.ua')
EDU_WEBINAR_PATH = _env('EDU_URL_WEBINAR', '/webinar/useractive')
WEBINAR_URL = f"{EDU_BASE.rstrip('/')}/{EDU_WEBINAR_PATH.lstrip('/')}"
DIARY_URL = f"{EDU_BASE.rstrip('/')}/user/diary"
WEBINAR_CHECK_INTERVAL = int(_env('WEBINAR_CHECK_INTERVAL', 60))
REDIS_HOST = _env('REDIS_HOST', 'redis')
REDIS_PORT = int(_env('REDIS_PORT', 6379))
PLAYWRIGHT_WS = _env('PLAYWRIGHT_WS', 'ws://playwright-service:3000/ws')
USER_AGENT = _env('USER_AGENT', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36')
WEBINAR_TELEGRAM_TOKEN = _env('WEBINAR_TELEGRAM_TOKEN')
ADMIN_ID = int(_env('WEBINAR_ADMIN_ID', '0'))
WEBINAR_URL = os.getenv('WEBINAR_URL', 'https://edu.edu.vn.ua/webinar/useractive')
WEBINAR_CHECK_INTERVAL = int(os.getenv('WEBINAR_CHECK_INTERVAL', 60))
REDIS_HOST = os.getenv('REDIS_HOST', 'redis')
REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))
PLAYWRIGHT_WS = os.getenv('PLAYWRIGHT_WS', 'ws://playwright-service:3000/ws')
USER_AGENT = os.getenv('USER_AGENT', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36')
WEBINAR_TELEGRAM_TOKEN = os.getenv('WEBINAR_TELEGRAM_TOKEN')
ADMIN_ID = int(os.getenv('WEBINAR_ADMIN_ID', '0'))
# Redis Keys
KEY_WHITELIST = "bot:whitelist"
KEY_WHITELIST_ENABLED = "bot:whitelist_enabled"
KEY_SUBSCRIBERS = "bot:subscribers"
KEY_PHPSESSID = "EDU_PHPSESSID"
KEY_WEBINAR_HISTORY = "bot:webinar_history" # Stores last 3 webinars
KEY_WEBINAR_HISTORY = "bot:webinar_history" # Stores last 5 webinars
# Initialize Redis
try:
@@ -67,7 +48,7 @@ TRANSLATIONS = {
'welcome_admin': "\n\n👑 <b>Режим администратора активен</b>",
'access_denied': "⛔ Доступ запрещен. Вас нет в белом списке.",
'help_title': "🤖 <b>Помощь по боту</b>\n\n",
'help_commands': "/start - Подписаться на уведомления\n/stop - Отписаться от уведомлений\n/help - Показать это сообщение\n/language - Сменить язык",
'help_commands': "/start - Подписаться на уведомления\n/help - Показать это сообщение\n/language - Сменить язык",
'help_admin': "\n<b>Команды администратора:</b>\n/adduser [user_id] - Добавить пользователя в белый список\n/removeuser [user_id] - Удалить пользователя из белого списка\nИли используйте панель ниже для управления настройками.",
'admin_only': "⛔ Только для администратора!",
'user_added': "✅ Пользователь {user_id} добавлен в белый список",
@@ -98,15 +79,13 @@ TRANSLATIONS = {
'flag_ru': "🇷🇺 Русский",
'flag_uk': "🇺🇦 Українська",
'flag_en': "🇬🇧 English",
'history_cleared': "✅ История вебинаров очищена",
'history_clear_failed': "❌ Ошибка при очистке истории",
},
'uk': {
'welcome': "👋 Привіт, {name}!\n\nЯ бот-сповіщувач про вебінари. Я повідомлятиму вас, коли з'явиться новий вебінар.\nВи підписані на сповіщення.",
'welcome_admin': "\n\n👑 <b>Режим адміністратора активний</b>",
'access_denied': "⛔ Доступ заборонено. Вас немає в білому списку.",
'help_title': "🤖 <b>Довідка по боту</b>\n\n",
'help_commands': "/start - Підписатися на сповіщення\n/stop - Відписатися від сповіщень\n/help - Показати це повідомлення\n/language - Змінити мову",
'help_commands': "/start - Підписатися на сповіщення\n/help - Показати це повідомлення\n/language - Змінити мову",
'help_admin': "\n<b>Команди адміністратора:</b>\n/adduser [user_id] - Додати користувача до білого списку\n/removeuser [user_id] - Видалити користувача з білого списку\nАбо використовуйте панель нижче для керування налаштуваннями.",
'admin_only': "⛔ Тільки для адміністратора!",
'user_added': "✅ Користувач {user_id} доданий до білого списку",
@@ -137,15 +116,13 @@ TRANSLATIONS = {
'flag_ru': "🇷🇺 Русский",
'flag_uk': "🇺🇦 Українська",
'flag_en': "🇬🇧 English",
'history_cleared': "✅ Історія вебінарів очищена",
'history_clear_failed': "❌ Помилка при очищенні історії",
},
'en': {
'welcome': "👋 Hello, {name}!\n\nI am the Webinar Checker Bot. I will notify you when a new webinar appears.\nYou have been subscribed to notifications.",
'welcome_admin': "\n\n👑 <b>Admin Mode Active</b>",
'access_denied': "⛔ Access denied. You are not on the whitelist.",
'help_title': "🤖 <b>Bot Help</b>\n\n",
'help_commands': "/start - Subscribe to notifications\n/stop - Unsubscribe from notifications\n/help - Show this message\n/language - Change language",
'help_commands': "/start - Subscribe to notifications\n/help - Show this message\n/language - Change language",
'help_admin': "\n<b>Admin Commands:</b>\n/adduser [user_id] - Add user to whitelist\n/removeuser [user_id] - Remove user from whitelist\nOr use the panel below to manage settings.",
'admin_only': "⛔ Admin only!",
'user_added': "✅ User {user_id} added to whitelist",
@@ -176,8 +153,6 @@ TRANSLATIONS = {
'flag_ru': "🇷🇺 Русский",
'flag_uk': "🇺🇦 Українська",
'flag_en': "🇬🇧 English",
'history_cleared': "✅ Webinar history cleared",
'history_clear_failed': "❌ Error clearing history",
}
}
@@ -228,21 +203,6 @@ def is_whitelisted(user_id: int) -> bool:
return redis_client.sismember(KEY_WHITELIST, str(user_id))
async def is_group_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Check if the user is an administrator in the group."""
user = update.effective_user
chat = update.effective_chat
if chat.type in [ChatType.PRIVATE, "private"]:
return True
try:
member = await context.bot.get_chat_member(chat.id, user.id)
return member.status in [ChatMember.OWNER, ChatMember.ADMINISTRATOR]
except Exception as e:
logger.error(f"Failed to check admin status: {e}")
return False
def get_admin_keyboard(user_id: int):
"""Generate admin panel keyboard."""
whitelist_enabled = redis_client.get(KEY_WHITELIST_ENABLED) != "0"
@@ -256,225 +216,24 @@ def get_admin_keyboard(user_id: int):
]
return InlineKeyboardMarkup(keyboard)
# --- Diary Functions ---
DIARY_MONTH_NAMES = ['', 'Січня', 'Лютого', 'Березня', 'Квітня', 'Травня', 'Червня',
'Липня', 'Серпня', 'Вересня', 'Жовтня', 'Листопада', 'Грудня']
DIARY_WEEKDAYS_SHORT = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Нд']
def get_diary_keyboard():
today = datetime.now()
keyboard = [
[
InlineKeyboardButton(f"📌 Сьогодні ({today.day}.{today.month:02d})", callback_data="diary_today"),
InlineKeyboardButton("📌 Завтра", callback_data="diary_tomorrow"),
],
[
InlineKeyboardButton("📅 Цей тиждень", callback_data="diary_week"),
InlineKeyboardButton("📅 Весь місяць", callback_data="diary_month"),
],
]
return InlineKeyboardMarkup(keyboard)
def _parse_calendar_html(table_html: str) -> tuple:
"""Parse calendar HTML table into (month_text, {day_num: {weekday, events}})."""
days = {}
weekdays = []
rows = re.findall(r'<tr[^>]*>(.*?)</tr>', table_html, re.DOTALL)
month_text = ''
for r_idx, row in enumerate(rows):
cells = re.findall(r'<t[dh][^>]*>(.*?)</t[dh]>', row, re.DOTALL)
if r_idx == 0:
# Month navigation row: extract "Травень 2026" from nav text
raw = re.sub(r'<[^>]+>', ' ', row).strip()
raw = re.sub(r'\s+', ' ', raw)
m = re.search(r'([А-Яа-яіїєґ\']+\s*:?\s*\d{4})', raw)
if m:
month_text = m.group(1).replace(' : ', ' ').strip()
else:
month_text = raw
elif r_idx == 1:
# Day names row
for cell in cells:
name = re.sub(r'<[^>]+>', '', cell).strip()
if name:
weekdays.append(name)
else:
# Data rows: each cell = a day
for col_idx, cell in enumerate(cells):
# Extract day number — first number in the cell text
text = re.sub(r'<[^>]+>', ' ', cell).strip()
text = re.sub(r'\s+', ' ', text)
dm = re.match(r'(\d+)', text)
if not dm:
continue
day_num = dm.group(1)
# Extract events: title attribute (full name) of ALL <a> tags inside the cell
events = []
for a_match in re.finditer(r'<a[^>]*>(.*?)</a>', cell, re.DOTALL):
a_tag = a_match.group(0)
# Prefer the title attribute (contains full name, not truncated)
title_m = re.search(r'title\s*=\s*"([^"]*)"', a_tag)
if title_m:
et = title_m.group(1).strip()
else:
et = re.sub(r'<[^>]+>', '', a_match.group(1)).strip()
if et:
events.append(et)
weekday = weekdays[col_idx] if col_idx < len(weekdays) else ''
days[day_num] = {'weekday': weekday, 'events': events}
return month_text, days
async def fetch_diary_data(phpsessid: str) -> dict | None:
logger.info("Fetching diary data via Playwright...")
try:
async with async_playwright() as p:
browser = await p.chromium.connect(PLAYWRIGHT_WS)
try:
context_browser = await browser.new_context(user_agent=USER_AGENT)
await context_browser.add_cookies([{
'name': 'PHPSESSID',
'value': phpsessid,
'domain': 'edu.edu.vn.ua',
'path': '/'
}])
page = await context_browser.new_page()
try:
await page.goto(DIARY_URL, wait_until='domcontentloaded')
await page.wait_for_selector('table.calendar', timeout=10000)
await page.wait_for_timeout(1500)
table_html = await page.evaluate("""
() => {
const t = document.querySelector('table.calendar');
return t ? t.outerHTML : null;
}
""")
if not table_html:
logger.error("table.calendar not found in DOM")
return None
# Debug: save HTML for troubleshooting
try:
with open('/tmp/diary_debug.html', 'w', encoding='utf-8') as f:
f.write(table_html)
except Exception:
pass
month_text, days = _parse_calendar_html(table_html)
logger.info(f"Diary parsed: month={month_text!r}, days_with_events={sum(1 for d in days.values() if d['events'])}/{len(days)}")
return {'monthFullText': month_text, 'days': days}
except Exception as e:
logger.error(f"Error parsing diary: {e}")
return None
finally:
await page.close()
await context_browser.close()
finally:
await browser.close()
except Exception as e:
logger.error(f"Playwright error in diary fetch: {e}")
return None
def _parse_diary_month(text: str) -> str:
match = re.search(r'([А-Яа-яіїєґ\']+\s*:\s*\d{4})', text)
if match:
return match.group(1).replace(' : ', ' ').strip()
return text.strip()
def format_diary_day(data: dict, day_num: int) -> str:
days = data.get('days', {})
month_str = _parse_diary_month(data.get('monthFullText', ''))
day_data = days.get(str(day_num))
lines = [f"📅 <b>{day_num} {month_str}</b>", "" * 18]
if not day_data or not day_data.get('events'):
lines.append("Немає подій")
else:
for e in day_data['events']:
lines.append(f"📌 {e}")
lines.append(f"\n🔗 {DIARY_URL}")
return "\n".join(lines)
def format_diary_week(data: dict, today: datetime) -> str:
days = data.get('days', {})
month_str = _parse_diary_month(data.get('monthFullText', ''))
monday = today - timedelta(days=today.weekday())
sunday = monday + timedelta(days=6)
lines = [f"📅 <b>Тиждень {monday.day}.{monday.month} {sunday.day}.{sunday.month}</b>\n"]
for i in range(7):
d = monday + timedelta(days=i)
day_data = days.get(str(d.day))
lines.append(f"─ <b>{DIARY_WEEKDAYS_SHORT[i]} {d.day}.{d.month}</b> ─")
if not day_data or not day_data.get('events'):
lines.append("Немає подій\n")
else:
for e in day_data['events']:
lines.append(f"📌 {e}")
lines.append("")
lines.append(f"🔗 {DIARY_URL}")
return "\n".join(lines)
def format_diary_month(data: dict) -> str:
days = data.get('days', {})
month_str = _parse_diary_month(data.get('monthFullText', ''))
lines = [f"📅 <b>{month_str}</b>\n"]
for day_num in sorted(days.keys(), key=int):
day_data = days[day_num]
events = day_data.get('events', [])
weekday = day_data.get('weekday', '')
lines.append(f"─ <b>{weekday} {day_num}</b> ─")
if not events:
lines.append("Немає подій\n")
else:
for e in events:
lines.append(f"📌 {e}")
lines.append("")
lines.append(f"🔗 {DIARY_URL}")
return "\n".join(lines)
async def _get_diary_data(context: ContextTypes.DEFAULT_TYPE) -> dict | None:
cached = context.user_data.get('diary_cache')
now_ts = time.time()
if cached and (now_ts - cached.get('timestamp', 0)) < 300:
return cached['data']
phpsessid = redis_client.get(KEY_PHPSESSID)
if not phpsessid:
return None
data = await fetch_diary_data(phpsessid)
if data:
context.user_data['diary_cache'] = {'data': data, 'timestamp': now_ts}
return data
# --- Command Handlers ---
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /start command."""
user = update.effective_user
chat = update.effective_chat
logger.info(f"User {user.id} ({user.username}) started the bot in chat {chat.id} ({chat.type}).")
logger.info(f"User {user.id} ({user.username}) started the bot.")
# Check whitelist - MUST be the user executing the command
if not is_whitelisted(user.id):
await update.message.reply_text(t(user.id, 'access_denied'))
return
# Add to subscribers (Chat ID!)
redis_client.sadd(KEY_SUBSCRIBERS, chat.id)
# Add to subscribers
redis_client.sadd(KEY_SUBSCRIBERS, user.id)
msg = t(chat.id, 'welcome', name=user.first_name)
msg = t(user.id, 'welcome', name=user.first_name)
if user.id == ADMIN_ID and chat.type == "private":
msg += t(chat.id, 'welcome_admin')
if user.id == ADMIN_ID:
msg += t(user.id, 'welcome_admin')
await update.message.reply_text(msg, parse_mode='HTML', reply_markup=get_admin_keyboard(user.id))
else:
await update.message.reply_text(msg, parse_mode='HTML')
@@ -482,39 +241,19 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /help command."""
user_id = update.effective_user.id
chat_id = update.effective_chat.id
msg = t(chat_id, 'help_title') + t(chat_id, 'help_commands')
msg = t(user_id, 'help_title') + t(user_id, 'help_commands')
if user_id == ADMIN_ID and update.effective_chat.type == "private":
msg += t(chat_id, 'help_admin')
if user_id == ADMIN_ID:
msg += t(user_id, 'help_admin')
await update.message.reply_text(msg, parse_mode='HTML', reply_markup=get_admin_keyboard(user_id))
else:
await update.message.reply_text(msg, parse_mode='HTML')
async def stop_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /stop command (unsubscribe)."""
user = update.effective_user
chat = update.effective_chat
# Permission check: Whitelisted user OR Group Admin
if not (is_whitelisted(user.id) or await is_group_admin(update, context)):
await update.message.reply_text(t(chat.id, 'access_denied')) # Or specific "admin only" message
return
redis_client.srem(KEY_SUBSCRIBERS, chat.id)
await update.message.reply_text(t(chat.id, 'whitelist_disabled').replace(" whitelist", " notifications").replace("Білий список", "Сповіщення").replace("Белый список", "Уведомления") if chat.id else "Unsubscribed")
async def language_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /language command."""
user = update.effective_user
chat = update.effective_chat
# Permission check for groups
if not (is_whitelisted(user.id) or await is_group_admin(update, context)):
return
user_id = update.effective_user.id
await update.message.reply_text(
t(chat.id, 'select_language'),
t(user_id, 'select_language'),
parse_mode='HTML',
reply_markup=get_language_keyboard()
)
@@ -564,89 +303,6 @@ async def remove_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
except ValueError:
await update.message.reply_text(t(admin_id, 'invalid_user_id'))
async def clear_history(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Clear webinar history (admin only)."""
admin_id = update.effective_user.id
if admin_id != ADMIN_ID:
await update.message.reply_text(t(admin_id, 'admin_only'))
return
try:
redis_client.delete(KEY_WEBINAR_HISTORY)
await update.message.reply_text(t(admin_id, 'history_cleared'))
logger.info("Admin cleared webinar history")
except Exception as e:
logger.error(f"Failed to clear history: {e}")
await update.message.reply_text(t(admin_id, 'history_clear_failed'))
async def diary_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
chat = update.effective_chat
if not is_whitelisted(user.id):
await update.message.reply_text(t(user.id, 'access_denied'))
return
await update.message.reply_text(
"📅 <b>Щоденник</b> — виберіть період:",
parse_mode='HTML',
reply_markup=get_diary_keyboard()
)
async def diary_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
user_id = query.from_user.id
await query.answer()
if user_id != ADMIN_ID:
if not is_whitelisted(user_id):
await query.edit_message_text("⛔ Доступ заборонено.")
return
data = query.data
if data == "diary_refresh":
context.user_data.pop('diary_cache', None)
await query.edit_message_text("🔄 Завантажую щоденник...")
diary_data = await _get_diary_data(context)
if not diary_data:
await query.edit_message_text("❌ Не вдалося завантажити щоденник. Немає сесії або помилка.")
return
await query.edit_message_text(
"📅 <b>Щоденник</b> — виберіть період:",
parse_mode='HTML',
reply_markup=get_diary_keyboard()
)
return
await query.edit_message_text("🔄 Завантажую щоденник...")
diary_data = await _get_diary_data(context)
if not diary_data:
await query.edit_message_text("❌ Не вдалося завантажити щоденник.")
return
today = datetime.now()
if data == "diary_today":
text = format_diary_day(diary_data, today.day)
elif data == "diary_tomorrow":
tomorrow = today + timedelta(days=1)
if tomorrow.day < today.day:
text = "❌ Дані за наступний місяць недоступні. Перейдіть на сайт."
else:
text = format_diary_day(diary_data, tomorrow.day)
elif data == "diary_week":
text = format_diary_week(diary_data, today)
elif data == "diary_month":
text = format_diary_month(diary_data)
else:
return
if len(text) > 4096:
text = text[:4090] + "\n\n✂️ ...(обрізано)"
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=get_diary_keyboard()
)
# --- Admin Callbacks ---
async def admin_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -707,9 +363,9 @@ async def language_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
# --- Webinar Checking Job ---
def get_webinar_key(url: str) -> str:
"""Generate unique key for a webinar based on URL."""
return url
def get_webinar_key(name: str, url: str) -> str:
"""Generate unique key for a webinar based on name and URL."""
return f"{name}|{url}"
def get_stored_webinars() -> list:
"""Get list of stored webinar keys from Redis."""
@@ -722,9 +378,9 @@ def get_stored_webinars() -> list:
return []
def store_webinars(webinar_keys: list):
"""Store up to 3 most recent webinar keys in Redis."""
# Keep only last 3
webinar_keys = webinar_keys[-3:]
"""Store up to 5 most recent webinar keys in Redis."""
# Keep only last 5
webinar_keys = webinar_keys[-5:]
try:
redis_client.set(KEY_WEBINAR_HISTORY, json.dumps(webinar_keys))
logger.info(f"Stored {len(webinar_keys)} webinar(s) in history")
@@ -859,7 +515,7 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
current_keys = []
for webinar in current_webinars:
key = get_webinar_key(webinar['url'])
key = get_webinar_key(webinar['name'], webinar['url'])
current_keys.append(key)
if key not in stored_keys:
@@ -879,7 +535,6 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
for sub_id in subscribers:
try:
# Build message in user's language
# sub_id comes from redis set as string, convert to int for translation lookup
webinar_items = "\n\n".join([
t(int(sub_id), 'webinar_item', name=w['name'], url=w['url'])
for w in new_webinars
@@ -914,16 +569,12 @@ def main():
# Handlers
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("stop", stop_command))
app.add_handler(CommandHandler("help", help_command))
app.add_handler(CommandHandler("language", language_command))
app.add_handler(CommandHandler("adduser", add_user))
app.add_handler(CommandHandler("removeuser", remove_user))
app.add_handler(CommandHandler("clearhistory", clear_history))
app.add_handler(CommandHandler("diary", diary_command))
# Callback handlers - diary first, then language selection, then admin panel
app.add_handler(CallbackQueryHandler(diary_callback, pattern="^diary_"))
# Callback handlers - language selection first, then admin panel
app.add_handler(CallbackQueryHandler(language_callback, pattern="^lang_"))
app.add_handler(CallbackQueryHandler(admin_callback))
-5
View File
@@ -1,5 +0,0 @@
FROM nginx:alpine
RUN rm -rf /usr/share/nginx/html/*
COPY html /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
-21
View File
@@ -1,21 +0,0 @@
services:
errorpage:
build: .
image: gcr.forust.xyz/forust/error-pages:latest
pull_policy: build
container_name: error-pages
restart: unless-stopped
# ports:
# - 1234:80
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.services.error-pages.loadbalancer.server.port=80"
# Error handler middleware
- "traefik.http.middlewares.error-pages.errors.status=400,402-599"
- "traefik.http.middlewares.error-pages.errors.service=error-pages"
- "traefik.http.middlewares.error-pages.errors.query=/{status}.html"
networks:
proxy:
external: true
-208
View File
@@ -1,208 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>403 // Forbidden</title>
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* hidden in a plain sight? */
:root {
--bg-color: #050505;
--text-color: #e0e0e0;
--accent: #ffffff;
--dim: #666666;
--font-mono: 'Courier New', Courier, monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: var(--font-mono);
line-height: 1.6;
font-size: 16px;
padding: 2rem;
}
a {
color: var(--text-color);
text-decoration: none;
border-bottom: 1px solid var(--dim);
transition: all 0.2s;
}
a:hover {
background-color: var(--text-color);
color: var(--bg-color);
border-color: var(--text-color);
}
.container {
max-width: 800px;
margin: 0 auto;
}
/* TEXT */
h1 {
font-size: 2.5rem;
text-transform: uppercase;
letter-spacing: -2px;
margin-bottom: 0.5rem;
}
h2 {
font-size: 1.2rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--dim);
display: inline-block;
padding-right: 20px;
}
.subtitle {
color: var(--dim);
margin-bottom: 2rem;
}
hr {
border: 0;
border-top: 1px dashed var(--dim);
margin: 2rem 0;
}
.comment {
color: var(--dim);
font-size: 0.9rem;
margin-left: 10px;
}
/* SECTIONS */
section {
margin-bottom: 3rem;
}
/* LISTS */
ul {
list-style: none;
}
.link-list li {
margin-bottom: 0.8rem;
display: flex;
align-items: center;
gap: 15px;
}
/* STACK GRID */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.skill-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.level {
font-weight: bold;
}
.special .level {
color: var(--text-color);
text-shadow: 1px 0 0 red, -1px 0 0 blue;
}
/* my dudes */
.team-grid {
display: flex;
gap: 2rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.member {
text-align: center;
width: 100px;
}
.avatar {
width: 80px;
height: 80px;
background-color: #222;
border: 2px solid var(--text-color);
margin: 0 auto 10px auto;
background-size: cover;
}
/* if no avatar added: */
.placeholder::before {
content: "?";
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 2rem;
color: var(--dim);
}
/* REPOS */
.repo-list li {
margin-bottom: 1rem;
}
/* FOOTER */
footer {
text-align: center;
color: var(--dim);
font-size: 0.8rem;
/* flag{why-are-you-here?} */
margin-top: 4rem;
}
/* SMTH RESPONSIVE */
@media (max-width: 600px) {
.grid-2 {
grid-template-columns: 1fr;
gap: 0;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1 class="glitch" data-text="403">403</h1>
<p class="subtitle">> Forbidden / Access Denied.</p>
</header>
<hr>
<section id="message">
<h2>./error_message</h2>
<p>You do not have permission to access this resource.</p>
<br>
<p>Check the <a href="https://status.forust.xyz">System Status</a> if you believe this is an
error.</p>
</section>
<footer>
<p>root@error:~$ sudo access_resource</p>
<p>User is not in the sudoers file. This incident will be reported.</p>
<p>&copy; XRock - Just Signal.</p>
</footer>
</div>
</body>
</html>
-207
View File
@@ -1,207 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 // Not Found</title>
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* hidden in a plain sight? */
:root {
--bg-color: #050505;
--text-color: #e0e0e0;
--accent: #ffffff;
--dim: #666666;
--font-mono: 'Courier New', Courier, monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: var(--font-mono);
line-height: 1.6;
font-size: 16px;
padding: 2rem;
}
a {
color: var(--text-color);
text-decoration: none;
border-bottom: 1px solid var(--dim);
transition: all 0.2s;
}
a:hover {
background-color: var(--text-color);
color: var(--bg-color);
border-color: var(--text-color);
}
.container {
max-width: 800px;
margin: 0 auto;
}
/* TEXT */
h1 {
font-size: 2.5rem;
text-transform: uppercase;
letter-spacing: -2px;
margin-bottom: 0.5rem;
}
h2 {
font-size: 1.2rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--dim);
display: inline-block;
padding-right: 20px;
}
.subtitle {
color: var(--dim);
margin-bottom: 2rem;
}
hr {
border: 0;
border-top: 1px dashed var(--dim);
margin: 2rem 0;
}
.comment {
color: var(--dim);
font-size: 0.9rem;
margin-left: 10px;
}
/* SECTIONS */
section {
margin-bottom: 3rem;
}
/* LISTS */
ul {
list-style: none;
}
.link-list li {
margin-bottom: 0.8rem;
display: flex;
align-items: center;
gap: 15px;
}
/* STACK GRID */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.skill-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.level {
font-weight: bold;
}
.special .level {
color: var(--text-color);
text-shadow: 1px 0 0 red, -1px 0 0 blue;
}
/* my dudes */
.team-grid {
display: flex;
gap: 2rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.member {
text-align: center;
width: 100px;
}
.avatar {
width: 80px;
height: 80px;
background-color: #222;
border: 2px solid var(--text-color);
margin: 0 auto 10px auto;
background-size: cover;
}
/* if no avatar added: */
.placeholder::before {
content: "?";
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 2rem;
color: var(--dim);
}
/* REPOS */
.repo-list li {
margin-bottom: 1rem;
}
/* FOOTER */
footer {
text-align: center;
color: var(--dim);
font-size: 0.8rem;
/* flag{why-are-you-here?} */
margin-top: 4rem;
}
/* SMTH RESPONSIVE */
@media (max-width: 600px) {
.grid-2 {
grid-template-columns: 1fr;
gap: 0;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1 class="glitch" data-text="404">404</h1>
<p class="subtitle">> Page Not Found / Lost in the Void.</p>
</header>
<hr>
<section id="message">
<h2>./error_message</h2>
<p>The page you are looking for does not exist or has been moved.</p>
<br>
<p>Check the <a href="https://status.forust.xyz">System Status</a> if you believe this is an error.</p>
</section>
<footer>
<p>root@error:~$ ping target</p>
<p>Destination Host Unreachable</p>
<p>&copy; XRock - Just Signal.</p>
</footer>
</div>
</body>
</html>
-207
View File
@@ -1,207 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>500 // Server Error</title>
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* hidden in a plain sight? */
:root {
--bg-color: #050505;
--text-color: #e0e0e0;
--accent: #ffffff;
--dim: #666666;
--font-mono: 'Courier New', Courier, monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: var(--font-mono);
line-height: 1.6;
font-size: 16px;
padding: 2rem;
}
a {
color: var(--text-color);
text-decoration: none;
border-bottom: 1px solid var(--dim);
transition: all 0.2s;
}
a:hover {
background-color: var(--text-color);
color: var(--bg-color);
border-color: var(--text-color);
}
.container {
max-width: 800px;
margin: 0 auto;
}
/* TEXT */
h1 {
font-size: 2.5rem;
text-transform: uppercase;
letter-spacing: -2px;
margin-bottom: 0.5rem;
}
h2 {
font-size: 1.2rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--dim);
display: inline-block;
padding-right: 20px;
}
.subtitle {
color: var(--dim);
margin-bottom: 2rem;
}
hr {
border: 0;
border-top: 1px dashed var(--dim);
margin: 2rem 0;
}
.comment {
color: var(--dim);
font-size: 0.9rem;
margin-left: 10px;
}
/* SECTIONS */
section {
margin-bottom: 3rem;
}
/* LISTS */
ul {
list-style: none;
}
.link-list li {
margin-bottom: 0.8rem;
display: flex;
align-items: center;
gap: 15px;
}
/* STACK GRID */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.skill-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.level {
font-weight: bold;
}
.special .level {
color: var(--text-color);
text-shadow: 1px 0 0 red, -1px 0 0 blue;
}
/* my dudes */
.team-grid {
display: flex;
gap: 2rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.member {
text-align: center;
width: 100px;
}
.avatar {
width: 80px;
height: 80px;
background-color: #222;
border: 2px solid var(--text-color);
margin: 0 auto 10px auto;
background-size: cover;
}
/* if no avatar added: */
.placeholder::before {
content: "?";
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 2rem;
color: var(--dim);
}
/* REPOS */
.repo-list li {
margin-bottom: 1rem;
}
/* FOOTER */
footer {
text-align: center;
color: var(--dim);
font-size: 0.8rem;
/* flag{why-are-you-here?} */
margin-top: 4rem;
}
/* SMTH RESPONSIVE */
@media (max-width: 600px) {
.grid-2 {
grid-template-columns: 1fr;
gap: 0;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1 class="glitch" data-text="500">500</h1>
<p class="subtitle">> Internal Server Error / System Failure.</p>
</header>
<hr>
<section id="message">
<h2>./error_message</h2>
<p>Something went wrong on our end. We are working to fix it.</p>
<br>
<p>Check the <a href="https://status.forust.xyz">System Status</a> for more information.</p>
</section>
<footer>
<p>root@error:~$ systemctl status service</p>
<p>Active: failed (Result: core-dump)</p>
<p>&copy; XRock - Just Signal.</p>
</footer>
</div>
</body>
</html>
-207
View File
@@ -1,207 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>502 // Bad Gateway</title>
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* hidden in a plain sight? */
:root {
--bg-color: #050505;
--text-color: #e0e0e0;
--accent: #ffffff;
--dim: #666666;
--font-mono: 'Courier New', Courier, monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: var(--font-mono);
line-height: 1.6;
font-size: 16px;
padding: 2rem;
}
a {
color: var(--text-color);
text-decoration: none;
border-bottom: 1px solid var(--dim);
transition: all 0.2s;
}
a:hover {
background-color: var(--text-color);
color: var(--bg-color);
border-color: var(--text-color);
}
.container {
max-width: 800px;
margin: 0 auto;
}
/* TEXT */
h1 {
font-size: 2.5rem;
text-transform: uppercase;
letter-spacing: -2px;
margin-bottom: 0.5rem;
}
h2 {
font-size: 1.2rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--dim);
display: inline-block;
padding-right: 20px;
}
.subtitle {
color: var(--dim);
margin-bottom: 2rem;
}
hr {
border: 0;
border-top: 1px dashed var(--dim);
margin: 2rem 0;
}
.comment {
color: var(--dim);
font-size: 0.9rem;
margin-left: 10px;
}
/* SECTIONS */
section {
margin-bottom: 3rem;
}
/* LISTS */
ul {
list-style: none;
}
.link-list li {
margin-bottom: 0.8rem;
display: flex;
align-items: center;
gap: 15px;
}
/* STACK GRID */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.skill-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.level {
font-weight: bold;
}
.special .level {
color: var(--text-color);
text-shadow: 1px 0 0 red, -1px 0 0 blue;
}
/* my dudes */
.team-grid {
display: flex;
gap: 2rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.member {
text-align: center;
width: 100px;
}
.avatar {
width: 80px;
height: 80px;
background-color: #222;
border: 2px solid var(--text-color);
margin: 0 auto 10px auto;
background-size: cover;
}
/* if no avatar added: */
.placeholder::before {
content: "?";
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 2rem;
color: var(--dim);
}
/* REPOS */
.repo-list li {
margin-bottom: 1rem;
}
/* FOOTER */
footer {
text-align: center;
color: var(--dim);
font-size: 0.8rem;
/* flag{why-are-you-here?} */
margin-top: 4rem;
}
/* SMTH RESPONSIVE */
@media (max-width: 600px) {
.grid-2 {
grid-template-columns: 1fr;
gap: 0;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1 class="glitch" data-text="502">502</h1>
<p class="subtitle">> Bad Gateway / System Failure.</p>
</header>
<hr>
<section id="message">
<h2>./error_message</h2>
<p>The server received an invalid response from the upstream server.</p>
<br>
<p>Check the <a href="https://status.forust.xyz">System Status</a> for more information.</p>
</section>
<footer>
<p>root@error:~$ curl -I upstream_host</p>
<p>HTTP/1.1 502 Bad Gateway</p>
<p>&copy; XRock - Just Signal.</p>
</footer>
</div>
</body>
</html>
-207
View File
@@ -1,207 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>503 // Service Unavailable</title>
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* hidden in a plain sight? */
:root {
--bg-color: #050505;
--text-color: #e0e0e0;
--accent: #ffffff;
--dim: #666666;
--font-mono: 'Courier New', Courier, monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: var(--font-mono);
line-height: 1.6;
font-size: 16px;
padding: 2rem;
}
a {
color: var(--text-color);
text-decoration: none;
border-bottom: 1px solid var(--dim);
transition: all 0.2s;
}
a:hover {
background-color: var(--text-color);
color: var(--bg-color);
border-color: var(--text-color);
}
.container {
max-width: 800px;
margin: 0 auto;
}
/* TEXT */
h1 {
font-size: 2.5rem;
text-transform: uppercase;
letter-spacing: -2px;
margin-bottom: 0.5rem;
}
h2 {
font-size: 1.2rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--dim);
display: inline-block;
padding-right: 20px;
}
.subtitle {
color: var(--dim);
margin-bottom: 2rem;
}
hr {
border: 0;
border-top: 1px dashed var(--dim);
margin: 2rem 0;
}
.comment {
color: var(--dim);
font-size: 0.9rem;
margin-left: 10px;
}
/* SECTIONS */
section {
margin-bottom: 3rem;
}
/* LISTS */
ul {
list-style: none;
}
.link-list li {
margin-bottom: 0.8rem;
display: flex;
align-items: center;
gap: 15px;
}
/* STACK GRID */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.skill-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.level {
font-weight: bold;
}
.special .level {
color: var(--text-color);
text-shadow: 1px 0 0 red, -1px 0 0 blue;
}
/* my dudes */
.team-grid {
display: flex;
gap: 2rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.member {
text-align: center;
width: 100px;
}
.avatar {
width: 80px;
height: 80px;
background-color: #222;
border: 2px solid var(--text-color);
margin: 0 auto 10px auto;
background-size: cover;
}
/* if no avatar added: */
.placeholder::before {
content: "?";
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 2rem;
color: var(--dim);
}
/* REPOS */
.repo-list li {
margin-bottom: 1rem;
}
/* FOOTER */
footer {
text-align: center;
color: var(--dim);
font-size: 0.8rem;
/* flag{why-are-you-here?} */
margin-top: 4rem;
}
/* SMTH RESPONSIVE */
@media (max-width: 600px) {
.grid-2 {
grid-template-columns: 1fr;
gap: 0;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1 class="glitch" data-text="503">503</h1>
<p class="subtitle">> Service Unavailable / System Failure.</p>
</header>
<hr>
<section id="message">
<h2>./error_message</h2>
<p>The server is currently unable to handle the request due to maintenance or overload.</p>
<br>
<p>Check the <a href="https://status.forust.xyz">System Status</a> for more information.</p>
</section>
<footer>
<p>root@error:~$ systemctl start service</p>
<p>Job for service failed because the control process exited with error code.</p>
<p>&copy; XRock - Just Signal.</p>
</footer>
</div>
</body>
</html>
-207
View File
@@ -1,207 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>504 // Gateway Timeout</title>
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* hidden in a plain sight? */
:root {
--bg-color: #050505;
--text-color: #e0e0e0;
--accent: #ffffff;
--dim: #666666;
--font-mono: 'Courier New', Courier, monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: var(--font-mono);
line-height: 1.6;
font-size: 16px;
padding: 2rem;
}
a {
color: var(--text-color);
text-decoration: none;
border-bottom: 1px solid var(--dim);
transition: all 0.2s;
}
a:hover {
background-color: var(--text-color);
color: var(--bg-color);
border-color: var(--text-color);
}
.container {
max-width: 800px;
margin: 0 auto;
}
/* TEXT */
h1 {
font-size: 2.5rem;
text-transform: uppercase;
letter-spacing: -2px;
margin-bottom: 0.5rem;
}
h2 {
font-size: 1.2rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--dim);
display: inline-block;
padding-right: 20px;
}
.subtitle {
color: var(--dim);
margin-bottom: 2rem;
}
hr {
border: 0;
border-top: 1px dashed var(--dim);
margin: 2rem 0;
}
.comment {
color: var(--dim);
font-size: 0.9rem;
margin-left: 10px;
}
/* SECTIONS */
section {
margin-bottom: 3rem;
}
/* LISTS */
ul {
list-style: none;
}
.link-list li {
margin-bottom: 0.8rem;
display: flex;
align-items: center;
gap: 15px;
}
/* STACK GRID */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.skill-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.level {
font-weight: bold;
}
.special .level {
color: var(--text-color);
text-shadow: 1px 0 0 red, -1px 0 0 blue;
}
/* my dudes */
.team-grid {
display: flex;
gap: 2rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.member {
text-align: center;
width: 100px;
}
.avatar {
width: 80px;
height: 80px;
background-color: #222;
border: 2px solid var(--text-color);
margin: 0 auto 10px auto;
background-size: cover;
}
/* if no avatar added: */
.placeholder::before {
content: "?";
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 2rem;
color: var(--dim);
}
/* REPOS */
.repo-list li {
margin-bottom: 1rem;
}
/* FOOTER */
footer {
text-align: center;
color: var(--dim);
font-size: 0.8rem;
/* flag{why-are-you-here?} */
margin-top: 4rem;
}
/* SMTH RESPONSIVE */
@media (max-width: 600px) {
.grid-2 {
grid-template-columns: 1fr;
gap: 0;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1 class="glitch" data-text="504">504</h1>
<p class="subtitle">> Gateway Timeout / System Failure.</p>
</header>
<hr>
<section id="message">
<h2>./error_message</h2>
<p>The server did not receive a timely response from the upstream server.</p>
<br>
<p>Check the <a href="https://status.forust.xyz">System Status</a> for more information.</p>
</section>
<footer>
<p>root@error:~$ timeout 30s curl upstream</p>
<p>curl: (28) Operation timed out after 30001 milliseconds with 0 bytes received</p>
<p>&copy; XRock - Just Signal.</p>
</footer>
</div>
</body>
</html>
-33
View File
@@ -1,33 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: error-pages-service
namespace: error-pages
spec:
selector:
app: error-pages
ports:
- port: 80
targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: error-pages-deployment
namespace: error-pages
spec:
replicas: 1
selector:
matchLabels:
app: error-pages
template:
metadata:
labels:
app: error-pages
spec:
containers:
- name: error-pages
image: gcr.forust.xyz/forust/error-pages:latest
ports:
- containerPort: 80
---
-4
View File
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: error-pages
-3
View File
@@ -1,6 +1,3 @@
GITEA_POSTGRES_USER=
GITEA_POSTGRES_PASSWORD=
GITEA_POSTGRES_DB=gitea
GITEA_SMTP_PASS=
MAILER_ADDR=
SERVICE_EMAIL=email.used.by.services@domain.tld
+21 -29
View File
@@ -1,8 +1,7 @@
services:
server:
image: docker.gitea.com/gitea:1.26
image: docker.gitea.com/gitea:1.25.1
container_name: gitea
restart: always
environment:
- USER_UID=1000
- USER_GID=1000
@@ -14,53 +13,45 @@ services:
- GITEA__database__NAME=gitea
#Server
- GITEA__server__ROOT_URL=https://gitea.forust.xyz
- GITEA__server__SSH_DOMAIN=gitssh.forust.xyz
- GITEA__server__SSH_PORT=2221
# Mailer
- GITEA__mailer__ENABLED=true
- GITEA__mailer__FROM=${SERVICE_EMAIL}
- GITEA__mailer__SMTP_ADDR=${MAILER_ADDR}:465
- GITEA__mailer__USER=${SERVICE_EMAIL}
- GITEA__mailer__PASSWD=${GITEA_SMTP_PASS}
- GITEA__mailer__PROTOCOL=SMTP
- GITEA__service__REGISTER_EMAIL_CONFIRM=true
- GITEA__service__ENABLE_NOTIFY_MAIL=true
restart: always
networks:
- gitea-db
- traefik-proxy
volumes:
- ./gitea-data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
labels:
- "traefik.enable=true"
- "traefik.http.services.gitea.loadbalancer.server.port=3000"
- "traefik.docker.network=traefik-proxy"
# Prod Router
- "traefik.http.routers.gitea.rule=Host(`gitea.forust.xyz`)"
- "traefik.http.routers.gitea.entrypoints=websecure"
- "traefik.http.routers.gitea.middlewares=security-headers@file"
- "traefik.http.routers.gitea.service=gitea"
- "traefik.http.routers.gitea.tls=true"
- "traefik.http.services.gitea.loadbalancer.server.port=3000"
# Local Router
- "traefik.http.routers.gitea-local.rule=Host(`gitea.workstation.internal`)"
- "traefik.http.routers.gitea-local.rule=Host(`gitea.workstation.internal`) || Host(`gitea.internal`)"
- "traefik.http.routers.gitea-local.entrypoints=websecure"
- "traefik.http.routers.gitea-local.middlewares=security-headers@file"
- "traefik.http.routers.gitea-local.service=gitea"
- "traefik.http.routers.gitea-local.tls=true"
# Dev Router
- "traefik.http.routers.gitea-dev.rule=Host(`gitea.gigaforust.internal`)"
- "traefik.http.routers.gitea-dev.entrypoints=websecure"
- "traefik.http.routers.gitea-dev.middlewares=security-headers@file"
- "traefik.http.routers.gitea-dev.service=gitea"
- "traefik.http.routers.gitea-dev.tls=true"
# SSH Router
- "traefik.tcp.services.gitea.loadbalancer.server.port=22"
- "traefik.tcp.routers.gitea.entrypoints=ssh"
- "traefik.tcp.routers.gitea.rule=HostSNI(`*`)"
# Gitea container registry Router
- "traefik.http.routers.gitea-registry.rule=Host(`gcr.forust.xyz`) && PathPrefix(`/v2`)"
- "traefik.http.routers.gitea-registry.entrypoints=websecure"
- "traefik.http.routers.gitea-registry.tls.certresolver=letsencrypt"
- "traefik.http.routers.gitea-registry.tls=true"
ports:
- "2221:22"
networks:
- gitea-db
- proxy
depends_on:
- db
db:
image: docker.io/library/postgres:14
restart: always
@@ -68,12 +59,13 @@ services:
- POSTGRES_USER=gitea
- POSTGRES_PASSWORD=gitea
- POSTGRES_DB=gitea
volumes:
- ./gitea-db/:/var/lib/postgresql/data
networks:
- gitea-db
volumes:
- ./gitea-db/:/var/lib/postgresql/data
networks:
gitea-db:
external: false
proxy:
traefik-proxy:
external: true
-20
View File
@@ -1,20 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: gitea-config
namespace: gitea
data:
GITEA__server__DOMAIN: "gitea.forust.xyz"
GITEA__server__ROOT_URL: "https://gitea.forust.xyz"
GITEA__server__SSH_DOMAIN: "gitssh.forust.xyz"
GITEA__server__SSH_PORT: "2221"
GITEA__database__DB_TYPE: "postgres"
GITEA__database__HOST: "gitea-postgres-service:5432"
GITEA__database__NAME: "gitea"
GITEA__security__REVERSE_PROXY_LIMIT: "1"
GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES: "*"
GITEA__mailer__ENABLED: "false"
USER_UID: "1000"
USER_GID: "1000"
-71
View File
@@ -1,71 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: gitea-service
namespace: gitea
spec:
selector:
app: gitea
ports:
- port: 3000
name: http
targetPort: 3000
- port: 2221
name: ssh
targetPort: 22
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitea-deployment
namespace: gitea
spec:
replicas: 1
selector:
matchLabels:
app: gitea
template:
metadata:
labels:
app: gitea
spec:
containers:
- name: gitea
image: docker.gitea.com/gitea:1.26
envFrom:
- configMapRef:
name: gitea-config
- secretRef:
name: gitea-secrets
ports:
- containerPort: 3000
name: http
- containerPort: 2221
name: ssh
volumeMounts:
- name: gitea-data
mountPath: /data
resources:
requests:
memory: "512Mi"
cpu: "300m"
limits:
memory: "1.5Gi"
cpu: "1300m"
volumes:
- name: gitea-data
persistentVolumeClaim:
claimName: gitea-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitea-pvc
namespace: gitea
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
-63
View File
@@ -1,63 +0,0 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: gitea-prod
namespace: gitea
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^gitea\.forust\.xyz$`)
kind: Rule
services:
- name: gitea-service
port: 3000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: gitea-local
namespace: gitea
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^gitea\.(workstation|gigaforust)\.internal$`)
kind: Rule
services:
- name: gitea-service
port: 3000
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: gitea-registry
namespace: gitea
spec:
entryPoints:
- websecure
routes:
- match: Host(`gcr.forust.xyz`) && PathPrefix(`/v2`)
kind: Rule
services:
- name: gitea-service
port: 3000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: gitea-ssh
namespace: gitea
spec:
entryPoints:
- ssh
routes:
- match: HostSNI(`*`)
services:
- name: gitea-service
port: 2221
-4
View File
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: gitea
-62
View File
@@ -1,62 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: gitea-postgres-service
namespace: gitea
spec:
clusterIP: None
selector:
app: gitea-postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: gitea-postgres-statefulset
namespace: gitea
spec:
selector:
matchLabels:
app: gitea-postgres
serviceName: gitea-postgres-service
replicas: 1
template:
metadata:
labels:
app: gitea-postgres
spec:
containers:
- name: gitea-postgres
image: postgres:14
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: gitea-secrets
key: GITEA__database__USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: gitea-secrets
key: GITEA__database__PASSWD
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: gitea-secrets
key: GITEA__database__USER
ports:
- containerPort: 5432
name: postgres
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
-9
View File
@@ -1,9 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: gitea-secrets
namespace: gitea
type: Opaque
stringData:
GITEA__database__USER: "gitea"
GITEA__database__PASSWD: "gitea"
+12 -5
View File
@@ -11,23 +11,30 @@ services:
env_file: .env
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.services.glance.loadbalancer.server.port=8080"
- "traefik.docker.network=traefik-proxy"
# Prod Router
- "traefik.http.routers.glance.rule=Host(`glance.forust.xyz`)"
- "traefik.http.routers.glance.entrypoints=websecure"
- "traefik.http.routers.glance.middlewares=security-chain@file"
- "traefik.http.routers.glance.tls=true"
# Local Router
- "traefik.http.routers.glance-local.rule=Host(`glance.workstation.internal`)"
- "traefik.http.routers.glance-local.rule=Host(`glance.workstation.internal`) || Host(`glance.internal`)"
- "traefik.http.routers.glance-local.entrypoints=websecure"
- "traefik.http.routers.glance-local.middlewares=security-headers@file"
- "traefik.http.routers.glance-local.tls=true"
# Dev Router
- "traefik.http.routers.glance-dev.rule=Host(`glance.gigaforust.internal`)"
- "traefik.http.routers.glance-dev.entrypoints=websecure"
- "traefik.http.routers.glance-dev.middlewares=security-chain@file"
- "traefik.http.routers.glance-dev.tls=true"
networks:
- proxy
- traefik-proxy
dns:
- 1.1.1.1
- 8.8.8.8
networks:
proxy:
traefik-proxy:
external: true
-209
View File
@@ -1,209 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: glance-assets
namespace: glance
data:
# Инжектируем твой брутализм напрямую в ассеты
user.css: |
:root {
--bg-color: #050505;
--text-color: #e0e0e0;
--accent: #ffffff;
--dim: #666666;
--font-mono: 'Courier New', Courier, monospace;
}
/* Принудительно ставим моноширинный шрифт для всего дашборда */
body, id, main, div, span, p, a, h1, h2, h3 {
font-family: var(--font-mono) !important;
letter-spacing: -0.5px;
}
/* Ломаем закругления Glance и делаем жесткие рамки */
.widget, .card, main div, [class*="widget"], [class*="card"] {
border-radius: 0px !important;
border: 1px solid var(--dim) !important;
box-shadow: none !important;
background-color: var(--bg-color) !important;
}
/* Стилизация ссылок под ховер-эффект из твоего style.css */
a {
color: var(--text-color) !important;
text-decoration: none !important;
border-bottom: 1px solid var(--dim) !important;
transition: all 0.2s;
}
a:hover {
background-color: var(--text-color) !important;
color: var(--bg-color) !important;
border-color: var(--text-color) !important;
}
/* Кастомизация заголовков внутри модулей */
h2, .widget-title, [class*="title"] {
text-transform: uppercase;
font-weight: bold;
}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: glance-config
namespace: glance
data:
glance.yml: |
server:
assets-path: /app/assets
theme:
# Перевели #050505 и #e0e0e0 в формат HSL для Glance
background-color: 0 0 2 # Истинно черный фон
primary-color: 0 0 88 # Светло-серый текст
contrast-multiplier: 1.4
positive-color: 140 50 50 # Зеленый для UP-сервисов (не вырвиглазный)
negative-color: 0 70 50 # Красный для упавших сайтов
custom-css-file: /assets/user.css
pages:
- $include: home.yml
- $include: docker.yml
- $include: monitor.yml
home.yml: |
- name: Home
columns:
- size: small
widgets:
- type: clock
hour-format: 24h
timezones:
- timezone: Europe/Bratislava
label: Bratislava
- timezone: Europe/Kyiv
label: Kyiv
- timezone: Europe/Moscow
label: St. Petersburg
- type: calendar
first-day-of-week: monday
- type: rss
limit: 10
collapse-after: 3
cache: 12h
feeds:
- url: https://selfh.st/rss/
title: selfh.st
- size: full
widgets:
- type: group
widgets:
- type: hacker-news
- type: lobsters
- type: videos
channels:
- UCXuqSBlHAE6Xw-yeJA0Tunw
- UCR-DXc1voovS8nhAvccRZhg
- UCsBjURrPoezykLs9EqgamOA
- UCBJycsmduvYEL83R_U4JriQ
- UCHnyfMqiRRG1u-2MsSQLbXA
- type: group
widgets:
- type: reddit
subreddit: technology
show-thumbnails: true
- type: reddit
subreddit: selfhosted
show-thumbnails: true
- size: small
widgets:
- type: weather
location: London, United Kingdom
units: metric
hour-format: 12h
hide-location: true
- type: markets
markets:
- symbol: SPY
name: S&P 500
- symbol: BTC-USD
name: Bitcoin
- symbol: NVDA
name: NVIDIA
- symbol: AAPL
name: Apple
- symbol: MSFT
name: Microsoft
- type: releases
cache: 1d
repositories:
- glanceapp/glance
- go-gitea/gitea
- nextcloud/all-in-one
docker.yml: |
- name: Docker
columns:
- size: full
widgets:
- type: docker-containers
hide-by-default: false
monitor.yml: |
- name: Monitoring
columns:
- size: small
widgets:
- type: dns-stats
service: adguard
url: http://adguard-service.adguard.svc.cluster.local:3000
username: forust
password: ${ADGUARD_PASSWORD}
- size: full
widgets:
- type: monitor
title: Services Status
cache: 1m
sites:
- title: forust.xyz
url: https://forust.xyz
- title: dns.forust.xyz
url: https://dns.forust.xyz
- title: www.lk-tour.com.ua
url: https://www.lk-tour.com.ua
- title: lk-tour.com.ua
url: https://lk-tour.com.ua
# - title: gitssh.forust.xyz
# url: https://gitssh.forust.xyz
# - title: gcr.forust.xyz
# url: https://gcr.forust.xyz/v2/
- title: gitea.forust.xyz
url: https://gitea.forust.xyz
- title: nextcloud.forust.xyz
url: https://nextcloud.forust.xyz
- title: mc.forust.xyz
url: https://mc.forust.xyz/map
- title: auth.forust.xyz
url: https://auth.forust.xyz
- title: metube.forust.xyz
url: https://metube.forust.xyz
- title: dockmon.forust.xyz
url: https://dockmon.forust.xyz
- title: portainer.forust.xyz
url: https://portainer.forust.xyz
- title: termix.forust.xyz
url: https://termix.forust.xyz
- title: uptime.forust.xyz
url: https://uptime.forust.xyz
- title: cmk.forust.xyz
url: https://cmk.forust.xyz
- title: search.forust.xyz
url: https://search.forust.xyz
- title: status.forust.xyz
url: https://status.forust.xyz
- title: traefik.forust.xyz
url: https://traefik.forust.xyz
- title: media.forust.xyz
url: https://media.forust.xyz
- title: wfs.forust.xyz
url: https://wfs.forust.xyz
- title: forust.xyz/convert
url: https://forust.xyz/convert
-78
View File
@@ -1,78 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: glance-service
namespace: glance
spec:
selector:
app: glance
ports:
- port: 8080
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: glance-deployment
namespace: glance
spec:
replicas: 1
selector:
matchLabels:
app: glance
template:
metadata:
labels:
app: glance
spec:
containers:
- name: glance
image: glanceapp/glance
envFrom:
- secretRef:
name: glance-secrets
ports:
- containerPort: 8080
volumeMounts:
- name: glance-config
mountPath: /app/config/glance.yml
subPath: glance.yml
- name: glance-config
mountPath: /app/config/home.yml
subPath: home.yml
- name: glance-config
mountPath: /app/config/docker.yml
subPath: docker.yml
- name: glance-config
mountPath: /app/config/monitor.yml
subPath: monitor.yml
- name: glance-assets
mountPath: /app/assets/user.css
subPath: user.css
- name: docker-socket
mountPath: /var/run/docker.sock
- name: localtime
mountPath: /etc/localtime
readOnly: true
resources:
requests:
memory: "10Mi"
cpu: "20m"
limits:
memory: "100Mi"
cpu: "50m"
volumes:
- name: glance-config
configMap:
name: glance-config
- name: glance-assets
configMap:
name: glance-config
- name: docker-socket
hostPath:
path: /var/run/docker.sock
type: Socket
- name: localtime
hostPath:
path: /etc/localtime
type: File
-35
View File
@@ -1,35 +0,0 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: glance-prod
namespace: glance
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^forust\.xyz$`)
kind: Rule
middlewares:
- name: glance-strupprefix
services:
- name: glance-service
port: 8080
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: glance-local
namespace: glance
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^glance\.(workstation|gigaforust)\.internal$`)
kind: Rule
services:
- name: glance-service
port: 8080
-4
View File
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: glance
-98
View File
@@ -1,98 +0,0 @@
services:
headscale:
image: headscale/headscale:latest
restart: unless-stopped
container_name: headscale-server
command: serve
ports:
- 18080:8080
- 19090:9090
networks:
- proxy
volumes:
- ./config/headscale.yaml:/etc/headscale/config.yaml
- data:/var/lib/headscale
- ./config/policy.json:/var/lib/headscale/policy.json
labels:
- "me.tale.headplane.target: headscale"
- "traefik.enable=true"
- "traefik.http.services.headscale.loadbalancer.server.port=8080"
- "traefik.http.services.headscale-metrics.loadbalancer.server.port=9090"
## SERVICE
# Prod Router
- "traefik.http.routers.headscale.rule=Host(`hs.forust.xyz`)"
- "traefik.http.routers.headscale.entrypoints=websecure"
- "traefik.http.routers.headscale.service=headscale"
- "traefik.http.routers.headscale.tls=true"
# Local Router
- "traefik.http.routers.headscale-local.rule=Host(`hs.workstation.internal`)"
- "traefik.http.routers.headscale-local.entrypoints=websecure"
- "traefik.http.routers.headscale-local.service=headscale"
- "traefik.http.routers.headscale-local.tls=true"
# Dev Router
- "traefik.http.routers.headscale-dev.rule=Host(`hs.gigaforust.internal`)"
- "traefik.http.routers.headscale-dev.entrypoints=websecure"
- "traefik.http.routers.headscale-dev.service=headscale"
- "traefik.http.routers.headscale-dev.tls=true"
## METRICS
# Prod Router
- "traefik.http.routers.headscale-metrics.rule=Host(`hs.forust.xyz`) && PathPrefix(`/metrics`)"
- "traefik.http.routers.headscale-metrics.entrypoints=websecure"
- "traefik.http.routers.headscale-metrics.service=headscale-metrics"
- "traefik.http.routers.headscale-metrics.tls=true"
# Local Router
- "traefik.http.routers.headscale-metrics-local.rule=Host(`hs.workstation.internal`) && PathPrefix(`/metrics`)"
- "traefik.http.routers.headscale-metrics-local.entrypoints=websecure"
- "traefik.http.routers.headscale-metrics-local.service=headscale-metrics"
- "traefik.http.routers.headscale-metrics-local.tls=true"
# Dev Router
- "traefik.http.routers.headscale-metrics-dev.rule=Host(`hs.gigaforust.internal`) && PathPrefix(`/metrics`)"
- "traefik.http.routers.headscale-metrics-dev.entrypoints=websecure"
- "traefik.http.routers.headscale-metrics-dev.service=headscale-metrics"
- "traefik.http.routers.headscale-metrics-dev.tls=true"
headplane:
image: ghcr.io/tale/headplane:latest
container_name: headplane
restart: unless-stopped
ports:
- '13000:3000'
volumes:
- ./config/headplane.yaml:/etc/headplane/config.yaml
- ./config/headscale.yaml:/etc/headscale/config.yaml
- headplane-data:/var/lib/headplane
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxy
web:
image: goodieshq/headscale-admin:latest
restart: unless-stopped
ports:
- 10080:80
labels:
- "traefik.enable=true"
- "traefik.http.services.headscale-ui.loadbalancer.server.port=80"
# Prod Router
- "traefik.http.routers.headscale-ui.rule=Host(`hs.forust.xyz`) && PathPrefix(`/admin`)"
- "traefik.http.routers.headscale-ui.entrypoints=websecure"
- "traefik.http.routers.headscale-ui.middlewares=security-chain@file"
- "traefik.http.routers.headscale-ui.tls=true"
# Local Router
- "traefik.http.routers.headscale-ui-local.rule=Host(`hs.workstation.internal`) && PathPrefix(`/admin`)"
- "traefik.http.routers.headscale-ui-local.entrypoints=websecure"
- "traefik.http.routers.headscale-ui-local.tls=true"
# Dev Router
- "traefik.http.routers.headscale-ui-dev.rule=Host(`hs.gigaforust.internal`) && PathPrefix(`/admin`)"
- "traefik.http.routers.headscale-ui-dev.entrypoints=websecure"
- "traefik.http.routers.headscale-ui-dev.tls=true"
networks:
- proxy
volumes:
data:
headplane-data:
name: headplane_data
networks:
proxy:
external: true
-221
View File
@@ -1,221 +0,0 @@
# Configuration for the Headplane server and web application
server:
# These are the default values, change them as needed
host: "0.0.0.0"
port: 3000
# Should not include the dashboard prefix (/admin) portion.
# # Prod server_url
# base_url: https://hs.forust.xyz
# # Local base_url
# base_url: https://hs.workstation.internal
# # Dev base_url
# base_url: https://hs.gigaforust.internal
# You may provide `cookie_secret_path` instead to read a value from disk.
# See https://headplane.net/configuration/#sensitive-values
cookie_secret: "<change_me_to_something_secure!>"
# Whether cookies should be marked as Secure
# * Should be false if running without HTTPs
# * Should be true if running behind a reverse proxy with HTTPs
cookie_secure: true
# The maximum age of the session cookie in seconds
cookie_max_age: 86400 # 1 day in seconds
# This is not required, but if you want to restrict the cookie
# to a specific domain, set it here. Otherwise leave it commented out.
# This may not work as expected if not using a reverse proxy.
# cookie_domain: ""
# The path to persist Headplane specific data. All data going forward
# is stored in this directory, including the internal database and
# any cache related files.
data_path: "/var/lib/headplane"
# The info secret is optional and allows access to certain debug endpoints
# that may expose sensitive information about your Headplane instance.
#
# As of now, this protects the /api/info endpoint which exposes details about
# the Headplane and Headscale versions in use. In the future, more endpoints
# may be protected by this secret.
#
# If not set, these endpoints will be disabled.
# info_secret: "<change_me_to_something_secure!>"
# Headscale specific settings to allow Headplane to talk
# to Headscale and access deep integration features
headscale:
# The URL to your Headscale instance
# (All API requests are routed through this URL)
# (THIS IS NOT the gRPC endpoint, but the HTTP endpoint)
#
# IMPORTANT: If you are using TLS this MUST be set to `https://`
url: "http://headscale-server:8080"
# If you use the TLS configuration in Headscale, and you are not using
# Let's Encrypt for your certificate, pass in the path to the certificate.
# (This has no effect if `url` does not start with `https://`)
# tls_cert_path: "/var/lib/headplane/tls.crt"
# Optional, public URL if its different from the `headscale.url`
# This affects certain parts of the web UI which shows Headscale's URL
public_url: "https://headscale.example.com"
# Path to the Headscale configuration file
# This is optional, but HIGHLY recommended for the best experience
# If this is read only, Headplane will show your configuration settings
# in the Web UI, but they cannot be changed.
config_path: "/etc/headscale/config.yaml"
# Whether the Headscale configuration should be strictly validated
# when reading from `config_path`. If true, Headplane will not interact
# with Headscale if there are any issues with the configuration file.
#
# This is recommended to be true for production deployments to, however it
# may not work if you are using a version of Headscale that has configuration
# options unknown to Headplane.
config_strict: true
# If you are using `dns.extra_records_path` in your Headscale
# configuration, you need to set this to the path for Headplane
# to be able to read the DNS records.
#
# Pass it in if using Docker and ensure that the file is both
# readable and writable to the Headplane process.
# When using this, Headplane will no longer need to automatically
# restart Headscale for DNS record changes.
# dns_records_path: "/var/lib/headscale/extra_records.json"
# Integration configurations for Headplane to interact with Headscale
integration:
# The Headplane agent allows retrieving information about nodes
# This allows the UI to display version, OS, and connectivity data
# You will see the Headplane agent in your Tailnet as a node when
# it connects.
agent:
enabled: false
# To connect to your Tailnet, you need to generate a pre-auth key
# This can be done via the web UI or through the `headscale` CLI.
pre_authkey: "<your-preauth-key>"
# Optionally change the name of the agent in the Tailnet.
# host_name: "headplane-agent"
# Configure different caching settings. By default, the agent will store
# caches in the path below for a maximum of 1 minute. If you want data
# to update faster, reduce the TTL, but this will increase the frequency
# of requests to Headscale.
# cache_ttl: 60
# cache_path: /var/lib/headplane/agent_cache.json
# The work_dir represents where the agent will store its data to be able
# to automatically reauthenticate with your Tailnet. It needs to be
# writable by the user running the Headplane process.
#
# If using Docker, it is best to leave this as the default.
# work_dir: "/var/lib/headplane/agent"
# Only one of these should be enabled at a time or you will get errors
# This does not include the agent integration (above), which can be enabled
# at the same time as any of these and is recommended for the best experience.
docker:
enabled: true
# By default we check for the presence of a container label (see the docs)
# to determine the container to signal when changes are made to DNS settings.
container_label: "me.tale.headplane.target=headscale"
# HOWEVER, you can fallback to a container name if you desire, but this is
# not recommended as its brittle and doesn't work with orchestrators that
# automatically assign container names.
#
# If `container_name` is set, it will override any label checks.
# container_name: "headscale-server"
# The path to the Docker socket (do not change this if you are unsure)
# Docker socket paths must start with unix:// or tcp:// and at the moment
# https connections are not supported.
socket: "unix:///var/run/docker.sock"
# Please refer to docs/integration/Kubernetes.md for more information
# on how to configure the Kubernetes integration. There are requirements in
# order to allow Headscale to be controlled by Headplane in a cluster.
kubernetes:
enabled: false
# Validates the manifest for the Pod to ensure all of the criteria
# are set correctly. Turn this off if you are having issues with
# shareProcessNamespace not being validated correctly.
validate_manifest: true
# This should be the name of the Pod running Headscale and Headplane.
# If this isn't static you should be using the Kubernetes Downward API
# to set this value (refer to docs/Integrated-Mode.md for more info).
pod_name: "headscale"
# Proc is the "Native" integration that only works when Headscale and
# Headplane are running outside of a container. There is no configuration,
# but you need to ensure that the Headplane process can terminate the
# Headscale process.
#
# (If they are both running under systemd as sudo, this will work).
proc:
enabled: false
# OIDC Configuration for simpler authentication
# (This is optional, but recommended for the best experience)
# oidc:
# The OIDC issuer URL
# issuer: "https://accounts.google.com"
# If you are using OIDC, you need to generate an API key
# that can be used to authenticate other sessions when signing in.
#
# This can be done with `headscale apikeys create --expiration 999d`
# headscale_api_key: "<your-headscale-api-key>"
# If your OIDC provider does not support discovery (does not have the URL at
# `/.well-known/openid-configuration`), you need to manually set endpoints.
# This also works to override endpoints if you so desire or if your OIDC
# discovery is missing certain endpoints (ie GitHub).
# For some typical providers, see https://headplane.net/features/sso.
# authorization_endpoint: ""
# token_endpoint: ""
# userinfo_endpoint: ""
# The authentication method to use when communicating with the token endpoint.
# This is fully optional and Headplane will attempt to auto-detect the best
# method and fall back to `client_secret_basic` if unsure.
# token_endpoint_auth_method: "client_secret_post"
# The client ID for the OIDC client
# For the best experience please ensure this is *identical* to the client_id
# you are using for Headscale. because
# client_id: "your-client-id"
# The client secret for the OIDC client
# You may also provide `client_secret_path` instead to read a value from disk.
# See https://headplane.net/configuration/#sensitive-values
# client_secret: "<your-client-secret>"
# Whether to use PKCE when authenticating users. This is recommended as it
# adds an extra layer of security to the authentication process. Enabling this
# means your OIDC provider must support PKCE and it must be enabled on the
# client.
# use_pkce: true
# If you want to disable traditional login via Headscale API keys
# disable_api_key_login: false
# By default profile pictures are pulled from the OIDC provider when
# we go to fetch the userinfo endpoint. Optionally, this can be set to
# "oidc" or "gravatar" as of 0.6.1.
# profile_picture_source: "gravatar"
# The scopes to request when authenticating users. The default is below.
# scope: "openid email profile"
# Extra query parameters can be passed to the authorization endpoint
# by setting them here. This is useful for providers that require any kind
# of custom hinting.
# extra_params:
# prompt: "select_account" # Example: force account selection on Google
-79
View File
@@ -1,79 +0,0 @@
# https://wiki.serversatho.me/en/headscale
# # Prod server_url
# server_url: https://hs.example.com
# # Local server_url
# server_url: https://hs.internal_domain.internal
# # Dev server_url
# server_url: https://hs.dev_internal_domain.internal
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: false
noise:
private_key_path: /var/lib/headscale/noise_private.key
prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
allocation: sequential
derp:
server:
enabled: true
region_id: 999
region_code: "headscale"
region_name: "Headscale Embedded DERP"
stun_listen_addr: "0.0.0.0:3478"
private_key_path: /var/lib/headscale/derp_server_private.key
automatically_add_embedded_derp_region: true
ipv4: 1.2.3.4
ipv6: 2001:db8::1
urls:
- https://controlplane.tailscale.com/derpmap/default
paths: []
auto_update_enabled: true
update_frequency: 24h
disable_check_updates: false
ephemeral_node_inactivity_timeout: 30m
database:
type: sqlite
debug: false
gorm:
prepare_stmt: true
parameterized_queries: true
skip_err_record_not_found: true
slow_threshold: 1000
sqlite:
path: /var/lib/headscale/db.sqlite
write_ahead_log: true
wal_autocheckpoint: 1000
acme_url: https://acme-v02.api.letsencrypt.org/directory
acme_email: ""
tls_letsencrypt_hostname: ""
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
tls_letsencrypt_challenge_type: HTTP-01
tls_letsencrypt_listen: ":http"
tls_cert_path: ""
tls_key_path: ""
log:
format: text
level: info
policy:
mode: database
path: ""
dns:
magic_dns: true
base_domain: example.com
nameservers:
global:
- 1.1.1.1
- 1.0.0.1
- 2606:4700:4700::1111
- 2606:4700:4700::1001
split: {}
search_domains: []
extra_records: []
unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"
logtail:
enabled: false
randomize_client_port: false
-26
View File
@@ -1,26 +0,0 @@
{
"groups": {
"group:admin": [
"admin@"
],
"group:users": []
},
"tagOwners": {},
"hosts": {},
"acls": [
{
"#ha-meta": {
"name": "users",
"open": true
},
"action": "accept",
"src": [
"autogroup:member"
],
"dst": [
"autogroup:self:*"
]
}
],
"ssh": []
}
-59
View File
@@ -1,59 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: headscale-server-external
namespace: headscale
spec:
ports:
- port: 8080
targetPort: 18080
name: http
- port: 9090
targetPort: 19090
name: metrics
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: headscale-server-external
namespace: headscale
labels:
kubernetes.io/service-name: headscale-server-external
addressType: IPv4
ports:
- port: 18080
protocol: TCP
name: http
- port: 19090
protocol: TCP
name: metrics
endpoints:
- addresses:
- "192.168.88.100"
---
apiVersion: v1
kind: Service
metadata:
name: headscale-ui-external
namespace: headscale
spec:
ports:
- port: 80
targetPort: 10080
name: http
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: headscale-ui-external
namespace: headscale
labels:
kubernetes.io/service-name: headscale-ui-external
addressType: IPv4
ports:
- port: 10080
protocol: TCP
name: http
endpoints:
- addresses:
- "192.168.88.100"
-101
View File
@@ -1,101 +0,0 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: headscale-server-prod
namespace: headscale
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^hs\.forust\.xyz$`)
kind: Rule
services:
- name: headscale-server-external
port: 8080
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: headscale-server-local
namespace: headscale
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^hs\.(workstation|gigaforust)\.internal$`)
kind: Rule
services:
- name: headscale-server-external
port: 8080
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: headscale-ui-prod
namespace: headscale
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^hs\.forust\.xyz$`) && PathPrefix(`/admin`)
kind: Rule
middlewares:
- name: security-chain@file
services:
- name: headscale-ui-external
port: 80
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: headscale-ui-local
namespace: headscale
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^hs\.(workstation|gigaforust)\.internal$`) && PathPrefix(`/admin`)
kind: Rule
services:
- name: headscale-ui-external
port: 80
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: headscale-metrics-prod
namespace: headscale
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^hs\.forust\.xyz$`) && PathPrefix(`/metrics`)
kind: Rule
services:
- name: headscale-server-external
port: 9090
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: headscale-metrics-local
namespace: headscale
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^hs\.(workstation|gigaforust)\.internal$`) && PathPrefix(`/metrics`)
kind: Rule
services:
- name: headscale-server-external
port: 9090
-4
View File
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: headscale
+37 -17
View File
@@ -3,61 +3,81 @@ services:
build:
context: .
dockerfile: Dockerfile.forust
image: gcr.forust.xyz/forust/forust-homepage:latest
pull_policy: build
# ports:
# - "8085:80"
ports:
- "8085:80"
restart: unless-stopped
volumes:
- ./forust_files:/usr/share/nginx/html
networks:
- proxy
- traefik-proxy
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik-proxy"
# Services
- "traefik.http.services.forust-homepage.loadbalancer.server.port=80"
# Prod Router
- "traefik.http.routers.forust-homepage.rule=Host(`forust.xyz`) || Host(`www.forust.xyz`)"
- "traefik.http.routers.forust-homepage.rule=Host(`forust.xyz`)"
- "traefik.http.routers.forust-homepage.entrypoints=websecure"
- "traefik.http.routers.forust-homepage.middlewares=security-headers@file"
- "traefik.http.routers.forust-homepage.service=forust-homepage"
- "traefik.http.routers.forust-homepage.tls=true"
# Local Router
- "traefik.http.routers.forust-homepage-local.rule=Host(`landing.workstation.internal`)"
- "traefik.http.routers.forust-homepage-local.rule=Host(`landing.workstation.internal`) || Host(`landing.internal`)"
- "traefik.http.routers.forust-homepage-local.entrypoints=websecure"
- "traefik.http.routers.forust-homepage-local.middlewares=security-headers@file"
- "traefik.http.routers.forust-homepage-local.service=forust-homepage"
- "traefik.http.routers.forust-homepage-local.tls=true"
# Dev Router
- "traefik.http.routers.forust-homepage-dev.rule=Host(`landing.gigaforust.internal`)"
- "traefik.http.routers.forust-homepage-dev.entrypoints=websecure"
- "traefik.http.routers.forust-homepage-dev.middlewares=security-headers@file"
- "traefik.http.routers.forust-homepage-dev.service=forust-homepage"
- "traefik.http.routers.forust-homepage-dev.tls=true"
xdfnx:
build:
context: .
dockerfile: Dockerfile.xdfnx
image: gcr.forust.xyz/forust/xdfnx-homepage:latest
pull_policy: build
ports:
- "8086:80"
restart: unless-stopped
# ports:
# - "8086:80"
volumes:
- ./xdfnx_files:/usr/share/nginx/html
networks:
- traefik-proxy
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik-proxy"
# Services
- "traefik.http.services.xdfnx-homepage.loadbalancer.server.port=80"
# Prod Router
- "traefik.http.routers.xdfnx.rule=Host(`xdfnx.cfd`) || Host(`www.xdfnx.cfd`)"
- "traefik.http.routers.xdfnx.rule=Host(`xdfnx.cfd`)"
- "traefik.http.routers.xdfnx.entrypoints=websecure"
- "traefik.http.routers.xdfnx.tls.certresolver=letsencrypt"
- "traefik.http.routers.xdfnx.middlewares=security-headers@file"
- "traefik.http.routers.xdfnx.service=xdfnx-homepage"
- "traefik.http.routers.xdfnx.tls=true"
# Local Router
- "traefik.http.routers.xdfnx-local.rule=Host(`xdfnx.workstation.internal`)"
- "traefik.http.routers.xdfnx-local.rule=Host(`xdfnx.workstation.internal`) || Host(`xdfnx.internal`)"
- "traefik.http.routers.xdfnx-local.entrypoints=websecure"
- "traefik.http.routers.xdfnx-local.middlewares=security-headers@file"
- "traefik.http.routers.xdfnx-local.service=xdfnx-homepage"
- "traefik.http.routers.xdfnx-local.tls=true"
# Dev Router
- "traefik.http.routers.xdfnx-dev.rule=Host(`xdfnx.gigaforust.internal`)"
- "traefik.http.routers.xdfnx-dev.entrypoints=websecure"
- "traefik.http.routers.xdfnx-dev.middlewares=security-headers@file"
- "traefik.http.routers.xdfnx-dev.service=xdfnx-homepage"
- "traefik.http.routers.xdfnx-dev.tls=true"
networks:
- proxy
networks:
proxy:
traefik-proxy:
external: true
+11 -1
View File
@@ -134,6 +134,7 @@ ul {
background-size: cover;
}
/* if no avatar added: */
.placeholder::before {
content: "?";
@@ -155,9 +156,10 @@ footer {
text-align: center;
color: var(--dim);
font-size: 0.8rem;
/* flag{why-are-you-here?} */
/* flag{why-are-you-here?} */
margin-top: 4rem;
}
/* SMTH RESPONSIVE */
@media (max-width: 600px) {
.grid-2 {
@@ -165,3 +167,11 @@ footer {
gap: 0;
}
}
/* nya~ */
.birthday {
color: var(--dim);
font-size: 0.75rem;
margin-top: 4px;
opacity: 0.75;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

+13 -23
View File
@@ -41,22 +41,7 @@
</li>
<li>
<i class="fas fa-envelope"></i>
<a href="mailto:contact@forust.xyz">mail/contact@forust.xyz</a>
</li>
<li>
<i class="fa fa-pie-chart"></i>
<a href="https://forust.xyz/glance">forust/dashboard</a>
</li>
<li>
<i class="fa fa-refresh"></i>
<a href="https://forust.xyz/convert">forust/converter</a>
</li>
<li>
<i class="fa-solid fa-key"></i>
<a href=".well-known/pgp-key.asc">security/PGP Key</a>
</li>
<li>
<p class="comment"># PGP Key Fingerprint: A777 7CB7 D9C4 0A97 443D CCF0 7A3D A455 F820 5B82</p>
<a href="mailto:forust@forust.xyz">forust@forust.xyz</a>
</li>
</ul>
</section>
@@ -69,12 +54,6 @@
<span>ArchLinux # btw</span>
<span class="level">[#######...]</span>
</div>
<div class="skill-item">
<span>Kubernetes</span> <span class="level">[###.......]</span>
</div>
<div class="skill-item">
<span>Docker</span> <span class="level">[####......]</span>
</div>
<div class="skill-item">
<span>Docker Compose</span> <span class="level">[#####.....]</span>
</div>
@@ -84,6 +63,9 @@
<div class="skill-item">
<span>Burpsuite</span> <span class="level">[#####.....]</span>
</div>
<div class="skill-item">
<span>Docker</span> <span class="level">[####......]</span>
</div>
<div class="skill-item">
<span>Steganography</span> <span class="level">[####......]</span>
</div>
@@ -127,7 +109,7 @@
<div class="avatar"
style="background-image: url('assets/images/team/anna.jpg'); background-size: cover; background-position: center;">
</div>
<a href="./assets/images/love.png" target="_blank">Anna~</a>
<a href="" target="_blank">Anna~</a>
</div>
<div class="member">
@@ -137,6 +119,14 @@
<a href="https://chernuha.space" target="_blank">Chernuha</a>
</div>
<div class="member">
<div class="avatar"
style="background-image: url('assets/images/team/vv.jpg'); background-size: cover; background-position: center;">
</div>
<a href="./miku.html" target="_blank">vv</a>
<p class="birthday">Happy Birthday, darling ♡</p>
</div>
<div class="member">
<div class="avatar"
style="background-image: url('assets/images/team/hudan.jpg'); background-size: cover; background-position: center;">
-79
View File
@@ -1,79 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: forust-homepage-service
namespace: homepages
spec:
selector:
app: forust-homepage
ports:
- port: 80
targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: forust-homepage-deployment
namespace: homepages
spec:
replicas: 1
selector:
matchLabels:
app: forust-homepage
template:
metadata:
labels:
app: forust-homepage
spec:
containers:
- name: forust-homepage
image: gcr.forust.xyz/forust/forust-homepage:latest
ports:
- containerPort: 80
resources:
requests:
memory: "10Mi"
cpu: "20m"
limits:
memory: "100Mi"
cpu: "50m"
---
apiVersion: v1
kind: Service
metadata:
name: xdfnx-homepage-service
namespace: homepages
spec:
selector:
app: xdfnx-homepage
ports:
- port: 80
targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: xdfnx-homepage-deployment
namespace: homepages
spec:
replicas: 1
selector:
matchLabels:
app: xdfnx-homepage
template:
metadata:
labels:
app: xdfnx-homepage
spec:
containers:
- name: xdfnx-homepage
image: gcr.forust.xyz/forust/xdfnx-homepage:latest
ports:
- containerPort: 80
resources:
requests:
memory: "10Mi"
cpu: "20m"
limits:
memory: "100Mi"
cpu: "50m"
-64
View File
@@ -1,64 +0,0 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: forust-homepage-prod
namespace: homepages
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^(forust\.xyz|www\.forust\.xyz)$`)
kind: Rule
services:
- name: forust-homepage-service
port: 80
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: forust-homepage-local
namespace: homepages
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^landing\.(workstation|gigaforust)\.internal$`)
kind: Rule
services:
- name: forust-homepage-service
port: 80
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: xdfnx-homepage-prod
namespace: homepages
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^(xdfnx\.cfd|www\.xdfnx\.cfd)$`)
kind: Rule
services:
- name: xdfnx-homepage-service
port: 80
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: xdfnx-homepage-local
namespace: homepages
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^xdfnx\.(workstation|gigaforust)\.internal$`)
kind: Rule
services:
- name: xdfnx-homepage-service
port: 80
-4
View File
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: homepages
Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

-54
View File
@@ -1,54 +0,0 @@
# Resolved: Overlay FS failure (and so containers)
15-12-2025 03:02 AM EET: Degraded control panels' performances, following by full cascade docker failure
15-12-2025 04:36 AM EET: Identified: Services are terminated due to server software (Overlay FS) + hardware issues (HDD).
15-12-2025 08:45 PM EET: Restored NextCloud service with few tweaks to lower I/O
---
16-12-2025 08:34 PM EET: Ordered new HDD, ETA 22nd of December - 2nd of January
---
17-12-2025 02:47 AM EET: To avoid additional I/O into kuma's database, disabled uptime monitoring for non-critical services, such as:
- Game servers
- Gitea (no public projects being hosted yet)
- Landings
- Cloud services
- PenPot
- Auth provider
- Secondary management tools
- Chernuha's non-important infrastructure
These can be identified by seeing ">2m ago" under monitor's heartbeats.
---
09-01-2026 02:32 PM EET: NextCloud's frontend files are corrupted due to the unknown issue. All user data is integrity-verified. To prevent user data corruption, NextCloud service will be restored after new hardware will be available.
---
14-01-2026 08:42 PM EET: After planned updating and restarting server, critical firmware software were corrupted because of physical degradation of the disk. Server inaccessible in any way
---
15-01-2026 11:30 AM EET: A new NAS-Grade HDD (Seagate IronWolf Pro) was ordered. ETA 16-01-2026 EET Before 12:00 PM
---
16-01-2026 12:47 AM EET: New server system is installed, data backed up. Experiencing docker memory leak.
---
17-01-2026 01:27 PM EET: All services except NextCloud and Satisfactory server are online.
17-01-2026 03:48 PM EET: Nextcloud is online. Satisfactory will be provided on-demand. Monitoring status
---
##### Status: All services are online
**Solution: moving all infrastructure onto new NAS-Grade HDD with fresh OS install**
-3
View File
@@ -1,3 +0,0 @@
KENER_SECRET_KEY=your_secret_key_here
ORIGIN=http://localhost:3000
TZ=Etc/UTC
-59
View File
@@ -1,59 +0,0 @@
services:
kener:
image: rajnandan1/kener:4.0.23
container_name: kener
restart: unless-stopped
# ports:
# - 3000:3000/tcp
environment:
- KENER_SECRET_KEY=${KENER_SECRET_KEY?Kener requires a secret key}
- ORIGIN=${ORIGIN:-http://localhost:3000}
- REDIS_URL=redis://redis:6379
- TZ:${TZ:-Etc/UTC}
depends_on:
redis:
condition: service_healthy
volumes:
- db:/app/database
- uploads:/app/uploads
labels:
- "traefik.enable=true"
- "traefik.http.services.kener.loadbalancer.server.port=3000"
# Prod Router
- "traefik.http.routers.kener.rule=Host(`status.forust.xyz`)"
- "traefik.http.routers.kener.entrypoints=websecure"
- "traefik.http.routers.kener.tls=true"
# Local Router
- "traefik.http.routers.kener-local.rule=Host(`status.workstation.internal`)"
- "traefik.http.routers.kener-local.entrypoints=websecure"
- "traefik.http.routers.kener-local.tls=true"
# Dev Router
- "traefik.http.routers.kener-dev.rule=Host(`status.gigaforust.internal`)"
- "traefik.http.routers.kener-dev.entrypoints=websecure"
- "traefik.http.routers.kener-dev.tls=true"
networks:
- proxy
- kener
redis:
image: redis:7-alpine
container_name: kener-redis
restart: unless-stopped
volumes:
- redis:/data
healthcheck:
test: [ "CMD", "redis-cli", "ping" ]
interval: 6s
timeout: 5s
retries: 5
networks:
- kener
volumes:
db:
uploads:
redis:
networks:
proxy:
external: true
kener:
external: false
-9
View File
@@ -1,9 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: kener-config
namespace: kener
data:
ORIGIN: "https://status.forust.xyz"
REDIS_URL: "redis://kener-redis-service:6379"
TZ: "Etc/UTC"
-32
View File
@@ -1,32 +0,0 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: kener-prod
namespace: kener
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^status\.forust\.xyz$`)
kind: Rule
services:
- name: kener-service
port: 3000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: kener-local
namespace: kener
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^status\.(workstation|gigaforust)\.internal$`)
kind: Rule
services:
- name: kener-service
port: 3000
-80
View File
@@ -1,80 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: kener-service
namespace: kener
spec:
selector:
app: kener
ports:
- port: 3000
targetPort: 3000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kener-deployment
namespace: kener
spec:
replicas: 1
selector:
matchLabels:
app: kener
template:
metadata:
labels:
app: kener
spec:
containers:
- name: kener
image: rajnandan1/kener:4.0.23
envFrom:
- configMapRef:
name: kener-config
- secretRef:
name: kener-secrets
resources:
requests:
memory: "300Mi"
cpu: "350m"
limits:
memory: "2Gi"
cpu: "1"
ports:
- containerPort: 3000
volumeMounts:
- name: kener-db
mountPath: /app/database
- name: kener-uploads
mountPath: /app/uploads
volumes:
- name: kener-db
persistentVolumeClaim:
claimName: kener-db-pvc
- name: kener-uploads
persistentVolumeClaim:
claimName: kener-uploads-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: kener-db-pvc
namespace: kener
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: kener-uploads-pvc
namespace: kener
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
-4
View File
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: kener
-46
View File
@@ -1,46 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: kener-redis-service
namespace: kener
spec:
clusterIP: None
selector:
app: kener-redis
ports:
- name: redis
port: 6379
targetPort: 6379
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: kener-redis
namespace: kener
spec:
serviceName: kener-redis-service
replicas: 1
selector:
matchLabels:
app: kener-redis
template:
metadata:
labels:
app: kener-redis
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
volumeMounts:
- name: redis-data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: redis-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
-8
View File
@@ -1,8 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: kener-secrets
namespace: kener
type: Opaque
stringData:
KENER_SECRET_KEY: ""
+15 -6
View File
@@ -1,7 +1,7 @@
services:
metube:
image: ghcr.io/alexta69/metube
container_name: metube
# container_name: metube
restart: unless-stopped
# ports:
# - "8081:8081"
@@ -13,24 +13,33 @@ services:
volumes:
- ./MeTube_downloads:/downloads
labels:
- "traefik.enable=true"
- "traefik.http.services.metube.loadbalancer.server.port=8081"
- traefik.enable=true
- "traefik.docker.network=traefik-proxy"
# Prod Router
- "traefik.http.routers.metube.rule=Host(`metube.forust.xyz`)"
- "traefik.http.routers.metube.entrypoints=websecure"
- "traefik.http.routers.metube.middlewares=security-chain@file"
- "traefik.http.routers.metube.service=metube"
- "traefik.http.routers.metube.tls=true"
- "traefik.http.services.metube.loadbalancer.server.port=8081"
# Local Router
- "traefik.http.routers.metube-local.rule=Host(`metube.workstation.internal`)"
- "traefik.http.routers.metube-local.rule=Host(`metube.workstation.internal`) || Host(`metube.internal`)"
- "traefik.http.routers.metube-local.entrypoints=websecure"
- "traefik.http.routers.metube-local.middlewares=security-headers@file"
- "traefik.http.routers.metube-local.service=metube"
- "traefik.http.routers.metube-local.tls=true"
# Dev Router
- "traefik.http.routers.metube-dev.rule=Host(`metube.gigaforust.internal`)"
- "traefik.http.routers.metube-dev.entrypoints=websecure"
- "traefik.http.routers.metube-dev.middlewares=security-chain@file"
- "traefik.http.routers.metube-dev.service=metube"
- "traefik.http.routers.metube-dev.tls=true"
networks:
- proxy
- traefik-proxy
networks:
proxy:
traefik-proxy:
external: true
-11
View File
@@ -1,11 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: metube-config
namespace: metube
data:
DOWNLOAD_MODE: "limited"
MAX_CONCURRENT_DOWNLOADS: "3"
DELETE_FILE_ON_TRASHCAN: "true"
DEFAULT_OPTION_PLAYLIST_STRICT_MODE: "true"
CLEAR_COMPLETED_AFTER: "600" # 10 min

Some files were not shown because too many files have changed in this diff Show More