Port to NixOS: replace Helm chart with flake-based NixOS config

Replaces the Helm/k3s setup with a declarative NixOS configuration targeting
a Raspberry Pi 4. Services run as podman containers under systemd, with data
on an external HD at /mnt/data. Key components:

- flake.nix: multi-host flake with pi-main (aarch64) and a placeholder for a
  second machine
- modules/common.nix: shared system config (nix, podman, sops, SSH)
- modules/storage.nix: external HD mount with per-service subdirs
- modules/caddy.nix: Caddy with cloudflare DNS-01 ACME + authelia forward_auth
- modules/cloudflared.nix: Cloudflare tunnel for remote access
- modules/backup.nix: restic daily backups with NC maintenance mode pre-hook
- modules/services/{openldap,authelia,gitea,nextcloud,phpldapadmin}.nix: core services
- modules/services/{jellyfin,transmission}.nix: media services (disabled by default)
- secrets/: sops-nix scaffold with .sops.yaml age key config
- hosts/pi-main/: hardware config + service selection for the Pi
- PORTING.md: step-by-step migration guide (SD card → data restore → verify)
This commit is contained in:
Aner Zakobar
2026-04-15 17:18:12 +03:00
parent d1948df47e
commit 2f0d0b5e4c
59 changed files with 2173 additions and 4666 deletions
-6
View File
@@ -1,6 +0,0 @@
apiVersion: v2
name: homey
description: Deploy a fancy home environment!
type: application
version: 0.1.0
appVersion: "1.16.0"
+400
View File
@@ -0,0 +1,400 @@
# Porting Guide — Helm/k3s → NixOS
This document walks through setting up the new NixOS-based home server from
scratch on a Raspberry Pi 4, restoring data from old Longhorn volumes, and
verifying each service.
---
## Prerequisites (on your workstation)
- `nix` with flakes enabled (`~/.config/nix/nix.conf`: `experimental-features = nix-command flakes`)
- `sops` + `age` CLI tools (`nix-shell -p sops age`)
- An SSH key pair
---
## Phase 0 — Secrets
### 0.1 Generate your age key (workstation)
```bash
age-keygen -o ~/.config/sops/age/keys.txt
age-keygen -y ~/.config/sops/age/keys.txt # print public key
```
You will add the Pi's public key in step 2.2; for now add your workstation
public key so you can edit the secrets file offline.
Edit `secrets/.sops.yaml`, replace the placeholder with your workstation pubkey:
```yaml
- age:
- AGE-PUBLIC-KEY-YOUR-WORKSTATION # ← paste here
```
### 0.2 Fill in secrets.yaml
`secrets/secrets.yaml` is a **plaintext template** — do not commit it until
encrypted. Fill in all values:
| Key | Source |
|-----|--------|
| `openldap/admin_password` | From old k8s secret `openldap-admin` |
| `openldap/config_password` | From old k8s secret `openldap-config` |
| `openldap/ro_password` | From old k8s secret `openldap-ro` |
| `gitea/admin_password` | From old k8s secret `gitea-admin-pass` |
| `nextcloud/admin_password` | From old k8s secret `nextcloud-admin-pass` |
| `nextcloud/postgres_password` | From old k8s secret `nextcloud-postgres-pass` |
| `authelia/jwt_secret` | Generate: `openssl rand -hex 64` |
| `authelia/session_secret` | Generate: `openssl rand -hex 64` |
| `authelia/storage_encryption_key` | Generate: `openssl rand -hex 64` |
| `gitea/lfs_jwt_secret` | Generate: `openssl rand -base64 32 \| tr -d '='` |
| `gitea/oauth2_jwt_secret` | Generate: `openssl rand -base64 32 \| tr -d '='` |
| `gitea/internal_token` | Generate: `openssl rand -base64 75 \| tr -d '\n='` |
| `cloudflare/api_token` | Cloudflare dashboard → API Tokens → DNS:Edit |
| `cloudflare/tunnel_token` | Created in Phase 3 (Cloudflare setup) |
| `restic/password` | Generate: `openssl rand -base64 32` |
To get old k8s secrets (if the cluster is still running):
```bash
kubectl get secret openldap-admin -n homey -o jsonpath='{.data.password}' | base64 -d
kubectl get secret openldap-config -n homey -o jsonpath='{.data.password}' | base64 -d
kubectl get secret openldap-ro -n homey -o jsonpath='{.data.password}' | base64 -d
kubectl get secret gitea-admin-pass -n homey -o jsonpath='{.data.password}' | base64 -d
kubectl get secret nextcloud-admin-pass -n homey -o jsonpath='{.data.password}' | base64 -d
kubectl get secret nextcloud-postgres-pass -n homey -o jsonpath='{.data.password}' | base64 -d
```
### 0.3 Encrypt secrets.yaml (workstation, before committing)
```bash
sops --encrypt --in-place secrets/secrets.yaml
git add secrets/secrets.yaml secrets/.sops.yaml
```
---
## Phase 1 — Install NixOS on the Raspberry Pi 4
### 1.1 Flash the SD card
Download the NixOS aarch64 SD card image:
```
https://nixos.org/download#nixos-iso
→ "Raspberry Pi (aarch64) SD card image"
```
Flash with:
```bash
# macOS
sudo dd if=nixos-*-aarch64-linux.img of=/dev/rdiskN bs=4m status=progress
# Linux
sudo dd if=nixos-*-aarch64-linux.img of=/dev/sdX bs=4M status=progress conv=fsync
```
Label the partitions to match `hardware.nix`:
```bash
# After flashing, mount the root partition and relabel if needed:
sudo e2label /dev/sdX2 NIXOS_SD
sudo fatlabel /dev/sdX1 FIRMWARE
```
Boot the Pi from the SD card. You should get a serial console or HDMI output.
### 1.2 Initial network setup
On the Pi (serial or HDMI):
```bash
# Find your IP
ip addr
# Set a temporary password for nixos user to SSH in
passwd nixos
```
From your workstation:
```bash
ssh nixos@<pi-ip>
```
### 1.3 Copy the flake to the Pi
```bash
# From your workstation (repo root)
rsync -avz --exclude='.git' . nixos@<pi-ip>:/tmp/homey/
```
### 1.4 Generate the Pi's age key
```bash
# On the Pi
nix-shell -p age --run 'age-keygen -o /tmp/pi-age-key.txt'
age-keygen -y /tmp/pi-age-key.txt # print public key
```
Copy the public key back to your workstation. Add it to `secrets/.sops.yaml`:
```yaml
- age:
- AGE-PUBLIC-KEY-YOUR-WORKSTATION
- AGE-PUBLIC-KEY-PI-MAIN-REPLACE-ME # ← paste Pi's public key here
```
Re-encrypt secrets so the Pi can decrypt them:
```bash
# On workstation
sops updatekeys secrets/secrets.yaml
git add secrets/.sops.yaml secrets/secrets.yaml
git commit -m "Add Pi age key"
# Copy updated files to Pi
rsync -avz secrets/ nixos@<pi-ip>:/tmp/homey/secrets/
```
Place the Pi's private key where sops-nix expects it:
```bash
# On the Pi
sudo mkdir -p /var/lib/sops-nix
sudo cp /tmp/pi-age-key.txt /var/lib/sops-nix/key.txt
sudo chmod 600 /var/lib/sops-nix/key.txt
```
### 1.5 Configure host-specific settings
Edit `hosts/pi-main/default.nix` on the Pi (or on workstation first):
1. Set your SSH public key in `users.users.admin.openssh.authorizedKeys.keys`
2. Set `homey.storage.device` to your USB drive:
```bash
ls -la /dev/disk/by-id/ | grep -v part
```
3. Set `homey.backup.repository` to your backup destination
Edit `hosts/pi-main/hardware.nix` if the disk labels differ from defaults.
### 1.6 Install NixOS
```bash
# On the Pi
sudo nixos-install --flake /tmp/homey#pi-main --no-root-passwd
# Reboot
sudo reboot
```
After reboot, SSH in with your admin key:
```bash
ssh admin@<pi-ip>
```
---
## Phase 2 — Restore Data from Old Volumes
Mount the external HD (if not auto-mounted):
```bash
sudo mount /dev/disk/by-id/<your-drive-id> /mnt/data
```
Copy data from the old Longhorn volume backups into the new layout:
```bash
# Adjust source paths to wherever your Longhorn volume dumps are
BACKUP_SRC=/path/to/longhorn/backups
# OpenLDAP
sudo rsync -av $BACKUP_SRC/openldap/etc-ldap-slapd.d/ /mnt/data/openldap/etc-ldap-slapd.d/
sudo rsync -av $BACKUP_SRC/openldap/var-lib-ldap/ /mnt/data/openldap/var-lib-ldap/
# Gitea
sudo rsync -av $BACKUP_SRC/gitea/data/ /mnt/data/gitea/data/
# Nextcloud
sudo rsync -av $BACKUP_SRC/nextcloud/html/ /mnt/data/nextcloud/html/
# Restore postgres from pg_dump if available, otherwise restore the data dir:
sudo rsync -av $BACKUP_SRC/nextcloud/db/ /mnt/data/nextcloud/db/
```
Fix ownership (containers run as UID 1000 or root depending on image):
```bash
# openldap runs as root inside the container
sudo chown -R root:root /mnt/data/openldap/
# gitea runs as git (UID 1000)
sudo chown -R 1000:1000 /mnt/data/gitea/
# nextcloud runs as www-data (UID 33)
sudo chown -R 33:33 /mnt/data/nextcloud/html/
# postgres data owned by postgres (UID 999 in the postgres image)
sudo chown -R 999:999 /mnt/data/nextcloud/db/
```
---
## Phase 3 — Cloudflare Tunnel Setup
### 3.1 Create the tunnel in Cloudflare Zero Trust
1. Go to [https://one.dash.cloudflare.com](https://one.dash.cloudflare.com) → Networks → Tunnels
2. Click "Create a tunnel" → Cloudflared → Name it `pi-main`
3. Copy the tunnel token (long string starting with `eyJ...`)
4. Add it to `secrets/secrets.yaml` under `cloudflare/tunnel_token`
5. Re-encrypt: `sops secrets/secrets.yaml` (the file opens in `$EDITOR`)
### 3.2 Configure public hostnames in the Cloudflare dashboard
In the tunnel's "Public Hostnames" tab, add:
| Subdomain | Domain | Service |
|-----------|--------|---------|
| `auth` | `home.zakobar.com` | `https://localhost:443` |
| `git` | `home.zakobar.com` | `https://localhost:443` |
| `nextcloud` | `home.zakobar.com` | `https://localhost:443` |
| `ldapadmin` | `home.zakobar.com` | `https://localhost:443` |
| `jellyfin` | `home.zakobar.com` | `https://localhost:443` |
| `torrent` | `home.zakobar.com` | `https://localhost:443` |
For each entry, under "Additional settings" → TLS → **No TLS Verify: ON**
(because cloudflared connects to `localhost` but the cert is for the real hostname).
### 3.3 Update DNS in Cloudflare
Add a CNAME for `home.zakobar.com` pointing to your tunnel's UUID (Cloudflare
creates this automatically when you add hostnames). You do not need to add
`home.zakobar.com` to your domain's A records — Cloudflare handles it.
---
## Phase 4 — Rebuild and Verify
After restoring data and completing Cloudflare setup, apply the final config:
```bash
# On the Pi
sudo nixos-rebuild switch --flake /path/to/homey#pi-main
```
### Verification checklist
```bash
# All container services running?
systemctl list-units 'podman-*' --state=active
# OpenLDAP responding?
ldapsearch -x -H ldap://127.0.0.1:389 -b dc=home,dc=zakobar,dc=com -D "cn=admin,dc=home,dc=zakobar,dc=com" -W
# Authelia health?
curl -s http://localhost:9091/api/health | python3 -m json.tool
# Caddy serving TLS?
curl -I https://auth.home.zakobar.com
# Gitea login?
# Visit https://git.home.zakobar.com — should redirect to authelia if not logged in
# Nextcloud?
# Visit https://nextcloud.home.zakobar.com
# Cloudflare tunnel connected?
systemctl status cloudflared-tunnel-pi-main
```
---
## Phase 5 — Local DNS (optional but recommended)
To access services without going through Cloudflare on the LAN, add these
records to your router's DNS or Pi-hole:
```
192.168.1.100 home.zakobar.com
192.168.1.100 auth.home.zakobar.com
192.168.1.100 git.home.zakobar.com
192.168.1.100 nextcloud.home.zakobar.com
192.168.1.100 ldapadmin.home.zakobar.com
192.168.1.100 jellyfin.home.zakobar.com
192.168.1.100 torrent.home.zakobar.com
```
Replace `192.168.1.100` with your Pi's actual LAN IP.
---
## Day-to-day Operations
### Apply config changes
```bash
sudo nixos-rebuild switch --flake /path/to/homey#pi-main
```
### Edit secrets
```bash
sops secrets/secrets.yaml
# Save and exit — sops re-encrypts automatically
# Then copy to Pi and rebuild
```
### Browse service data on disk
```bash
ls /mnt/data/
ls /mnt/data/gitea/data/
# No special tools needed — plain filesystem
```
### Trigger a manual backup
```bash
sudo systemctl start restic-backups-homey.service
```
### List backup snapshots
```bash
sudo restic -r <your-repo-url> \
--password-file /run/secrets/restic_password \
snapshots
```
### Restore a single service from backup
```bash
sudo systemctl stop podman-gitea.service
sudo restic -r <repo> restore latest \
--target / \
--include /mnt/data/gitea
sudo systemctl start podman-gitea.service
```
---
## Adding a Second Machine (future)
1. Create `hosts/pi-secondary/default.nix` and `hardware.nix`
2. Enable the services you want on that machine
3. Services communicating cross-machine: reference the primary Pi's LAN IP or
hostname directly in environment variables (e.g. point gitea's LDAP config
at `192.168.1.100:389` rather than `127.0.0.1:389`).
4. Add the new host to `flake.nix`:
```nix
pi-secondary = mkHost {
system = "x86_64-linux";
hostPath = ./hosts/pi-secondary/default.nix;
};
```
5. Generate an age key on the new machine and add it to `.sops.yaml`.
-115
View File
@@ -1,115 +0,0 @@
#+title: Homey
A home environment for everyone!
* Installation
Install using
#+begin_src bash
helm upgrade --install homey . -n homey
#+end_src
* Backing up
We must find a better solution
https://perfectmediaserver.com/day-two/top10apps.html
Nefarious
* LDAP Configuration
Logins are done to PHPLDAPADMIN
DN is like:
cn=admin,dc=home,dc=,dc=io
get-secret-val.sh homey openldap-admin password
First thing we do is create an organization unit called users
To add a new user, we create a child entry to ou=users
It has to be of type inetOrgPerson
cn = Common Name, sn = Sur Name.
Select RDN = User Name (uid) (FROM DROP DOWN MENU)
UID = USERNAME, that is what is important. (In PHPLdapAdmin it is under User Name)
Now we may continue!
* GITEA
Site Title: whatever
SSH Server Domain: git.<YOUR URL>
SSH Server Port: 2222
Gitea Base URL: http://git.<YOUR URL>
Then add Administrator Account Settings:
Administrator Username: gitea-admin
Password: from gitea-admin-pass
Email address must be populated
That will work after a few minutes.
Now we go into Authentication Sources
Add a new LDAP Authentication source
Authentication name: Home LDAP
Host: openldap
Port: 389
Bind DN = cn=readonly,dc=home,dc=,dc=io
Bind Password: openldap-ro password
User Search Base: ou=users,dc=home,dc=,dc=io
user search filter = (uid=%s)
Admin filter (title=admin)
Username Attribute: uid
First Name Attribute: cn
Surname Attribute: sn
Email Attribute: mail
* AUTHELIA
https://github.com/authelia/authelia/blob/57d5fbd3f5c82e83296023dc1de6e4f5ff063c00/examples/compose/lite/authelia/configuration.yml
This fucking sucks
https://gist.github.com/james-d-elliott/5152d27c0781aee856a3383f1284998e
* EVERYTHING
https://www.talkingquickly.co.uk/gitea-sso-with-keycloak-openldap-openid-connect
* DRONE AND GITEA
?
https://dev.to/ruanbekker/self-hosted-cicd-with-gitea-and-drone-ci-200l
* DAV
https://gitlab.com/davical-project/davical/-/blob/master/config/example-config.php
Line 800 ish for auth from reverse proxy
* NEXTCLOUD
I ran THIS command inside
su www-data -s /bin/bash -c php occ ldap:promote-group "admins"
** When maintenence mode
#+begin_example
kubectl exec --tty --stdin -n homey deploy/nextcloud -- su -l www-data -s /bin/bash
php /var/www/html/occ maintenance:mode --off
#+end_src
* I UNDERSTAND
I need to backup Chen's stuff
And... I need to Jellyfin
* PAPERLESS
https://github.com/paperless-ngx/paperless-ngx/blob/74c44fe418a91a526b5dab1a91fde4aaebd28bb1/docker/compose/docker-compose.postgres.yml
For docker
-60
View File
@@ -1,60 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="$HOME/homey-backup"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
NODE="root@192.168.1.100"
NC_DATA_PVC="pvc-5c1f48e3-346f-4c35-8e6a-8fc0c4c3a842-96d72815"
NC_DB_PVC="pvc-c5b28179-1b9c-462a-be5b-05c4f0bb36ca-5f2dbf4d"
NC_DATA_DEST="$BACKUP_DIR/longhorn-nextcloud-data/$TIMESTAMP"
NC_DB_DEST="$BACKUP_DIR/longhorn-nextcloud-db/$TIMESTAMP"
echo "=== Longhorn Volume Backup (Emergency) ==="
echo "Started: $(date)"
echo ""
echo "WARNING: Backing up raw Longhorn volume images"
echo "These are sparse files - actual data is smaller than file size"
echo ""
mkdir -p "$NC_DATA_DEST"
mkdir -p "$NC_DB_DEST"
echo "--- Backing up Nextcloud data volume ---"
echo "Source: $NODE:/hda/replicas/$NC_DATA_PVC/"
echo "Dest: $NC_DATA_DEST/"
echo ""
rsync -avzP --no-owner --no-group --sparse \
"$NODE:/hda/replicas/$NC_DATA_PVC/" \
"$NC_DATA_DEST/"
echo ""
echo "Nextcloud volume backup: $(du -sh "$NC_DATA_DEST" | cut -f1)"
echo ""
echo "--- Backing up PostgreSQL volume ---"
echo "Source: $NODE:/hda/replicas/$NC_DB_PVC/"
echo "Dest: $NC_DB_DEST/"
echo ""
rsync -avzP --no-owner --no-group --sparse \
"$NODE:/hda/replicas/$NC_DB_PVC/" \
"$NC_DB_DEST/"
echo ""
echo "PostgreSQL volume backup: $(du -sh "$NC_DB_DEST" | cut -f1)"
echo ""
echo "=== Backup Complete ==="
echo "Timestamp: $TIMESTAMP"
echo "Finished: $(date)"
echo ""
echo "Total backup size:"
du -sh "$BACKUP_DIR/longhorn-nextcloud-data/$TIMESTAMP" "$BACKUP_DIR/longhorn-nextcloud-db/$TIMESTAMP"
echo ""
echo "NOTE: These are raw Longhorn volume images."
echo "To restore, copy back to /hda/replicas/ and restart Longhorn."
-120
View File
@@ -1,120 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="$HOME/homey-backup"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
NAMESPACE="homey"
NODE="root@192.168.1.100"
NC_DATA_DEST="$BACKUP_DIR/nextcloud-data/backups/$TIMESTAMP"
NC_DB_DEST="$BACKUP_DIR/nextcloud-postgres/backups/$TIMESTAMP"
show_progress() {
local file="$1"
local label="$2"
local total="${3:-0}"
while [[ ! -f "$file" ]]; do
sleep 0.2
done
while kill -0 "$BACKUP_PID" 2>/dev/null; do
local size=$(stat -c%s "$file" 2>/dev/null || echo "0")
if [[ "$total" -gt 0 ]]; then
local pct=$((size * 100 / total))
local size_hr=$(numfmt --to=iec-i --suffix=B "$size" 2>/dev/null || echo "${size}B")
local total_hr=$(numfmt --to=iec-i --suffix=B "$total" 2>/dev/null || echo "${total}B")
printf "\r%s: %s / %s (%d%%) " "$label" "$size_hr" "$total_hr" "$pct"
else
local size_hr=$(numfmt --to=iec-i --suffix=B "$size" 2>/dev/null || echo "${size}B")
printf "\r%s: %s " "$label" "$size_hr"
fi
sleep 0.5
done
local final=$(stat -c%s "$file" 2>/dev/null || echo "0")
local final_hr=$(numfmt --to=iec-i --suffix=B "$final" 2>/dev/null || echo "${final}B")
printf "\r%s: %s - done! \n" "$label" "$final_hr"
}
wait_for_cluster() {
echo "Waiting for Kubernetes cluster..."
local max_wait=300
local start=$(date +%s)
while true; do
if kubectl get nodes &>/dev/null; then
echo "Cluster ready!"
return 0
fi
local now=$(date +%s)
if [[ $((now - start)) -ge $max_wait ]]; then
echo "ERROR: Cluster not available after ${max_wait}s"
return 1
fi
printf "\rWaiting... %ds " $((now - start))
sleep 5
done
}
echo "=== Nextcloud Backup ==="
echo "Started: $(date)"
mkdir -p "$NC_DATA_DEST"
mkdir -p "$NC_DB_DEST"
if ! wait_for_cluster; then
echo "Cluster unavailable. Cannot proceed."
exit 1
fi
echo ""
echo "--- Backing up PostgreSQL ---"
DB_POD=$(kubectl get pods -n "$NAMESPACE" -l app=nextcloud-postgres -o jsonpath='{.items[0].metadata.name}')
if [[ -z "$DB_POD" ]]; then
echo "ERROR: PostgreSQL pod not found"
exit 1
fi
DB_SIZE=$(kubectl exec -n "$NAMESPACE" "$DB_POD" -- psql -U postgres -d nextcloud_db -t -c "SELECT pg_database_size('nextcloud_db');" 2>/dev/null | tr -d ' ' || echo "0")
echo "Database size: $(numfmt --to=iec-i --suffix=B "$DB_SIZE" 2>/dev/null || echo "$DB_SIZE bytes")"
echo "Dumping database from $DB_POD..."
kubectl exec -n "$NAMESPACE" "$DB_POD" -- sh -c "pg_dump -U postgres nextcloud_db" | gzip > "$NC_DB_DEST/dump.sql.gz" &
BACKUP_PID=$!
show_progress "$NC_DB_DEST/dump.sql.gz" "Database" "$DB_SIZE"
wait $BACKUP_PID 2>/dev/null || true
echo "Database backup: $(du -sh "$NC_DB_DEST/dump.sql.gz" | cut -f1)"
echo ""
echo "--- Backing up Nextcloud data ---"
NC_POD=$(kubectl get pods -n "$NAMESPACE" -l app=nextcloud -o jsonpath='{.items[0].metadata.name}')
if [[ -z "$NC_POD" ]]; then
echo "ERROR: Nextcloud pod not found"
exit 1
fi
NC_SIZE=$(kubectl exec -n "$NAMESPACE" "$NC_POD" -- du -sb /var/www/html 2>/dev/null | awk '{print $1}' || echo "0")
echo "Data size: $(numfmt --to=iec-i --suffix=B "$NC_SIZE" 2>/dev/null || echo "$NC_SIZE bytes")"
echo "Creating tar archive from $NC_POD..."
kubectl exec -n "$NAMESPACE" "$NC_POD" -- tar cf - -C /var/www/html . 2>/dev/null | gzip > "$NC_DATA_DEST/data.tar.gz" &
BACKUP_PID=$!
show_progress "$NC_DATA_DEST/data.tar.gz" "Data" "$NC_SIZE"
wait $BACKUP_PID 2>/dev/null || true
echo "Data backup: $(du -sh "$NC_DATA_DEST/data.tar.gz" | cut -f1)"
echo ""
echo "--- Creating latest symlinks ---"
rm -f "$BACKUP_DIR/nextcloud-data/latest" "$BACKUP_DIR/nextcloud-postgres/latest"
ln -sf "backups/$TIMESTAMP" "$BACKUP_DIR/nextcloud-data/latest"
ln -sf "backups/$TIMESTAMP" "$BACKUP_DIR/nextcloud-postgres/latest"
echo ""
echo "=== Backup Complete ==="
echo "Timestamp: $TIMESTAMP"
echo "Finished: $(date)"
echo ""
du -sh "$NC_DATA_DEST" "$NC_DB_DEST"
-41
View File
@@ -1,41 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SRC="/mnt/replicas"
DEST="$HOME/homey-backup/longhorn-volumes"
SKIP="pvc-dfe2aa08-bbb8-423b-9001-fb6aea181597-baf06a7f"
mkdir -p "$DEST"
echo "=== Copying Longhorn volumes from HD ==="
echo "Source: $SRC"
echo "Dest: $DEST"
echo "Skip: $SKIP (Jellyfin)"
echo ""
for pvc in "$SRC"/*/; do
name=$(basename "$pvc")
if [[ "$name" == "$SKIP" ]]; then
echo "Skipping: $name"
continue
fi
echo ""
echo "Copying: $name"
src_size=$(sudo du -sb "$pvc" 2>/dev/null | awk '{print $1}' || echo "0")
src_size_hr=$(numfmt --to=iec-i --suffix=B "$src_size" 2>/dev/null || echo "${src_size}B")
echo "Size: $src_size_hr"
sudo rsync -a --no-owner --no-group --info=progress2 "${pvc%/}" "$DEST/"
sudo chown -R "$USER" "$DEST/$name"
size=$(du -sh "$DEST/$name" | cut -f1)
echo "Done: $size"
done
echo ""
echo "=== Copy Complete ==="
echo "Total size:"
sudo du -sh "$DEST"
-87
View File
@@ -1,87 +0,0 @@
###############################################################
# Authelia minimal configuration #
###############################################################
theme: "light"
log:
level: "debug"
jwt_secret: {{ .homey_authelia_jwt | quote }}
authentication_backend:
ldap:
implementation: "custom"
url: "ldap://openldap:389"
timeout: "5s"
start_tls: false
base_dn: "{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim}}"
users_filter: "({username_attribute}={input})"
username_attribute: "uid"
additional_users_dn: "ou=users"
groups_filter: "(&(uniquemember=uid={input},ou=users,{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim}})(objectclass=groupOfUniqueNames))"
group_name_attribute: "cn"
additional_groups_dn: "ou=groups"
mail_attribute: "mail"
display_name_attribute: "uid"
permit_referrals: false
permit_unauthenticated_bind: false
user: "cn=readonly,{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim }}"
password: {{ .homey_openldap_ro | quote }}
totp:
issuer: "{{ .Values.homey.url }}"
disable: false
session:
name: authelia_session
secret: {{ .homey_authelia_session | quote }}
expiration: 3600 # 1 hour
inactivity: 7200 # 2 hours
domain: "{{ .Values.homey.url}}" # needs to be your root domain
storage:
local:
path: "/config/db.sqlite3"
encryption_key: {{ .homey_authelia_encryption_key | quote }}
access_control:
default_policy: "deny"
rules:
- domain:
- "auth.zakobar.com"
policy: "bypass"
- domain:
- "dav.{{ .Values.homey.url }}"
policy: "one_factor"
- domain:
- "ldapadmin.{{ .Values.homey.url }}"
subject:
- 'group:admins'
policy: "two_factor"
- domain:
- "*.admin.{{ .Values.homey.url }}"
subject:
- 'group:admins'
policy: "two_factor"
- domain:
- "*.admin.{{ .Values.homey.url }}"
policy: "deny"
- domain:
- "torrent.{{ .Values.homey.url }}"
subject:
- 'group:admins'
policy: "two_factor"
- domain:
- "torrent.{{ .Values.homey.url }}"
policy: "deny"
- domain:
- "stash-dl.{{ .Values.homey.url }}"
policy: "one_factor"
- domain:
- "stash.{{ .Values.homey.url }}"
policy: "one_factor"
- domain:
- "paperless.{{ .Values.homey.url }}"
policy: "one_factor"
notifier:
filesystem:
filename: "/var/lib/authelia/emails.txt"
ntp:
address: 'udp://time.cloudflare.com:123'
version: 3
max_desync: '3s'
disable_startup_check: false
disable_failure: true
-30
View File
@@ -1,30 +0,0 @@
<?php
use
Sabre\DAV;
// The autoloader
require 'vendor/autoload.php';
// Now we're creating a whole bunch of objects
$rootDirectory = new DAV\FS\Directory('public');
// The server object is responsible for making sense out of the WebDAV protocol
$server = new DAV\Server($rootDirectory);
// If your server is not on your webroot, make sure the following line has the
// correct information
$server->setBaseUri('server.php');
// The lock manager is reponsible for making sure users don't overwrite
// each others changes.
$lockBackend = new DAV\Locks\Backend\File('data/locks');
$lockPlugin = new DAV\Locks\Plugin($lockBackend);
$server->addPlugin($lockPlugin);
// This ensures that we get a pretty index in the browser, but it is
// optional.
$server->addPlugin(new DAV\Browser\Plugin());
// All we need to do now, is to fire up the server
$server->exec();
-580
View File
@@ -1,580 +0,0 @@
<?php
$c->pg_connect[] = "dbname=davical user=postgres port=5432 host=davical-postgres password={{ .homey_davical_postgres_pass }}";
/****************************
********* Desirable *********
*****************************/
$c->system_name = "{{ .Values.homey.organization }} CalDAV Server";
$c->dbg = array( 'statistics' => 1, 'request' => 1, 'response' => 1 );
// $c->admin_email = 'calendar-admin@example.com';
$c->restrict_setup_to_admin = true;
/***************************************************************************
* *
* Caldav Server *
* *
***************************************************************************/
/**
* The "collections_always_exist" value defines whether a MKCALENDAR
* command is needed to create a calendar collection before calendar
* resources can be stored in it. You will want to leave this to the
* default (true) if people will be using Evolution or Sunbird /
* Lightning against this because that software does not support the
* creation of calendar collections.
*
* Default: true
*/
// $c->collections_always_exist = false;
/**
* The name of a user's "home" calendar and addressbook. These will be created
* for each new user.
*
* Defaults:
* home_calendar_name: 'calendar'
* home_addressbook_name: 'addresses'
*/
// $c->home_calendar_name = 'calendar';
// $c->home_addressbook_name = 'addresses';
/**
* Sets a numeric value indicating the maximum size in octets (bytes) of a resource
* that the server is willing to accept when an address object resource is stored
* in an address book collection (e.g. contacts with image attachments).
* Note that not all clients respect that property and that DAViCal won't deny creating
* or updating a resource that is larger than the specified limit if the client willingly or
* unwillingly ignores that property. Currently (late 2018) we only know of iOS devices to handle it properly.
*
* Default: 6550000
*/
// $c->carddav_max_resource_size = 6550000;
/**
* If the above options are not suitable for your new users, use this to create
* a more complex default collection management.
*
* Note: if you use this configuration option both $c->home_calendar_name and
* $c->home_addressbook_name are ignored!
*
* See https://wiki.davical.org/index.php/Configuration/settings/default_collections
*/
// $c->default_collections = array(
// array(
// 'type' => 'addressbook',
// 'name' => 'addresses',
// 'displayname' => '%fn addressbook',
// 'privileges' => null
// ),
// array(
// 'type' => 'calendar',
// 'name' => 'calendar',
// 'displayname' => '%fn calendar',
// 'privileges' => null
// )
// );
/**
* An array of groups / permissions which should be automatically added
* for each new user created. This is a crude mechanism which we
* will hopefully manage to work out some better approach for in the
* future. For now, create an array that looks something like:
* array( 9 => 'R', 4 => 'A' )
* to create a 'read' relationship to user_no 9 and an 'all' relation
* with user_no 4.
*
* Default: none
*/
// $c->default_relationships = array();
/**
* An array of the privileges which will be configured for a user by default
* from the possible set of real privileges:
* 'read', 'write-properties', 'write-content', 'unlock', 'read-acl', 'read-current-user-privilege-set',
* 'bind', 'unbind', 'write-acl', 'read-free-busy',
* 'schedule-deliver-invite', 'schedule-deliver-reply', 'schedule-query-freebusy',
* 'schedule-send-invite', 'schedule-send-reply', 'schedule-send-freebusy'
*
* Or also from these aggregated privileges:
* 'write', 'schedule-deliver', 'schedule-send', 'all'
*/
// $c->default_privileges = array('read-free-busy', 'schedule-query-freebusy');
/**
* An array of fields on the usr record which should be set to specific
* values when the users are created.
*
* Default: none
*/
// $c->template_usr = array(
// 'active' => true,
// 'locale' => 'it_IT',
// 'date_format_type' => 'E',
// 'email_ok' => date('Y-m-d')
// );
/**
* If "hide_TODO" is true, then VTODO requested from someone other than the
* admin or owner of a calendar will not get an answer. Often these todo are
* only relevant to the owner, but in some shared calendar situations they
* might not be in which case you should set this to false.
*
* Default: true
*/
// $c->hide_TODO = false;
/**
* If true, then VALARM from someone other than the admin or owner of a
* calendar will not be included in the response. The default is false because
* the preferred behaviour is to enable/disable the alarms in your CalDAV
* client software.
*
* Default: false
*/
// $c->hide_alarm = true;
/**
* If you want to hide older events (in order to save resources, speed up
* clients, etc.) define the desired time interval in number of days.
*/
// $c->hide_older_than = 90;
/**
* Hide bound collections from certain clients
*
* If you want to use iOS (which does not support delegation) in combination
* with other software which does supports degation, you can use this option
* to tailor a working solution: bind all collections you want to see on iOS
* (emulation of delegation) and then hide these collections from other clients
* with real delegation support.
*
* Default: false/not set: always show bound collections
*
* If set to true: never show bound collections
* If set to an array: hide if any header => regex tuple matches
* Example: Hide bound collections from clients which send a User-Agent header
* matching regex1 OR an X-Client header matching regex2
*/
// $c->hide_bound = array( 'User-Agent'=>'#regex1#', 'X-Client'=>'#regex2#');
/**
* External subscription (BIND) minimum refresh interval
* Required if you want to enable remote binding ( webcal subscriptions )
*
* Default: none
*/
// $c->external_refresh = 60;
/**
* External subscription (BIND) user agent string
* Required if your remote calendar only delivers to known user agents.
*
* Default: none
*/
// $c->external_ua_string = '';
/**
* If you want to force DAViCal to use HTTP Digest Authentication for CalDAV
* access. Note that this requires all user passwords to be stored in plain text
* in the database. It is probably better to configure the webserver to do
* Digest auth against a separate user database (see below for Webserver Auth).
*/
// $c->http_auth_mode = "Digest";
/**
* Provide freebusy information to any (unauthenticated) user via the
* freebusy.php URL. Only events marked as PRIVATE will be excluded from the
* report.
*
* Default: false (authentication required)
*/
// $c->public_freebusy_url = true;
/**
* The "support_obsolete_free_busy_property" value controls whether,
* during a PROPFIND, the obsolete Scheduling property "calendar-free-busy-set"
* is returned. Set the value to true to support the property only if your
* client requires it, however note that PROPFIND performance may be
* adversely affected if you do so.
*
* Introduced in DAViCal version 1.1.4 in support of Issue #31 Database
* Performance Improvements.
*
* Default: false
*/
// $c->support_obsolete_free_busy_property = false;
/**
* The default locale will be "en_NZ";
*
* If you are in a non-English locale, you can set the default_locale
* configuration to one of the supported locales.
*
* Supported Locales (at present, see: "select * from supported_locales ;" for a full list)
*
* "de_DE", "en_NZ", "es_AR", "fr_FR", "nl_NL", "ru_RU"
*
* If you want locale support you probably know more about configuring it than me, but
* at this stage it should be noted that all translations are UTF-8, and pages are
* served as UTF-8, so you will need to ensure that the UTF-8 versions of these locales
* are supported on your system.
*
* People interested in providing new translations are directed to the Wiki:
* https://wiki.davical.org/w/Translating_DAViCal
*/
// $c->default_locale = "en_NZ";
/**
* This is used to construct URLs which are passed in the answers to the client. You may
* want to force this to a specific domain in responses if your system is accessed by
* multiple names, otherwise you probably won't need to change it.
*
* Default: $_SERVER['SERVER_NAME']
*/
// $c->domain_name = 'example.com';
/**
* If this option is set to true, then "@$c->domain_name" is appended to the
* user login name if it does not contain the @ character. If email addresses
* are used as user names in Davical, this fixes a problem with MacOS X 10.6
* Addressbook that cannot login to CardDav account.
*
* Default: false
*/
// $c->login_append_domain_if_missing = true;
/**
* Many people want this, but it may be a security issue for you, so it is
* disabled by default. If you enable it, then confidential / private events
* will be visible to the 'organizer' or 'attendee' lists. The reason that
* this becomes a security issue is that this identification needs to be based
* on the user's e-mail address. The user's e-mail address is generally
* something which they can set, so they could change it to be the address of
* an attendee of a meeting and then would be able to read the meeting.
*
* Without this, the only person who can view/change PRIVATE or CONFIDENTIAL
* events in a calendar is someone with full administrative rights to the calendar
* usually the owner.
*
* If the only person that devious is your sysadmin then you probably already
* enabled this option...
*
* Default: false
*/
// $c->allow_get_email_visibility = false;
/**
* Disable calendar-proxy-{read,write} on PROPFIND
*
* This can be useful if clients are known to not use this information,
* as it is very expensive to compute (especially on servers with lots of
* users who share their collections) and most clients will never use it,
* or ask for it explicitly using an expand-property REPORT, which is not
* affected by this option.
*
* Default: false/unset
*
* If set to false (or unset): always show
* If set to true: never show
* If set to an array: hide if any header => regex tuple matches
*/
// $c->disable_caldav_proxy_propfind_collections = array( 'User-Agent'=>'#regex1#', 'X-Client'=>'#regex2#');
/**
* A limiter on how many times we'll apply the recurrence rules for an event
* to find the next valid one.
*
* Default: 100
*
* If you see the following error message, you may want to consider increasing
* it:
* RRULE, loop limit has been hit in GetMoreInstances, you probably want to increase $c->rrule_loop_limit
*/
// $c->rrule_loop_limit = 100;
/**
* EXPERIMENTAL:
* If true, names of groups (prefixed with "@") given as an event attendee
* will get resolved to a list of members of that group. Note that CalDAV
* clients might get confused by this server behavior until they get
* synced again.
*
* Default: false.
*/
// $c->enable_attendee_group_resolution = true;
/***************************************************************************
* *
* Scheduling *
* *
***************************************************************************/
/**
* If you want to turn off scheduling functions you can set this to 'false' and
* DAViCal will not advertise the ability to schedule, leaving it to calendar
* clients to send out and receive scheduling requests.
*
* Default: true
*/
// $c->enable_auto_schedule = false;
/**
* If true, then remote scheduling will be enabled. There is a possibility
* of receiving spam events in calendars if enabled, you will at least know
* what domain the spam came from as domain key signatures are required for
* events to be accepted.
*
* You probably need to setup Domain Keys for your domain as well as the
* appropiate DNS SRV records.
*
* for example, if DAViCal is installed on cal.example.com you should have
* DNS SRV records like this:
* _ischedules._tcp.example.com. IN SRV 0 1 443 cal.example.com
* _ischedule._tcp.example.com. IN SRV 0 1 80 cal.example.com
*
* DNS TXT record for signing outbound requests
* example:
* cal._domainkey.example.com. 86400 IN TXT "k=rsa\; t=s\; p=PUBKEY"
*
* Default: false
*/
// $c->enable_scheduling = true;
/**
* Domain Key domain to use when signing outbound scheduling requests, this
* is the domain with the public key in a TXT record as shown above.
*
* TODO: enable domain/signing by per user keys, patches welcome.
*
* Default: none
*/
// $c->scheduling_dkim_domain = '';
/**
* Domain Key selector to use when signing outbound scheduling requests.
*
* TODO: enable selectors/signing by per user keys, patches welcome.
*
* Default: 'cal'
*/
// $c->scheduling_dkim_selector = 'cal';
/*
* Domain Key private key
* Required if you want to enable outbound remote server scheduling
*
* Default: none
*/
// $c->schedule_private_key = 'PRIVATE-KEY-BASE-64-DATA';
/***************************************************************************
* *
* Operation behind a Reverse Proxy *
* *
***************************************************************************/
/**
* If you install DAViCal behind a reverse proxy (e.g. an SSL offloader or
* application firewall, or in order to present services from different machines
* on a single public IP / hostname), the client IP, protocol and port used may
* be different from what the web server is reporting to DAViCal. Often, the
* original values are written to the X-Real-IP and/or X-Forwarded-For,
* X-Forwarded-Proto and X-Forwarded-Port headers. You can instruct DAViCal to
* attempt to "do the right thing" and use the content of these headers instead,
* when they are available.
*
* CAUTION: Malicious clients can spoof these headers. When you enable this, you
* need to make sure your reverse proxy erases any pre-existing values of all
* these headers, and that no untrusted requests can reach DAViCal without
* passing the proxy server.
*
* Default: false
*/
unset( $_SERVER['HTTP_X_REAL_IP'] );
$c->trust_x_forwarded = true;
/* Set all values manually. */
// $_SERVER['HTTPS'] = 'on';
// $_SERVER['SERVER_PORT'] = 443;
// $_SERVER['REMOTE_ADDR'] = $_SERVER['Client-IP'];
/***************************************************************************
* *
* External Authentication Sources *
* *
***************************************************************************/
/**
* Allow specifying another way to control access of the user by authenticating
* him against other drivers such has LDAP (the default is the PgSQL DB)
* $c->authenticate_hook['call'] should be set to the name of the plugin and must
* be a valid function that will be call like this:
* call_user_func( $c->authenticate_hook['call'], $username, $password )
*
* The login mechanism is used in 2 different places:
* - for the web interface in: index.php that calls DAViCalSession.php that extends
* Session.php (from AWL libraries)
* - for the caldav client in: caldav.php that calls HTTPAuthSession.php
* Both Session.php and HTTPAuthSession.php check against the
* authenticate_hook['call'], although for HTTPAuthSession.php this will be for
* each page. For Session.php this will only occur during login.
*
* $c->authenticate_hook['config'] should be set up with any configuration data
* needed by the authenticate call - see below or in the Wiki for details.
* If you want to develop your own authentication plugin, have a look at
* awl/inc/AuthPlugins.php or any of the inc/drivers_*.php files.
*
* $c->authenticate_hook['optional'] = true; can be set to try default authentication
* as well in case the configured hook should report a failure.
*/
// $c->authenticate_hook['optional'] = true;
/********************************/
/******* Other AWL hook *********/
/********************************/
// require_once('auth-functions.php');
// $c->authenticate_hook = array(
// 'call' => 'AuthExternalAwl',
// 'config' => array(
// // A PgSQL database connection string for the database containing user records
// 'connection' => 'dbname=wrms host=otherhost port=5433 user=general',
// // Which columns should be fetched from the database
// 'columns' => "user_no, active, email_ok, joined, last_update AS updated, last_used, username, password, fullname, email",
// // a WHERE clause to limit the records returned.
// 'where' => "active AND org_code=7"
// )
// );
/********************************/
/*********** LDAP hook **********/
/********************************/
/*
* For Active Directory go down to the next example.
*/
putenv('LDAPTLS_REQCERT=never');
$c->authenticate_hook['call'] = 'LDAP_check';
$c->authenticate_hook['config'] = array(
'uri' => 'ldaps://openldap:636',
'bindDN' => 'cn=readonly,{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim }}',
'passDN' => '{{ .homey_openldap_ro }}',
'protocolVersion' => 3, // Version of LDAP protocol to use
'optReferrals' => 0, // whether to automatically follow referrals returned by the LDAP server
'networkTimeout' => 10, // timeout in seconds
'baseDNUsers' => 'ou=users,{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim}}',
'filterUsers' => 'objectClass=person',
'baseDNGroups' => 'ou=groups,{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim}}',
'filterGroups' => 'objectClass=groupOfUniqueNames',
'mapping_field' => array("username" => "uid",
"modified" => "modifyTimestamp",
"fullname" => "cn" ,
"email" => "mail"
), // used to create the user based on their ldap properties
'group_mapping_field' => array("username" => "cn",
"modified" => "modifyTimestamp",
"fullname" => "cn" ,
"members" => "memberUid"
), // used to create the group based on the ldap properties
'group_member_dnfix' => true, // if your "members" field contains the full DN and needs to be truncated to just the uid
'startTLS' => 'no',
);
include('drivers_ldap.php');
/********************************/
/****** Webserver does Auth *****/
/********************************/
/**
* It is quite common that the webserver can do the authentication for you,
* and you just want DAViCal to trust the username that the webserver will pass
* through (in the REMOTE_USER or REDIRECT_REMOTE_USER environment variable).
* In that case, set server_auth_type (can be an array) to the value provided by
* the webserver in the AUTH_TYPE environment variable, as well as the two
* following options as needed.
*
* Note that this method does not pull account details from anywhere, so you
* will first need to create an account in DAViCal for each username that will
* authenticate in this way - it's just that the password on that account will
* be ignored and authentication will happen through the authentication method
* that the webserver is configured with.
*/
$c->authenticate_hook['server_auth_type'] = 'Basic';
include_once('AuthPlugins.php');
/**
* Uncomment this to use Webserver Auth for CalDAV access in addition to the
* Admin web pages.
*/
/**
* If your Webserver Auth method provides a logout URL (traditional Basic Auth
* does not), you can enter it here so the Logout link in the Admin web pages
* can point to it.
*/
$c->authenticate_hook['logout'] = 'https://auth.{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim}}/logout?rd=dav.{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim}}';
/***************************************************************************
* *
* Push Notification Server *
* *
***************************************************************************/
/*
* This enable XMPP PubSub push notifications to clients that request them.
* N.B. this will publish urls for ALL updates and does NOT restrict
* subscription permissions on the jabber server! That means anyone with
* read access to the pubsub tree of your jabber server can watch for updates,
* they will only see URL's to the updated entries not the calendar data.
*
* Only tested with ejabberd 2.0.x
*/
// $c->notifications_server = array(
// 'host' => $_SERVER['SERVER_NAME'], // jabber server hostname
// 'jid' => 'user@example.com', // user(JID) to login/ publish as
// 'password' => '', // password for above account
// // 'debug_jid' => 'otheruser@example.com' // send a copy of all publishes to this jid
// );
// include ( 'pubsub.php' );
/***************************************************************************
* *
* Detailed Metrics *
* *
***************************************************************************/
/*
* This enables a /metrics.php URL containing detailed metrics about the
* operation of DAViCal. Ideally you will be running memcache if you are
* interested in keeping metrics, but there is a simple metrics collection
* available to you without running memcache.
*
* Note that there is currently no way of enabling metrics via memcache
* without memcache being enabled for all of DAViCal.
*/
// $c->metrics_style = 'counters'; // Just the simple counter-based metrics
// $c->metrics_style = 'memcache'; // Only the metrics using memcache
// $c->metrics_style = 'both'; // Both styles of metrics
// $c->metrics_collectors = array('127.0.0.1'); // Restrict access to only this IP address
// $c->metrics_require_user = 'metricsuser'; // Restrict access to only connections authenticating as this user
/***************************************************************************
* *
* Audit Logging *
* *
***************************************************************************/
/* To enable audit logging to syslog you can uncomment the following line.
*
* This file is suitable for basic auditing, if you want/need more comprehensive
* logging then see:
* http://wiki.davical.org/index.php/Configuration/hooks/log_caldav_action
*/
// include('log_caldav_action.php');
-95
View File
@@ -1,95 +0,0 @@
APP_NAME = {{ .Values.homey.organization }}
RUN_MODE = prod
RUN_USER = git
WORK_PATH = /data/gitea
[repository]
ROOT = /data/git/repositories
[repository.local]
LOCAL_COPY_PATH = /data/gitea/tmp/local-repo
[repository.upload]
TEMP_PATH = /data/gitea/uploads
[server]
APP_DATA_PATH = /data/gitea
DOMAIN = git.{{ .Values.homey.url }}
HTTP_PORT = 3000
ROOT_URL = https://git.{{ .Values.homey.url }}/
DISABLE_SSH = true
SSH_PORT = 443
SSH_LISTEN_PORT = 22
LFS_START_SERVER = true
LFS_JWT_SECRET = {{ .homey_gitea_lfs_jwt_secret | b64enc | replace "=" "" }}
OFFLINE_MODE = false
[lfs]
PATH = /data/git/lfs
[database]
PATH = /data/gitea/gitea.db
DB_TYPE = sqlite3
HOST = localhost:3306
NAME = gitea
USER = root
PASSWD =
LOG_SQL = false
SCHEMA =
SSL_MODE = disable
CHARSET = utf8
[indexer]
ISSUE_INDEXER_PATH = /data/gitea/indexers/issues.bleve
[session]
PROVIDER_CONFIG = /data/gitea/sessions
PROVIDER = file
[picture]
AVATAR_UPLOAD_PATH = /data/gitea/avatars
REPOSITORY_AVATAR_UPLOAD_PATH = /data/gitea/repo-avatars
DISABLE_GRAVATAR = false
ENABLE_FEDERATED_AVATAR = false
[attachment]
PATH = /data/gitea/attachments
[log]
MODE = console
LEVEL = info
ROUTER = console
ROOT_PATH = /data/gitea/log
[security]
INSTALL_LOCK = true
SECRET_KEY =
REVERSE_PROXY_LIMIT = 1
REVERSE_PROXY_TRUSTED_PROXIES = *
INTERNAL_TOKEN = {{ .homey_gitea_random_internal_token }}
PASSWORD_HASH_ALGO = pbkdf2
[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = false
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false
ALLOW_ONLY_EXTERNAL_REGISTRATION = true
ENABLE_CAPTCHA = false
DEFAULT_KEEP_EMAIL_PRIVATE = false
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
DEFAULT_ENABLE_TIMETRACKING = true
NO_REPLY_ADDRESS = noreply.localhost
ENABLE_REVERSE_PROXY_AUTHENTICATION = true
ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = true
[mailer]
ENABLED = false
[openid]
ENABLE_OPENID_SIGNIN = false
ENABLE_OPENID_SIGNUP = false
[oauth2]
ENABLE = false
JWT_SECRET = {{ .homey_gitea_oauth2_jwt_secret | b64enc | replace "=" "" }}
-11
View File
@@ -1,11 +0,0 @@
[server]
hosts = 0.0.0.0:5232
[auth]
type = http_x_remote_user
[storage]
filesystem_folder = /data/collections
[web]
type = none
-30
View File
@@ -1,30 +0,0 @@
<?php
use
Sabre\DAV;
// The autoloader
require 'vendor/autoload.php';
// Now we're creating a whole bunch of objects
$rootDirectory = new DAV\FS\Directory('public');
// The server object is responsible for making sense out of the WebDAV protocol
$server = new DAV\Server($rootDirectory);
// If your server is not on your webroot, make sure the following line has the
// correct information
$server->setBaseUri('server.php');
// The lock manager is reponsible for making sure users don't overwrite
// each others changes.
$lockBackend = new DAV\Locks\Backend\File('data/locks');
$lockPlugin = new DAV\Locks\Plugin($lockBackend);
$server->addPlugin($lockPlugin);
// This ensures that we get a pretty index in the browser, but it is
// optional.
$server->addPlugin(new DAV\Browser\Plugin());
// All we need to do now, is to fire up the server
$server->exec();
-94
View File
@@ -1,94 +0,0 @@
{
/* ********************* Main SOGo configuration file **********************
* *
* Since the content of this file is a dictionary in OpenStep plist format, *
* the curly braces enclosing the body of the configuration are mandatory. *
* See the Installation Guide for details on the format. *
* *
* C and C++ style comments are supported. *
* *
* This example configuration contains only a subset of all available *
* configuration parameters. Please see the installation guide more details. *
* *
* ~sogo/GNUstep/Defaults/.GNUstepDefaults has precedence over this file, *
* make sure to move it away to avoid unwanted parameter overrides. *
* *
* **************************************************************************/
/* Database configuration (mysql:// or postgresql://) */
SOGoProfileURL = "postgresql://sogo:sogo@sogo-postgres:5432/sogo/sogo_user_profile";
OCSFolderInfoURL = "postgresql://sogo:sogo@sogo-postgres:5432/sogo/sogo_folder_info";
OCSSessionsFolderURL = "postgresql://sogo:sogo@sogo-postgres:5432/sogo/sogo_sessions_folder";
/* Mail */
SOGoDraftsFolderName = Drafts;
SOGoSentFolderName = Sent;
SOGoTrashFolderName = Trash;
//SOGoIMAPServer = localhost;
//SOGoSieveServer = sieve://127.0.0.1:4190;
//SOGoSMTPServer = smtp://domain:port/?tls=YES;
//SOGoMailDomain = acme.com;
SOGoMailingMechanis = smtp;
//SOGoForceExternalLoginWithEmail = NO;
//SOGoMailSpoolPath = /var/spool/sogo;
//NGImap4ConnectionStringSeparator = "/";
/* Notifications */
//SOGoAppointmentSendEMailNotifications = NO;
//SOGoACLsSendEMailNotifications = NO;
//SOGoFoldersSendEMailNotifications = NO;
/* Authentication */
SOGoPasswordChangeEnabled = YES;
SOGoUserSources = (
{
type = ldap;
CNFieldName = cn;
UIDFieldName = uid;
IDFieldName = uid; // first field of the DN for direct binds
bindFields = (uid, mail); // array of fields to use for indirect binds
baseDN = "ou=users,{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim }}";
bindDN = "cn=readonly,{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim }}";
bindPassword = "{{ .homey_openldap_ro }}";
canAuthenticate = YES;
displayName = "Shared Addresses";
hostname = ldap://openldap:389;
id = public;
isAddressBook = YES;
}
);
/* Web Interface */
//SOGoPageTitle = SOGo;
SOGoVacationEnabled = YES;
SOGoForwardEnabled = YES;
SOGoSieveScriptsEnabled = YES;
//SOGoMailAuxiliaryUserAccountsEnabled = YES;
//SOGoTrustProxyAuthentication = NO;
SOGoXSRFValidationEnabled = YES;
/* General - SOGoTimeZone *MUST* be defined */
SOGoLanguage = English;
SOGoTimeZone = Asia/Jerusalem;
//SOGoCalendarDefaultRoles = (
// PublicDAndTViewer,
// ConfidentialDAndTViewer
//);
//SOGoSuperUsernames = (sogo1, sogo2); // This is an array - keep the parens!
SxVMemLimit = 384;
//WOPidFile = "/var/run/sogo/sogo.pid";
SOGoMemcachedHost = "/var/run/memcached/memcached.sock";
/* Debug */
SOGoDebugRequests = YES;
SoDebugBaseURL = YES;
ImapDebugEnabled = YES;
LDAPDebugEnabled = YES;
PGDebugEnabled = YES;
MySQL4DebugEnabled = YES;
SOGoUIxDebugEnabled = YES;
WODontZipResponse = YES;
//WOLogFile = /var/log/sogo/sogo.log;
}
+73
View File
@@ -0,0 +1,73 @@
{
description = "Homey - self-hosted home server NixOS configuration";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
# sops-nix for secret management
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
# Caddy with Cloudflare DNS plugin (not in nixpkgs mainline)
caddy-cloudflare = {
url = "github:NixOS/nixpkgs/nixos-24.11"; # see modules/caddy.nix for override
};
};
outputs = { self, nixpkgs, sops-nix, ... }@inputs:
let
# Shared specialArgs passed to every host
commonArgs = {
inherit inputs;
# Top-level site config — override per-host if needed
homeyConfig = {
domain = "home.zakobar.com"; # base domain for all services
organization = "Zakobar Home Server";
timezone = "Asia/Jerusalem";
# External HD mount point — set in hardware.nix per host
# dataDir is intentionally NOT set here; each host sets it
};
};
mkHost = { system, hostPath, extraModules ? [] }:
nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = commonArgs;
modules = [
sops-nix.nixosModules.sops
hostPath
./modules/common.nix
./modules/storage.nix
./modules/caddy.nix
./modules/cloudflared.nix
./modules/backup.nix
./modules/services/openldap.nix
./modules/services/authelia.nix
./modules/services/gitea.nix
./modules/services/nextcloud.nix
./modules/services/phpldapadmin.nix
./modules/services/jellyfin.nix
./modules/services/transmission.nix
] ++ extraModules;
};
in {
nixosConfigurations = {
# Primary Raspberry Pi 4
pi-main = mkHost {
system = "aarch64-linux";
hostPath = ./hosts/pi-main/default.nix;
};
# Future second machine (placeholder — uncomment and configure when ready)
# pi-secondary = mkHost {
# system = "x86_64-linux"; # or aarch64-linux for another Pi
# hostPath = ./hosts/pi-secondary/default.nix;
# };
};
};
}
-1
View File
@@ -1 +0,0 @@
kubectl get secret -n $1 $2 --template={{.data.$3}} | base64 -d | xclip -selection c
-10
View File
@@ -1,10 +0,0 @@
kubectl exec -it -n homey deploy/gitea -- su - git -c "/usr/local/bin/gitea admin auth update-ldap --id=1 --name ldap --security-protocol unencrypted --host openldap --port 389 --user-search-base ou=users,dc=zakobar,dc=com --user-filter \"(&(objectClass=person)(uid=%s))\" --admin-filter \"(memberOf=CN=admins,ou=groups,dc=zakobar,dc=com)\" --email-attribute mail --bind-dn=cn=readonly,dc=zakobar,dc=com --bind-password=VqxPZHwDCkFsLWaroyb880zdH1JTCvz9"
kubectl exec -it -n homey deploy/gitea -- su - git -c "/usr/local/bin/gitea admin user delete --username aner"
gitea admin auth add-ldap --name ldap --security-protocol unencrypted --host openldap --port 389 --user-search-base ou=users,dc=zakobar,dc=com --user-filter "&(objectClass=inetOrgPerson)(uid=%s)" --email-attribute mail --bind-dn="cn=readonly,dc=zakobar,dc=com" --bind-password=VqxPZHwDCkFsLWaroyb880zdH1JTCvz9
gitea admin auth update-ldap --id=1 --name ldap --security-protocol unencrypted --host openldap --port 389 --user-search-base ou=users,dc=zakobar,dc=com --user-filter "(&(objectClass=person)(uid=%s))" --email-attribute mail --bind-dn="cn=readonly,dc=zakobar,dc=com" --bind-password=VqxPZHwDCkFsLWaroyb880zdH1JTCvz9
kubectl exec -it -n homey deploy/authelia -- /bin/bash -c "cat /var/lib/authelia/emails.txt"
+83
View File
@@ -0,0 +1,83 @@
{ config, lib, pkgs, homeyConfig, ... }:
# Pi-main host configuration.
# This file declares which services run on this machine and any
# host-specific overrides. Hardware config lives in hardware.nix.
{
imports = [
./hardware.nix
];
# -------------------------------------------------------------------------
# Identity
# -------------------------------------------------------------------------
networking.hostName = "pi-main";
# -------------------------------------------------------------------------
# Admin user
# -------------------------------------------------------------------------
users.users.admin = {
isNormalUser = true;
extraGroups = [ "wheel" "podman" ];
# Paste your SSH public key here
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAA... your-key-here"
];
};
security.sudo.wheelNeedsPassword = false; # convenience on a home server
# -------------------------------------------------------------------------
# External HD
# -------------------------------------------------------------------------
homey.storage = {
# Replace with the actual by-id path of your USB drive.
# Find it: ls -la /dev/disk/by-id/ | grep -v part
device = "/dev/disk/by-id/REPLACE-WITH-YOUR-DRIVE-ID";
mountPoint = "/mnt/data";
fsType = "ext4";
};
# -------------------------------------------------------------------------
# Services enabled on this host
# -------------------------------------------------------------------------
# Auth stack (run these together — authelia depends on openldap)
homey.openldap.enable = true;
homey.authelia.enable = true;
# Productivity
homey.gitea.enable = true;
homey.nextcloud.enable = true;
homey.phpldapadmin.enable = true;
# Media (enable when ready)
homey.jellyfin.enable = false;
homey.transmission.enable = false;
# Reverse proxy + Cloudflare
homey.caddy.enable = true;
homey.cloudflared.enable = true;
# Backups
homey.backup.enable = true;
# Where to send restic backups — set to your backup destination:
# "sftp:user@nas.local:/backups/homey"
# "b2:your-bucket-name:homey"
# "rclone:remote:homey"
homey.backup.repository = "sftp:REPLACE-WITH-BACKUP-DESTINATION";
# -------------------------------------------------------------------------
# Local DNS overrides (optional — makes LAN clients hit the Pi directly
# instead of going through Cloudflare for *.home.zakobar.com)
# -------------------------------------------------------------------------
# If you run Pi-hole or Adguard, add these records there instead.
# networking.extraHosts = ''
# 192.168.1.100 home.zakobar.com
# 192.168.1.100 auth.home.zakobar.com
# 192.168.1.100 git.home.zakobar.com
# 192.168.1.100 nextcloud.home.zakobar.com
# 192.168.1.100 ldapadmin.home.zakobar.com
# '';
}
+84
View File
@@ -0,0 +1,84 @@
{ config, lib, pkgs, modulesPath, ... }:
# Hardware configuration for the primary Raspberry Pi 4 (8 GB).
#
# SD card layout assumed:
# /dev/mmcblk0p1 — /boot/firmware (FAT32, ~256 MB)
# /dev/mmcblk0p2 — / (ext4)
#
# External HD:
# Set homey.storage.device to the by-id path of your USB drive.
# Example: /dev/disk/by-id/usb-WD_Elements_12345-0:0-part1
# Find it with: ls -la /dev/disk/by-id/
#
# To generate this file fresh after installing NixOS on the Pi, run:
# nixos-generate-config --show-hardware-config
# and merge the output here.
{
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
];
# -------------------------------------------------------------------------
# Boot loader — Raspberry Pi 4 uses U-Boot / extlinux
# -------------------------------------------------------------------------
boot = {
loader = {
grub.enable = false;
generic-extlinux-compatible.enable = true;
};
# Pi 4 kernel — use the mainline kernel with RPi patches
kernelPackages = pkgs.linuxPackages_rpi4;
# tmpfs for /tmp — keep the SD card writes down
tmp.useTmpfs = true;
# Modules needed for USB storage (external HD)
initrd.availableKernelModules = [ "xhci_pci" "usbhid" "usb_storage" "uas" ];
kernelModules = [];
extraModulePackages = [];
};
# -------------------------------------------------------------------------
# Filesystems
# -------------------------------------------------------------------------
fileSystems."/" = {
device = "/dev/disk/by-label/NIXOS_SD"; # label the root partition NIXOS_SD when flashing
fsType = "ext4";
options = [ "noatime" ];
};
fileSystems."/boot/firmware" = {
device = "/dev/disk/by-label/FIRMWARE"; # FAT32 boot partition
fsType = "vfat";
options = [ "fmask=0022" "dmask=0022" ];
};
# External HD — device path is set in default.nix via homey.storage.device.
# storage.nix creates the actual fileSystems entry from that option.
swapDevices = [];
# -------------------------------------------------------------------------
# Hardware
# -------------------------------------------------------------------------
hardware = {
# Enable the RPi firmware (needed for GPU, WiFi, Bluetooth)
raspberry-pi."4".apply-overlays-dtmerge.enable = true;
# Disable GPU memory split for a headless server (gives more RAM to OS)
# Set via config.txt if needed: gpu_mem=16
};
# -------------------------------------------------------------------------
# Platform
# -------------------------------------------------------------------------
nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux";
# -------------------------------------------------------------------------
# Power management
# -------------------------------------------------------------------------
powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand";
}
+150
View File
@@ -0,0 +1,150 @@
{ config, lib, pkgs, homeyConfig, ... }:
# Restic backup module.
#
# Backs up all service data directories from the external HD.
# Schedule: daily at 03:00, keep 7 daily / 4 weekly / 6 monthly snapshots.
#
# Before a backup, Nextcloud is put into maintenance mode and postgres is
# pg_dump'd to a file. This ensures consistent DB backups.
#
# Secrets consumed from sops:
# restic/password
#
# The backup repository URL is set per-host in default.nix:
# homey.backup.repository = "sftp:user@nas:/backups/homey";
#
# Restore:
# restic -r <repo> restore latest --target /mnt/data
# (or restore a single path: --include /mnt/data/openldap)
let
cfg = config.homey.backup;
dataDir = config.homey.storage.mountPoint;
in
{
options.homey.backup = {
enable = lib.mkEnableOption "Restic backup jobs";
repository = lib.mkOption {
type = lib.types.str;
example = "sftp:user@nas.local:/backups/homey";
description = ''
Restic repository URL. Examples:
sftp:user@host:/path
b2:bucket-name:prefix
rclone:remote:path
/local/path (for testing)
'';
};
schedule = lib.mkOption {
type = lib.types.str;
default = "03:00";
description = "systemd OnCalendar expression for the daily backup.";
};
pruneRetention = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = {
daily = "7";
weekly = "4";
monthly = "6";
};
};
};
config = lib.mkIf cfg.enable {
# -----------------------------------------------------------------------
# Secrets
# -----------------------------------------------------------------------
sops.secrets."restic/password" = { owner = "root"; };
# -----------------------------------------------------------------------
# Pre-backup hook: pg_dump + nextcloud maintenance mode
# -----------------------------------------------------------------------
systemd.services."homey-backup-pre" = {
description = "Pre-backup hooks (pg_dump, NC maintenance mode)";
serviceConfig = {
Type = "oneshot";
ExecStart = pkgs.writeShellScript "backup-pre" ''
set -euo pipefail
# Put Nextcloud into maintenance mode (if running)
if systemctl is-active --quiet podman-nextcloud.service; then
podman exec nextcloud php occ maintenance:mode --on || true
fi
# Dump postgres (if running)
if systemctl is-active --quiet podman-nextcloud-postgres.service; then
install -d -m 700 ${dataDir}/nextcloud/db-dump
podman exec nextcloud-postgres \
pg_dump -U postgres nextcloud_db \
> ${dataDir}/nextcloud/db-dump/nextcloud.sql
fi
'';
};
};
systemd.services."homey-backup-post" = {
description = "Post-backup hooks (take NC out of maintenance mode)";
serviceConfig = {
Type = "oneshot";
ExecStart = pkgs.writeShellScript "backup-post" ''
set -euo pipefail
if systemctl is-active --quiet podman-nextcloud.service; then
podman exec nextcloud php occ maintenance:mode --off || true
fi
'';
};
};
# -----------------------------------------------------------------------
# Restic backup service
# -----------------------------------------------------------------------
services.restic.backups.homey = {
repository = cfg.repository;
passwordFile = config.sops.secrets."restic/password".path;
cacheDir = "${dataDir}/restic-cache";
paths = [
"${dataDir}/openldap"
"${dataDir}/authelia"
"${dataDir}/gitea"
"${dataDir}/nextcloud"
# media and transmission config included when those services are enabled:
"${dataDir}/jellyfin"
"${dataDir}/transmission"
# Deliberately excluded: media/* (large, can be re-downloaded)
];
# Exclude Nextcloud's raw DB directory in favour of the pg_dump file
exclude = [
"${dataDir}/nextcloud/db"
"${dataDir}/restic-cache"
];
timerConfig = {
OnCalendar = cfg.schedule;
Persistent = true; # run on next boot if missed
};
pruneOpts = [
"--keep-daily ${cfg.pruneRetention.daily}"
"--keep-weekly ${cfg.pruneRetention.weekly}"
"--keep-monthly ${cfg.pruneRetention.monthly}"
];
};
# Wire the pre/post hooks around the restic job
systemd.services."restic-backups-homey" = {
requires = [ "homey-backup-pre.service" ];
after = [ "homey-backup-pre.service" ];
};
systemd.services."homey-backup-post" = {
after = [ "restic-backups-homey.service" ];
wantedBy = [ "restic-backups-homey.service" ];
};
};
}
+185
View File
@@ -0,0 +1,185 @@
{ config, lib, pkgs, homeyConfig, ... }:
# Caddy reverse proxy.
#
# Features:
# - DNS-01 ACME via Cloudflare API → real wildcard cert for *.home.zakobar.com
# - forward_auth to Authelia for protected vhosts
# - Plain reverse_proxy for public vhosts (authelia itself, nextcloud)
# - Listens on :80 (redirect) and :443 (TLS)
#
# Because nixpkgs ships Caddy without the cloudflare DNS plugin by default,
# we build a custom Caddy with it using the xcaddy wrapper from nixpkgs.
#
# Secrets consumed from sops:
# cloudflare/api_token
let
cfg = config.homey.caddy;
domain = homeyConfig.domain;
# Build Caddy with the Cloudflare DNS plugin.
# This compiles on the Pi (slow once, cached after).
caddyWithCloudflare = pkgs.caddy.override {
externalPlugins = [
{
name = "github.com/caddy-dns/cloudflare";
version = "89f16b99c18ef49c8bb470a82f895bce01cbaece";
}
];
vendorHash = lib.fakeHash; # replace with real hash after first build
};
# Reusable Authelia forward_auth snippet
# Returns a Caddyfile snippet block that applies forward_auth.
# copy_headers makes Authelia's Remote-* headers available downstream.
autheliaForwardAuth = ''
forward_auth localhost:9091 {
uri /api/verify?rd=https://auth.${domain}
copy_headers Remote-User Remote-Name Remote-Groups Remote-Email
# On auth failure, redirect to the authelia login page
@goauth status 401
handle_response @goauth {
redir https://auth.${domain}?rm={method} 302
}
}
'';
in
{
options.homey.caddy = {
enable = lib.mkEnableOption "Caddy reverse proxy";
acmeEmail = lib.mkOption {
type = lib.types.str;
default = "admin@zakobar.com";
description = "Email for Let's Encrypt ACME registration.";
};
};
config = lib.mkIf cfg.enable {
# -----------------------------------------------------------------------
# Secrets
# -----------------------------------------------------------------------
sops.secrets."cloudflare/api_token" = {
owner = config.services.caddy.user;
};
# -----------------------------------------------------------------------
# Caddy service
# -----------------------------------------------------------------------
services.caddy = {
enable = true;
package = caddyWithCloudflare;
# Global options
globalConfig = ''
email ${cfg.acmeEmail}
# Use Cloudflare DNS-01 challenge for wildcard cert
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
'';
# Each virtual host
virtualHosts = {
# ------------------------------------------------------------------
# Authelia — public, no auth gate (it IS the auth gate)
# ------------------------------------------------------------------
"auth.${domain}" = {
extraConfig = ''
reverse_proxy localhost:9091
'';
};
# ------------------------------------------------------------------
# Gitea — protected behind one_factor Authelia
# ------------------------------------------------------------------
"git.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
reverse_proxy localhost:3000
'';
};
# ------------------------------------------------------------------
# Nextcloud — public auth (Nextcloud manages its own users + LDAP)
# Authelia is not gating nextcloud directly because NC has its own
# login flow. We still want HTTPS.
# ------------------------------------------------------------------
"nextcloud.${domain}" = {
extraConfig = ''
# Redirect CardDAV/CalDAV discovery
redir /.well-known/carddav /remote.php/dav/ 301
redir /.well-known/caldav /remote.php/dav/ 301
# Large uploads (5 GB)
request_body {
max_size 5GB
}
reverse_proxy localhost:8080
'';
};
# ------------------------------------------------------------------
# phpLDAPadmin — two_factor, admins only (enforced by authelia policy)
# ------------------------------------------------------------------
"ldapadmin.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
reverse_proxy localhost:8081
'';
};
# ------------------------------------------------------------------
# Jellyfin — one_factor (added when enabled)
# ------------------------------------------------------------------
"jellyfin.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
reverse_proxy localhost:8096
'';
};
# ------------------------------------------------------------------
# Transmission — two_factor, admins only (enforced by authelia policy)
# ------------------------------------------------------------------
"torrent.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
reverse_proxy localhost:9091_transmission
'';
# NOTE: transmission uses 9091 too; we'll bind it to 9092 in its
# module to avoid a clash with authelia.
};
};
};
# -----------------------------------------------------------------------
# Pass Cloudflare token as env var to the caddy systemd unit
# -----------------------------------------------------------------------
systemd.services.caddy = {
serviceConfig = {
EnvironmentFile = pkgs.writeText "caddy-cf-env"
"CLOUDFLARE_API_TOKEN_FILE=${config.sops.secrets."cloudflare/api_token".path}";
# Caddy supports _FILE suffix for env vars via its secret file reader,
# but cloudflare plugin reads CLOUDFLARE_API_TOKEN directly.
# We write a wrapper ExecStartPre to populate the env var from the file:
ExecStartPre = [
(pkgs.writeShellScript "caddy-inject-cf-token" ''
export CLOUDFLARE_API_TOKEN=$(cat ${config.sops.secrets."cloudflare/api_token".path})
systemctl set-environment CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN"
'')
];
};
after = lib.mkAfter [ "podman-authelia.service" ];
wants = lib.mkAfter [ "podman-authelia.service" ];
};
# -----------------------------------------------------------------------
# Firewall — open HTTP + HTTPS (already in common.nix, explicit here too)
# -----------------------------------------------------------------------
networking.firewall.allowedTCPPorts = [ 80 443 ];
};
}
+77
View File
@@ -0,0 +1,77 @@
{ config, lib, pkgs, homeyConfig, ... }:
# Cloudflare Tunnel (cloudflared) — remote access without open inbound ports.
#
# Architecture:
# Internet → Cloudflare edge → cloudflared tunnel (outbound from Pi)
# → Caddy on localhost → service containers
#
# The tunnel is configured to route each hostname to Caddy's HTTPS listener.
# Caddy handles TLS and forward_auth; cloudflared just carries the traffic.
#
# Setup steps (one-time, done from the Cloudflare dashboard):
# 1. Go to Zero Trust → Networks → Tunnels → Create a tunnel
# 2. Name it (e.g. "pi-main")
# 3. Copy the tunnel token — add it to secrets.yaml as cloudflare/tunnel_token
# 4. In the tunnel's "Public Hostnames" config, add routes:
# auth.home.zakobar.com → http://localhost:80 (or https://localhost:443)
# git.home.zakobar.com → https://localhost:443
# nextcloud.home.zakobar.com → https://localhost:443
# ldapadmin.home.zakobar.com → https://localhost:443
# jellyfin.home.zakobar.com → https://localhost:443
# torrent.home.zakobar.com → https://localhost:443
# Set "No TLS Verify" = true (Caddy's cert is from Let's Encrypt but
# the hostname seen by cloudflared is localhost, so hostname verification
# would fail without this flag).
#
# The tunnel_token approach (--token) is the simplest: one secret, no config
# file needed on the Pi.
#
# Secrets consumed from sops:
# cloudflare/tunnel_token
let
cfg = config.homey.cloudflared;
in
{
options.homey.cloudflared = {
enable = lib.mkEnableOption "Cloudflare Tunnel for remote access";
};
config = lib.mkIf cfg.enable {
# -----------------------------------------------------------------------
# Secrets
# -----------------------------------------------------------------------
sops.secrets."cloudflare/tunnel_token" = { owner = "cloudflared"; };
# -----------------------------------------------------------------------
# cloudflared service
# NixOS 24.11 ships services.cloudflared natively.
# -----------------------------------------------------------------------
services.cloudflared = {
enable = true;
tunnels = {
"pi-main" = {
# credentialsFile is not used with token-based auth;
# the token is passed via environment variable instead.
# We override the systemd unit below to inject it.
default = "http_status:404";
};
};
};
# Inject the tunnel token from the sops secret file
systemd.services."cloudflared-tunnel-pi-main" = {
serviceConfig = {
ExecStart = lib.mkForce (pkgs.writeShellScript "cloudflared-start" ''
exec ${pkgs.cloudflared}/bin/cloudflared tunnel \
--no-autoupdate \
run \
--token "$(cat ${config.sops.secrets."cloudflare/tunnel_token".path})"
'');
};
after = lib.mkAfter [ "caddy.service" ];
wants = lib.mkAfter [ "caddy.service" ];
};
};
}
+117
View File
@@ -0,0 +1,117 @@
{ config, lib, pkgs, homeyConfig, ... }:
# Common configuration shared by every host in the homey ecosystem.
# Hardware-specific settings (disk layout, device trees, etc.) go in
# hosts/<name>/hardware.nix instead.
{
# -------------------------------------------------------------------------
# Nix / flakes
# -------------------------------------------------------------------------
nix = {
settings = {
experimental-features = [ "nix-command" "flakes" ];
# Save disk space on Pi
auto-optimise-store = true;
};
# Weekly garbage collection — keeps the system from filling the SD card
gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 14d";
};
};
# Allow unfree packages (e.g. cloudflared binary)
nixpkgs.config.allowUnfree = true;
# -------------------------------------------------------------------------
# Boot — set in hardware.nix; this is just a safe default
# -------------------------------------------------------------------------
# boot.loader is intentionally left to hardware.nix
# -------------------------------------------------------------------------
# Locale / timezone
# -------------------------------------------------------------------------
time.timeZone = homeyConfig.timezone;
i18n.defaultLocale = "en_US.UTF-8";
# -------------------------------------------------------------------------
# Networking
# -------------------------------------------------------------------------
networking = {
# hostname is set per-host in default.nix
firewall = {
enable = true;
allowedTCPPorts = [
22 # SSH
80 # Caddy HTTP (redirect to HTTPS or ACME challenge)
443 # Caddy HTTPS
];
};
# Use systemd-resolved for DNS — supports mDNS and local overrides
nameservers = [ "1.1.1.1" "8.8.8.8" ];
};
# -------------------------------------------------------------------------
# SSH
# -------------------------------------------------------------------------
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
};
};
# -------------------------------------------------------------------------
# Container runtime — podman (rootless-capable, no daemon needed)
# -------------------------------------------------------------------------
virtualisation.podman = {
enable = true;
dockerCompat = true; # allow `docker` CLI commands against podman
defaultNetwork.settings.dns_enabled = true;
};
# -------------------------------------------------------------------------
# Core packages available on every host
# -------------------------------------------------------------------------
environment.systemPackages = with pkgs; [
git
vim
htop
curl
wget
rsync
lsof
sops # secret editing
age # key generation for sops
restic # backup (CLI, also used by services.restic)
podman-compose
];
# -------------------------------------------------------------------------
# sops-nix global config — point at the secrets file and the host's age key
# -------------------------------------------------------------------------
sops = {
defaultSopsFile = ../secrets/secrets.yaml;
# The age private key must be present on the host at this path.
# Generate on the Pi with: age-keygen -o /var/lib/sops-nix/key.txt
# Then add the PUBLIC key to secrets/.sops.yaml before encrypting.
age.keyFile = "/var/lib/sops-nix/key.txt";
};
# -------------------------------------------------------------------------
# Admin user — adjust username / SSH key in hosts/<name>/default.nix
# -------------------------------------------------------------------------
users.mutableUsers = false; # all user config must be declared here
# The actual admin user is declared in hosts/<name>/default.nix so the
# SSH authorized key can be host-specific.
# -------------------------------------------------------------------------
# System state version — do not change after first install
# (tracks NixOS backwards-compat markers)
# -------------------------------------------------------------------------
system.stateVersion = "24.11";
}
+200
View File
@@ -0,0 +1,200 @@
{ config, lib, pkgs, homeyConfig, ... }:
# Authelia — SSO gateway.
#
# Connects to OpenLDAP on 127.0.0.1:389.
# Exposes port 9091 on localhost; Caddy reverse-proxies it and provides
# the forward_auth endpoint for protected vhosts.
#
# Volume layout:
# <dataDir>/authelia/config/ → /config (sqlite db, notification log, etc.)
#
# The configuration file is rendered by Nix (no Go templates) and written
# to a NixOS-managed path, then bind-mounted read-only into the container.
#
# Secrets consumed from sops:
# authelia/jwt_secret
# authelia/session_secret
# authelia/storage_encryption_key
# openldap/ro_password (shared with openldap module)
let
cfg = config.homey.authelia;
dataDir = config.homey.storage.mountPoint;
domain = homeyConfig.domain;
# LDAP base DN derived from domain: home.zakobar.com → dc=home,dc=zakobar,dc=com
ldapBaseDN = lib.concatStringsSep ","
(map (p: "dc=${p}") (lib.splitString "." domain));
# The authelia config is written as a Nix string so all values are
# resolved at build time except for secrets, which are injected at
# runtime via a wrapper script (same pattern as openldap).
autheliaConfig = ''
###############################################################
# Authelia configuration #
# Generated by NixOS do not edit by hand #
###############################################################
theme: "light"
log:
level: "info"
# jwt_secret injected at runtime via env var AUTHELIA_JWT_SECRET_FILE
authentication_backend:
ldap:
implementation: "custom"
url: "ldap://127.0.0.1:389"
timeout: "5s"
start_tls: false
base_dn: "${ldapBaseDN}"
users_filter: "({username_attribute}={input})"
username_attribute: "uid"
additional_users_dn: "ou=users"
groups_filter: "(&(uniquemember=uid={input},ou=users,${ldapBaseDN})(objectclass=groupOfUniqueNames))"
group_name_attribute: "cn"
additional_groups_dn: "ou=groups"
mail_attribute: "mail"
display_name_attribute: "uid"
permit_referrals: false
permit_unauthenticated_bind: false
user: "cn=readonly,${ldapBaseDN}"
# password injected at runtime via AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE
totp:
issuer: "${domain}"
disable: false
session:
name: authelia_session
# secret injected at runtime via AUTHELIA_SESSION_SECRET_FILE
expiration: 3600
inactivity: 7200
domain: "${domain}"
storage:
local:
path: "/config/db.sqlite3"
# encryption_key injected at runtime via AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE
access_control:
default_policy: "deny"
rules:
- domain:
- "auth.${domain}"
policy: "bypass"
- domain:
- "ldapadmin.${domain}"
subject:
- "group:admins"
policy: "two_factor"
- domain:
- "ldapadmin.${domain}"
policy: "deny"
- domain:
- "torrent.${domain}"
subject:
- "group:admins"
policy: "two_factor"
- domain:
- "torrent.${domain}"
policy: "deny"
- domain:
- "git.${domain}"
policy: "one_factor"
- domain:
- "nextcloud.${domain}"
policy: "one_factor"
- domain:
- "jellyfin.${domain}"
policy: "one_factor"
notifier:
filesystem:
filename: "/config/emails.txt"
ntp:
address: "udp://time.cloudflare.com:123"
version: 3
max_desync: "3s"
disable_startup_check: false
disable_failure: true
'';
in
{
options.homey.authelia = {
enable = lib.mkEnableOption "Authelia SSO gateway";
image = lib.mkOption {
type = lib.types.str;
default = "docker.io/authelia/authelia:latest";
};
port = lib.mkOption {
type = lib.types.port;
default = 9091;
description = "Host port Authelia listens on (bound to 127.0.0.1).";
};
};
config = lib.mkIf cfg.enable {
# -----------------------------------------------------------------------
# Secrets
# -----------------------------------------------------------------------
sops.secrets."authelia/jwt_secret" = { owner = "root"; };
sops.secrets."authelia/session_secret" = { owner = "root"; };
sops.secrets."authelia/storage_encryption_key" = { owner = "root"; };
# openldap/ro_password is declared in openldap.nix; reference it here too
# (sops-nix deduplicates identical declarations)
sops.secrets."openldap/ro_password" = { owner = "root"; };
# -----------------------------------------------------------------------
# Write the config file into /etc (read-only in the container)
# -----------------------------------------------------------------------
environment.etc."authelia/configuration.yml" = {
text = autheliaConfig;
mode = "0444";
};
# -----------------------------------------------------------------------
# Container
# -----------------------------------------------------------------------
virtualisation.oci-containers.containers.authelia = {
image = cfg.image;
ports = [ "127.0.0.1:${toString cfg.port}:9091" ];
environment = {
TZ = homeyConfig.timezone;
# Tell authelia to read secrets from files (its native mechanism)
AUTHELIA_JWT_SECRET_FILE = "/run/secrets/jwt_secret";
AUTHELIA_SESSION_SECRET_FILE = "/run/secrets/session_secret";
AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE = "/run/secrets/storage_encryption_key";
AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE = "/run/secrets/ldap_ro_password";
};
volumes = [
"/etc/authelia/configuration.yml:/config/configuration.yml:ro"
"${dataDir}/authelia/config:/config"
# Mount sops secret files into the container under /run/secrets/
"${config.sops.secrets."authelia/jwt_secret".path}:/run/secrets/jwt_secret:ro"
"${config.sops.secrets."authelia/session_secret".path}:/run/secrets/session_secret:ro"
"${config.sops.secrets."authelia/storage_encryption_key".path}:/run/secrets/storage_encryption_key:ro"
"${config.sops.secrets."openldap/ro_password".path}:/run/secrets/ldap_ro_password:ro"
];
extraOptions = [
"--network=host"
"--hostname=authelia"
];
};
# -----------------------------------------------------------------------
# Systemd — wait for openldap and external HD
# -----------------------------------------------------------------------
systemd.services."podman-authelia" = {
after = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" ];
requires = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" ];
};
};
}
+198
View File
@@ -0,0 +1,198 @@
{ config, lib, pkgs, homeyConfig, ... }:
# Gitea — self-hosted Git service.
#
# Auth model: LDAP authentication is configured through Gitea's admin UI
# (or CLI) after first start. Reverse proxy auth headers from Caddy/Authelia
# handle transparent login.
#
# Volume layout:
# <dataDir>/gitea/data/ → /data (repos, sqlite db, avatars, lfs, etc.)
#
# The app.ini is rendered by Nix and bind-mounted read-only.
#
# Secrets consumed from sops:
# gitea/admin_password
# gitea/lfs_jwt_secret
# gitea/oauth2_jwt_secret
# gitea/internal_token
let
cfg = config.homey.gitea;
dataDir = config.homey.storage.mountPoint;
domain = homeyConfig.domain;
# Gitea app.ini — generated at build time.
# Secrets that Gitea reads from env vars are referenced as env var names here.
# The actual values are injected by the ExecStartPre wrapper below.
giteaAppIni = ''
APP_NAME = ${homeyConfig.organization}
RUN_MODE = prod
RUN_USER = git
WORK_PATH = /data/gitea
[repository]
ROOT = /data/git/repositories
[repository.local]
LOCAL_COPY_PATH = /data/gitea/tmp/local-repo
[repository.upload]
TEMP_PATH = /data/gitea/uploads
[server]
APP_DATA_PATH = /data/gitea
DOMAIN = git.${domain}
HTTP_PORT = 3000
ROOT_URL = https://git.${domain}/
DISABLE_SSH = true
LFS_START_SERVER = true
; LFS_JWT_SECRET injected at container start via env var / startup script
LFS_JWT_SECRET = __GITEA_LFS_JWT_SECRET__
OFFLINE_MODE = false
[lfs]
PATH = /data/git/lfs
[database]
DB_TYPE = sqlite3
PATH = /data/gitea/gitea.db
LOG_SQL = false
[indexer]
ISSUE_INDEXER_PATH = /data/gitea/indexers/issues.bleve
[session]
PROVIDER_CONFIG = /data/gitea/sessions
PROVIDER = file
[picture]
AVATAR_UPLOAD_PATH = /data/gitea/avatars
REPOSITORY_AVATAR_UPLOAD_PATH = /data/gitea/repo-avatars
DISABLE_GRAVATAR = false
[attachment]
PATH = /data/gitea/attachments
[log]
MODE = console
LEVEL = info
ROUTER = console
ROOT_PATH = /data/gitea/log
[security]
INSTALL_LOCK = true
REVERSE_PROXY_LIMIT = 1
REVERSE_PROXY_TRUSTED_PROXIES = *
; INTERNAL_TOKEN injected at container start
INTERNAL_TOKEN = __GITEA_INTERNAL_TOKEN__
[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = false
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false
ALLOW_ONLY_EXTERNAL_REGISTRATION = true
ENABLE_CAPTCHA = false
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
DEFAULT_ENABLE_TIMETRACKING = true
NO_REPLY_ADDRESS = noreply.localhost
ENABLE_REVERSE_PROXY_AUTHENTICATION = true
ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = true
[mailer]
ENABLED = false
[openid]
ENABLE_OPENID_SIGNIN = false
ENABLE_OPENID_SIGNUP = false
[oauth2]
ENABLE = false
; JWT_SECRET injected at container start
JWT_SECRET = __GITEA_OAUTH2_JWT_SECRET__
'';
in
{
options.homey.gitea = {
enable = lib.mkEnableOption "Gitea Git server";
image = lib.mkOption {
type = lib.types.str;
default = "docker.io/gitea/gitea:latest";
};
port = lib.mkOption {
type = lib.types.port;
default = 3000;
description = "Host port Gitea listens on (bound to 127.0.0.1).";
};
};
config = lib.mkIf cfg.enable {
# -----------------------------------------------------------------------
# Secrets
# -----------------------------------------------------------------------
sops.secrets."gitea/admin_password" = { owner = "root"; };
sops.secrets."gitea/lfs_jwt_secret" = { owner = "root"; };
sops.secrets."gitea/oauth2_jwt_secret" = { owner = "root"; };
sops.secrets."gitea/internal_token" = { owner = "root"; };
# -----------------------------------------------------------------------
# Write the app.ini template to /etc (will be processed by ExecStartPre)
# -----------------------------------------------------------------------
environment.etc."gitea/app.ini.tpl" = {
text = giteaAppIni;
mode = "0444";
};
# -----------------------------------------------------------------------
# Container
# -----------------------------------------------------------------------
virtualisation.oci-containers.containers.gitea = {
image = cfg.image;
ports = [ "127.0.0.1:${toString cfg.port}:3000" ];
environment = {
USER_UID = "1000";
USER_GID = "1000";
# Tell gitea where to look for the config
GITEA_CUSTOM = "/data/gitea";
};
volumes = [
"${dataDir}/gitea/data:/data"
# The processed app.ini is written by ExecStartPre into /run/gitea-conf/
"/run/gitea-conf/app.ini:/data/gitea/conf/app.ini:ro"
];
extraOptions = [ "--network=host" ];
};
# -----------------------------------------------------------------------
# ExecStartPre: substitute secret placeholders into the ini template
# -----------------------------------------------------------------------
systemd.services."podman-gitea" = {
serviceConfig = {
ExecStartPre = [
(pkgs.writeShellScript "gitea-build-config" ''
set -euo pipefail
install -d -m 700 /run/gitea-conf
LFS=$(cat ${config.sops.secrets."gitea/lfs_jwt_secret".path})
OAUTH=$(cat ${config.sops.secrets."gitea/oauth2_jwt_secret".path})
TOKEN=$(cat ${config.sops.secrets."gitea/internal_token".path})
sed \
-e "s|__GITEA_LFS_JWT_SECRET__|$LFS|g" \
-e "s|__GITEA_OAUTH2_JWT_SECRET__|$OAUTH|g" \
-e "s|__GITEA_INTERNAL_TOKEN__|$TOKEN|g" \
/etc/gitea/app.ini.tpl > /run/gitea-conf/app.ini
chmod 444 /run/gitea-conf/app.ini
'')
];
};
after = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" ];
requires = lib.mkAfter [ "mnt-data.mount" ];
};
};
}
+55
View File
@@ -0,0 +1,55 @@
{ config, lib, pkgs, homeyConfig, ... }:
# Jellyfin — media server. (Deferred — enable when ready.)
#
# Volume layout:
# <dataDir>/jellyfin/config/ → /config
# <dataDir>/media/movies/ → /data/movies
# <dataDir>/media/tvshows/ → /data/tvshows
let
cfg = config.homey.jellyfin;
dataDir = config.homey.storage.mountPoint;
domain = homeyConfig.domain;
in
{
options.homey.jellyfin = {
enable = lib.mkEnableOption "Jellyfin media server";
image = lib.mkOption {
type = lib.types.str;
default = "docker.io/jellyfin/jellyfin:latest";
};
port = lib.mkOption {
type = lib.types.port;
default = 8096;
};
};
config = lib.mkIf cfg.enable {
virtualisation.oci-containers.containers.jellyfin = {
image = cfg.image;
ports = [ "127.0.0.1:${toString cfg.port}:8096" ];
environment = {
JELLYFIN_PublishedServerUrl = "https://jellyfin.${domain}";
PUID = "1000";
PGID = "1000";
};
volumes = [
"${dataDir}/jellyfin/config:/config"
"${dataDir}/media/movies:/data/movies:ro"
"${dataDir}/media/tvshows:/data/tvshows:ro"
];
extraOptions = [ "--network=host" ];
};
systemd.services."podman-jellyfin" = {
after = lib.mkAfter [ "mnt-data.mount" ];
requires = lib.mkAfter [ "mnt-data.mount" ];
};
};
}
+135
View File
@@ -0,0 +1,135 @@
{ config, lib, pkgs, homeyConfig, ... }:
# Nextcloud + PostgreSQL.
#
# Two containers:
# nextcloud-postgres — PostgreSQL, bound to localhost:5432
# nextcloud — Nextcloud PHP-FPM + Apache, bound to localhost:8080
#
# Volume layout:
# <dataDir>/nextcloud/db/ → /var/lib/postgresql/data (postgres)
# <dataDir>/nextcloud/html/ → /var/www/html (nextcloud)
#
# Secrets consumed from sops:
# nextcloud/admin_password
# nextcloud/postgres_password
let
cfg = config.homey.nextcloud;
dataDir = config.homey.storage.mountPoint;
domain = homeyConfig.domain;
in
{
options.homey.nextcloud = {
enable = lib.mkEnableOption "Nextcloud file server";
image = lib.mkOption {
type = lib.types.str;
default = "docker.io/nextcloud:latest";
};
postgresImage = lib.mkOption {
type = lib.types.str;
default = "docker.io/postgres:16";
};
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = "Host port Nextcloud listens on (bound to 127.0.0.1).";
};
postgresPort = lib.mkOption {
type = lib.types.port;
default = 5432;
description = "Host port PostgreSQL listens on (bound to 127.0.0.1).";
};
};
config = lib.mkIf cfg.enable {
# -----------------------------------------------------------------------
# Secrets
# -----------------------------------------------------------------------
sops.secrets."nextcloud/admin_password" = { owner = "root"; };
sops.secrets."nextcloud/postgres_password" = { owner = "root"; };
# -----------------------------------------------------------------------
# PostgreSQL container
# -----------------------------------------------------------------------
virtualisation.oci-containers.containers.nextcloud-postgres = {
image = cfg.postgresImage;
ports = [ "127.0.0.1:${toString cfg.postgresPort}:5432" ];
environment = {
POSTGRES_DB = "nextcloud_db";
POSTGRES_USER = "postgres";
# Password injected via env file
};
volumes = [
"${dataDir}/nextcloud/db:/var/lib/postgresql/data"
];
extraOptions = [ "--network=host" ];
};
systemd.services."podman-nextcloud-postgres" = {
serviceConfig = {
ExecStartPre = [
(pkgs.writeShellScript "nc-postgres-secrets-env" ''
set -euo pipefail
install -m 600 /dev/null /run/nc-postgres-secrets.env
echo "POSTGRES_PASSWORD=$(cat ${config.sops.secrets."nextcloud/postgres_password".path})" \
>> /run/nc-postgres-secrets.env
'')
];
EnvironmentFile = "/run/nc-postgres-secrets.env";
};
postStop = "rm -f /run/nc-postgres-secrets.env";
after = lib.mkAfter [ "mnt-data.mount" ];
requires = lib.mkAfter [ "mnt-data.mount" ];
};
# -----------------------------------------------------------------------
# Nextcloud container
# -----------------------------------------------------------------------
virtualisation.oci-containers.containers.nextcloud = {
image = cfg.image;
ports = [ "127.0.0.1:${toString cfg.port}:80" ];
environment = {
POSTGRES_HOST = "127.0.0.1";
POSTGRES_DB = "nextcloud_db";
POSTGRES_USER = "postgres";
NEXTCLOUD_ADMIN_USER = "admin";
NEXTCLOUD_TRUSTED_DOMAINS = "nextcloud.${domain}";
OVERWRITEPROTOCOL = "https";
OVERWRITECLIURL = "https://nextcloud.${domain}";
# Passwords injected via env file
};
volumes = [
"${dataDir}/nextcloud/html:/var/www/html"
];
extraOptions = [ "--network=host" ];
};
systemd.services."podman-nextcloud" = {
serviceConfig = {
ExecStartPre = [
(pkgs.writeShellScript "nc-secrets-env" ''
set -euo pipefail
install -m 600 /dev/null /run/nc-secrets.env
echo "POSTGRES_PASSWORD=$(cat ${config.sops.secrets."nextcloud/postgres_password".path})" >> /run/nc-secrets.env
echo "NEXTCLOUD_ADMIN_PASSWORD=$(cat ${config.sops.secrets."nextcloud/admin_password".path})" >> /run/nc-secrets.env
'')
];
EnvironmentFile = "/run/nc-secrets.env";
};
postStop = "rm -f /run/nc-secrets.env";
after = lib.mkAfter [ "mnt-data.mount" "podman-nextcloud-postgres.service" ];
requires = lib.mkAfter [ "mnt-data.mount" "podman-nextcloud-postgres.service" ];
};
};
}
+116
View File
@@ -0,0 +1,116 @@
{ config, lib, pkgs, homeyConfig, ... }:
# OpenLDAP — central identity provider.
#
# Runs as a podman container (osixia/openldap).
# Listens on localhost:389 only — not exposed to the outside world.
# Authelia and other services connect to it over the container network (127.0.0.1).
#
# Volume layout on host:
# <dataDir>/openldap/etc-ldap-slapd.d/ → /etc/ldap/slapd.d (config DB)
# <dataDir>/openldap/var-lib-ldap/ → /var/lib/ldap (data)
#
# Secrets consumed from sops:
# openldap/admin_password
# openldap/config_password
# openldap/ro_password
let
cfg = config.homey.openldap;
dataDir = config.homey.storage.mountPoint;
in
{
options.homey.openldap = {
enable = lib.mkEnableOption "OpenLDAP identity provider";
image = lib.mkOption {
type = lib.types.str;
default = "docker.io/osixia/openldap:latest";
description = "Container image to use for OpenLDAP.";
};
port = lib.mkOption {
type = lib.types.port;
default = 389;
description = "Host port OpenLDAP listens on (bound to 127.0.0.1).";
};
};
config = lib.mkIf cfg.enable {
# -----------------------------------------------------------------------
# Secrets
# -----------------------------------------------------------------------
sops.secrets."openldap/admin_password" = { owner = "root"; };
sops.secrets."openldap/config_password" = { owner = "root"; };
sops.secrets."openldap/ro_password" = { owner = "root"; };
# -----------------------------------------------------------------------
# Container
# -----------------------------------------------------------------------
virtualisation.oci-containers.containers.openldap = {
image = cfg.image;
# Bind only to localhost — no external exposure
ports = [ "127.0.0.1:${toString cfg.port}:389" ];
environment = {
LDAP_ORGANISATION = homeyConfig.organization;
LDAP_DOMAIN = homeyConfig.domain;
LDAP_ADMIN_USERNAME = "admin";
LDAP_READONLY_USER = "true";
# TLS disabled — traffic stays on localhost
LDAP_TLS = "false";
};
# Inject passwords from sops-managed secret files
environmentFiles = []; # we use secretFiles below instead
# sops writes secret values to files; we read them into env vars
# via a wrapper script run as ExecStartPre (see systemd override below).
# Podman's --env-file doesn't support arbitrary paths, so we use
# a secrets tmpfile approach via the systemd unit override.
volumes = [
"${dataDir}/openldap/etc-ldap-slapd.d:/etc/ldap/slapd.d"
"${dataDir}/openldap/var-lib-ldap:/var/lib/ldap"
];
extraOptions = [
"--network=host" # simplest for single-host: services talk on 127.0.0.1
"--hostname=openldap"
];
};
# -----------------------------------------------------------------------
# Systemd override to inject sops secrets as env vars
# -----------------------------------------------------------------------
# podman containers are managed by systemd units named
# podman-<container-name>.service
systemd.services."podman-openldap" = {
serviceConfig = {
# Write an env file with secret values before the container starts,
# then pass it to podman run via EnvironmentFile.
ExecStartPre = [
(pkgs.writeShellScript "openldap-secrets-env" ''
set -euo pipefail
install -m 600 /dev/null /run/openldap-secrets.env
echo "LDAP_ADMIN_PASSWORD=$(cat ${config.sops.secrets."openldap/admin_password".path})" >> /run/openldap-secrets.env
echo "LDAP_CONFIG_PASSWORD=$(cat ${config.sops.secrets."openldap/config_password".path})" >> /run/openldap-secrets.env
echo "LDAP_READONLY_USER_PASSWORD=$(cat ${config.sops.secrets."openldap/ro_password".path})" >> /run/openldap-secrets.env
'')
];
EnvironmentFile = "/run/openldap-secrets.env";
};
# Clean up the env file on stop
postStop = "rm -f /run/openldap-secrets.env";
# Wait for the external HD to be mounted before starting
after = lib.mkAfter [ "mnt-data.mount" ];
requires = lib.mkAfter [ "mnt-data.mount" ];
};
# -----------------------------------------------------------------------
# Firewall — openldap port is NOT opened externally (localhost only)
# -----------------------------------------------------------------------
# No firewall rule needed; bound to 127.0.0.1.
};
}
+46
View File
@@ -0,0 +1,46 @@
{ config, lib, pkgs, homeyConfig, ... }:
# phpLDAPadmin — web UI for OpenLDAP management.
#
# Stateless container (no persistent volumes needed).
# Protected by Authelia two_factor, admins-only policy (defined in authelia.nix).
# Bound to localhost:8081; Caddy reverse-proxies it.
let
cfg = config.homey.phpldapadmin;
in
{
options.homey.phpldapadmin = {
enable = lib.mkEnableOption "phpLDAPadmin web interface";
image = lib.mkOption {
type = lib.types.str;
default = "docker.io/osixia/phpldapadmin:latest";
};
port = lib.mkOption {
type = lib.types.port;
default = 8081;
description = "Host port phpLDAPadmin listens on (bound to 127.0.0.1).";
};
};
config = lib.mkIf cfg.enable {
virtualisation.oci-containers.containers.phpldapadmin = {
image = cfg.image;
ports = [ "127.0.0.1:${toString cfg.port}:80" ];
environment = {
PHPLDAPADMIN_HTTPS = "false";
PHPLDAPADMIN_LDAP_HOSTS = "127.0.0.1"; # openldap on host network
};
extraOptions = [ "--network=host" ];
};
systemd.services."podman-phpldapadmin" = {
after = lib.mkAfter [ "podman-openldap.service" ];
wants = lib.mkAfter [ "podman-openldap.service" ];
};
};
}
+61
View File
@@ -0,0 +1,61 @@
{ config, lib, pkgs, homeyConfig, ... }:
# Transmission — BitTorrent client. (Deferred — enable when ready.)
#
# NOTE: Transmission's web UI also runs on port 9091. To avoid clashing
# with Authelia (also 9091), this module binds Transmission to 9092.
#
# Volume layout:
# <dataDir>/transmission/config/ → /config
# <dataDir>/media/movies/ → /downloads/movies
# <dataDir>/media/tvshows/ → /downloads/tvshows
# <dataDir>/media/general/ → /downloads/general
# <dataDir>/media/complete/ → /downloads/complete
let
cfg = config.homey.transmission;
dataDir = config.homey.storage.mountPoint;
in
{
options.homey.transmission = {
enable = lib.mkEnableOption "Transmission torrent client";
image = lib.mkOption {
type = lib.types.str;
default = "docker.io/linuxserver/transmission:latest";
};
port = lib.mkOption {
type = lib.types.port;
default = 9092;
description = "Host port for Transmission web UI (9092 to avoid clash with authelia@9091).";
};
};
config = lib.mkIf cfg.enable {
virtualisation.oci-containers.containers.transmission = {
image = cfg.image;
ports = [ "127.0.0.1:${toString cfg.port}:9091" ];
environment = {
PUID = "1000";
PGID = "1000";
};
volumes = [
"${dataDir}/transmission/config:/config"
"${dataDir}/media/movies:/downloads/movies"
"${dataDir}/media/tvshows:/downloads/tvshows"
"${dataDir}/media/general:/downloads/general"
"${dataDir}/media/complete:/downloads/complete"
];
extraOptions = [ "--network=host" ];
};
systemd.services."podman-transmission" = {
after = lib.mkAfter [ "mnt-data.mount" ];
requires = lib.mkAfter [ "mnt-data.mount" ];
};
};
}
+105
View File
@@ -0,0 +1,105 @@
{ config, lib, pkgs, ... }:
# External hard-drive storage module.
#
# Each host sets:
# homey.storage.device = "/dev/disk/by-id/usb-WD_..."; (by-id is stable across reboots)
# homey.storage.mountPoint = "/mnt/data"; (default)
#
# All service data lives under <mountPoint>/<service-name>/, so the whole
# dataset can be browsed, backed up, or restored with plain filesystem tools.
#
# Directory layout under mountPoint:
# openldap/
# etc-ldap-slapd.d/ ← /etc/ldap/slapd.d in container
# var-lib-ldap/ ← /var/lib/ldap in container
# authelia/
# config/ ← /config in container (sqlite db etc.)
# gitea/
# data/ ← /data in container
# nextcloud/
# html/ ← /var/www/html in container
# db/ ← /var/lib/postgresql/data in postgres container
# jellyfin/
# config/
# media/
# movies/
# tvshows/
# general/
# complete/
# transmission/
# config/
# restic-cache/ ← restic local cache (not the backup destination)
let
cfg = config.homey.storage;
in
{
options.homey.storage = {
device = lib.mkOption {
type = lib.types.str;
example = "/dev/disk/by-id/usb-WD_Elements_12345-0:0";
description = ''
Block device for the external hard drive.
Use /dev/disk/by-id/ paths for stable identification across reboots.
Leave empty to skip automount (useful during initial setup).
'';
default = "";
};
mountPoint = lib.mkOption {
type = lib.types.str;
default = "/mnt/data";
description = "Where the external HD is mounted. All service data lives here.";
};
fsType = lib.mkOption {
type = lib.types.str;
default = "ext4";
description = "Filesystem type of the external drive.";
};
};
config = lib.mkIf (cfg.device != "") {
# Mount the external drive
fileSystems."${cfg.mountPoint}" = {
device = cfg.device;
fsType = cfg.fsType;
options = [
"defaults"
"nofail" # Don't block boot if drive is absent
"noatime" # Better performance / less SD wear
"x-systemd.automount"
"x-systemd.idle-timeout=0"
];
};
# Ensure the mount point directory exists
systemd.tmpfiles.rules = [
"d ${cfg.mountPoint} 0755 root root -"
# Service subdirectories — created on boot so containers can start
# even before any data is restored into them.
"d ${cfg.mountPoint}/openldap 0750 root root -"
"d ${cfg.mountPoint}/openldap/etc-ldap-slapd.d 0750 root root -"
"d ${cfg.mountPoint}/openldap/var-lib-ldap 0750 root root -"
"d ${cfg.mountPoint}/authelia 0750 root root -"
"d ${cfg.mountPoint}/authelia/config 0750 root root -"
"d ${cfg.mountPoint}/gitea 0750 root root -"
"d ${cfg.mountPoint}/gitea/data 0750 root root -"
"d ${cfg.mountPoint}/nextcloud 0750 root root -"
"d ${cfg.mountPoint}/nextcloud/html 0750 root root -"
"d ${cfg.mountPoint}/nextcloud/db 0750 root root -"
"d ${cfg.mountPoint}/jellyfin 0750 root root -"
"d ${cfg.mountPoint}/jellyfin/config 0750 root root -"
"d ${cfg.mountPoint}/media 0755 root root -"
"d ${cfg.mountPoint}/media/movies 0755 root root -"
"d ${cfg.mountPoint}/media/tvshows 0755 root root -"
"d ${cfg.mountPoint}/media/general 0755 root root -"
"d ${cfg.mountPoint}/media/complete 0755 root root -"
"d ${cfg.mountPoint}/transmission 0750 root root -"
"d ${cfg.mountPoint}/transmission/config 0750 root root -"
"d ${cfg.mountPoint}/restic-cache 0700 root root -"
];
};
}
-184
View File
@@ -1,184 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SRC="${SRC:-/mnt/replicas}"
DEST="${DEST:-/mnt2/homey-backup}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
MANIFEST="$DEST/manifest.json"
PVC_MAPPING=(
"pvc-0310a337-9642-464b-a458-fcb3439328e7-fbc07d5a:ldap-pvc"
"pvc-1cdc51ee-b965-4cab-baf7-077cc6df6f11-0fcfb9cd:authelia-pvc"
"pvc-4888bf84-62c8-4340-adbc-cb31073d8fd2-d065d20b:gitea-pvc"
"pvc-5c1f48e3-346f-4c35-8e6a-8fc0c4c3a842-96d72815:nextcloud-data-pvc"
"pvc-c5b28179-1b9c-462a-be5b-05c4f0bb36ca-5f2dbf4d:nextcloud-postgres-pvc"
"pvc-7f73ee94-5583-4e4a-9788-cba054214b1c-f767850a:radicale-pvc"
"pvc-9e75f35a-27c3-4251-b25a-1a876f82f6c7-c9c8b185:jellyfin-config-pvc"
"pvc-dfe2aa08-bbb8-423b-9001-fb6aea181597-baf06a7f:jellyfin-data-pvc"
"pvc-dd4a069a-a638-49c0-8c95-f954510816e5-7e81a6f6:transmission-config-pvc"
"pvc-e4ba414d-d9c2-4927-b0ae-f6bfb90ce311-a0963101:unknown-pvc-1"
"pvc-ec6afe10-aca3-42ce-9d89-32fc4ac77f9a-8d6baa34:unknown-pvc-2"
)
progress_bar() {
local current=$1
local total=$2
local width=40
local percent=$((current * 100 / total))
local filled=$((current * width / total))
local empty=$((width - filled))
printf "\r["
printf "%${filled}s" | tr ' ' '='
printf "%${empty}s" | tr ' ' ' '
printf "] %3d%% (%d/%d)" "$percent" "$current" "$total"
}
get_pvc_name() {
local pvc_id="$1"
for mapping in "${PVC_MAPPING[@]}"; do
if [[ "$mapping" == "$pvc_id:"* ]]; then
echo "${mapping#*:}"
return
fi
done
echo "unknown"
}
echo "========================================"
echo " Longhorn Volume Backup Tool"
echo "========================================"
echo ""
echo "Source: $SRC"
echo "Destination: $DEST"
echo "Timestamp: $TIMESTAMP"
echo ""
mkdir -p "$DEST/volumes"
mkdir -p "$DEST/metadata"
VOLUMES=()
TOTAL_SIZE=0
echo "Scanning volumes..."
for pvc_dir in "$SRC"/*/; do
pvc_name=$(basename "$pvc_dir")
friendly_name=$(get_pvc_name "$pvc_name")
VOLUMES+=("$pvc_name:$friendly_name")
size=$(sudo du -sb "$pvc_dir" 2>/dev/null | awk '{print $1}' || echo "0")
TOTAL_SIZE=$((TOTAL_SIZE + size))
printf " %-50s %s\n" "$friendly_name" "$(numfmt --to=iec-i --suffix=B "$size" 2>/dev/null || echo "${size}B")"
done
TOTAL_VOLUMES=${#VOLUMES[@]}
echo ""
echo "Found $TOTAL_VOLUMES volumes, total size: $(numfmt --to=iec-i --suffix=B "$TOTAL_SIZE")"
echo ""
read -p "Continue with backup? [y/N] " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
echo ""
echo "Starting backup..."
echo ""
COPIED_SIZE=0
START_TIME=$(date +%s)
for i in "${!VOLUMES[@]}"; do
volume="${VOLUMES[$i]}"
pvc_name="${volume%%:*}"
friendly_name="${volume#*:}"
CURRENT=$((i + 1))
progress_bar "$CURRENT" "$TOTAL_VOLUMES"
echo " - $friendly_name"
sudo rsync -a --no-owner --no-group --info=progress2 \
"$SRC/$pvc_name/" \
"$DEST/volumes/$pvc_name/" 2>&1 | while read -r line; do
if [[ "$line" =~ to-chk=*([0-9]+)/([0-9]+) ]]; then
printf "\r %s" "$line"
fi
done
sudo chown -R "$USER:$USER" "$DEST/volumes/$pvc_name" 2>/dev/null || true
if [[ -f "$SRC/$pvc_name/volume.meta" ]]; then
sudo cp "$SRC/$pvc_name/volume.meta" "$DEST/metadata/${pvc_name}.meta" 2>/dev/null || true
fi
echo ""
done
echo ""
echo "Generating manifest..."
cat > "$MANIFEST" << EOF
{
"backup_timestamp": "$TIMESTAMP",
"source_path": "$SRC",
"destination_path": "$DEST",
"total_volumes": $TOTAL_VOLUMES,
"total_size_bytes": $TOTAL_SIZE,
"volumes": [
EOF
FIRST=true
for volume in "${VOLUMES[@]}"; do
pvc_name="${volume%%:*}"
friendly_name="${volume#*:}"
vol_size=$(sudo du -sb "$SRC/$pvc_name" 2>/dev/null | awk '{print $1}' || echo "0")
vol_size_hr=$(numfmt --to=iec-i --suffix=B "$vol_size" 2>/dev/null || echo "${vol_size}B")
head_file=$(sudo find "$DEST/volumes/$pvc_name" -name "volume-head-*.img" 2>/dev/null | head -1)
head_file=$(basename "$head_file" 2>/dev/null || echo "")
if [[ "$FIRST" == "true" ]]; then
FIRST=false
else
echo "," >> "$MANIFEST"
fi
cat >> "$MANIFEST" << EOF
{
"pvc_id": "$pvc_name",
"friendly_name": "$friendly_name",
"size_bytes": $vol_size,
"size_human": "$vol_size_hr",
"volume_head": "$head_file",
"backup_path": "volumes/$pvc_name"
}
EOF
done
cat >> "$MANIFEST" << EOF
]
}
EOF
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
echo ""
echo "========================================"
echo " Backup Complete!"
echo "========================================"
echo ""
echo "Duration: $((DURATION / 60))m $((DURATION % 60))s"
echo "Location: $DEST"
echo "Manifest: $MANIFEST"
echo ""
echo "Backup size:"
sudo du -sh "$DEST/volumes"
echo ""
echo "To mount a volume, run:"
echo " ./scripts/mount-longhorn-volume.sh <pvc-name-or-friendly-name>"
echo ""
echo "To restore a volume, run:"
echo " ./scripts/restore-longhorn-volume.sh <pvc-name-or-friendly-name>"
-16
View File
@@ -1,16 +0,0 @@
for dir in /mnt/replicas/pvc-*/; do
name=$(basename "$dir")
head=$(sudo find "$dir" -name "volume-head-*.img" | head -1)
sudo mkdir -p /tmp/inspect
loop=$(sudo losetup -fP --show "$head")
echo "=== $name ==="
sudo mount "$loop" /tmp/inspect 2>/dev/null && {
sudo ls -la /tmp/inspect | head -10
sudo umount /tmp/inspect
} || echo "(mount failed)"
sudo losetup -d "$loop"
echo ""
done
-37
View File
@@ -1,37 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="${BACKUP_DIR:-/mnt2/homey-backup}"
MANIFEST="$BACKUP_DIR/manifest.json"
echo "========================================"
echo " Longhorn Volume Backup List"
echo "========================================"
echo ""
if [[ ! -f "$MANIFEST" ]]; then
echo "No manifest found at $MANIFEST"
echo "Run backup-longhorn-to-disk.sh first."
exit 1
fi
echo "Backup timestamp: $(grep -oP '"backup_timestamp":\s*"\K[^"]+' "$MANIFEST")"
echo "Source: $(grep -oP '"source_path":\s*"\K[^"]+' "$MANIFEST")"
echo "Total volumes: $(grep -oP '"total_volumes":\s*\K[0-9]+' "$MANIFEST")"
echo "Total size: $(grep -oP '"total_size_bytes":\s*\K[0-9]+' "$MANIFEST" | numfmt --to=iec-i --suffix=B)"
echo ""
echo "Volumes:"
echo "----------------------------------------"
grep -A5 '"volumes"' "$MANIFEST" | grep -E '"friendly_name"|"size_human"' | \
while read -r name_line; read -r size_line; do
name=$(echo "$name_line" | grep -oP '"friendly_name":\s*"\K[^"]+')
size=$(echo "$size_line" | grep -oP '"size_human":\s*"\K[^"]+')
pvc=$(grep -B1 "$name_line" "$MANIFEST" | grep -oP '"pvc_id":\s*"\K[^"]+' || echo "")
printf " %-30s %10s %s\n" "$name" "$size" "$pvc"
done
echo ""
echo "Commands:"
echo " Mount: ./scripts/mount-longhorn-volume.sh <name>"
echo " Restore: ./scripts/restore-longhorn-volume.sh <name>"
-70
View File
@@ -1,70 +0,0 @@
#!/usr/bin/env python3
import json
import os
import sys
from fuse import FUSE, FuseOSError, Operations
class LonghornBackupFS(Operations):
def __init__(self, backup_dir):
self.backup_dir = backup_dir
self.blocks_dir = f"{backup_dir}/blocks"
backup_cfg = f"{backup_dir}/backups/backup_backup-eac0221d1cab4a9c.cfg"
with open(backup_cfg) as f:
data = json.load(f)
self.size = int(data['Size'])
self.block_map = {b['Offset']: b['BlockChecksum'] for b in data['Blocks']}
self.block_size = 2097152 # 2MB
print(f"Volume size: {self.size}")
print(f"Blocks: {len(self.block_map)}")
def getattr(self, path, fh=None):
return {'st_size': self.size, 'st_mode': 0o100644, 'st_nlink': 1}
def read(self, path, size, offset, fh):
result = bytearray()
remaining = size
current_offset = offset
while remaining > 0:
block_start = (current_offset // self.block_size) * self.block_size
block_offset = current_offset - block_start
read_size = min(remaining, self.block_size - block_offset)
if block_start in self.block_map:
checksum = self.block_map[block_start]
block_path = f"{self.blocks_dir}/{checksum[:2]}/{checksum[2:4]}/{checksum}.blk"
if os.path.exists(block_path):
with open(block_path, 'rb') as f:
f.seek(block_offset)
result.extend(f.read(read_size))
else:
result.extend(b'\x00' * read_size)
else:
result.extend(b'\x00' * read_size)
current_offset += read_size
remaining -= read_size
return bytes(result)
if __name__ == '__main__':
if len(sys.argv) < 3:
print(f"Usage: {sys.argv[0]} <backup_dir> <mount_point>")
print(f"Example: {sys.argv[0]} /mnt2/backed-up-drive/backupstore/volumes/2c/df/pvc-5c1f48e3-346f-4c35-8e6a-8fc0c4c3a842 /tmp/longhorn-fuse")
sys.exit(1)
backup_dir = sys.argv[1]
mount_point = sys.argv[2]
os.makedirs(mount_point, exist_ok=True)
print(f"Mounting {backup_dir} at {mount_point}")
print("This creates a virtual block device file at the mount point")
print("Then run: sudo losetup -fP {mount_point}/volume.img && sudo mount /dev/loopX /mnt/point")
fs = LonghornBackupFS(backup_dir)
fuse = FUSE(fs, mount_point, nothreads=True, foreground=True, allow_other=True)
-49
View File
@@ -1,49 +0,0 @@
#!/usr/bin/env python3
import nbdkit
import json
import os
backup_dir = "/mnt2/backed-up-drive/backupstore/volumes/2c/df/pvc-5c1f48e3-346f-4c35-8e6a-8fc0c4c3a842"
blocks_dir = f"{backup_dir}/blocks"
backup_cfg = f"{backup_dir}/backups/backup_backup-eac0221d1cab4a9c.cfg"
with open(backup_cfg) as f:
data = json.load(f)
size = int(data['Size'])
block_map = {b['Offset']: b['BlockChecksum'] for b in data['Blocks']}
block_size = 2097152
def thread_model():
return nbdkit.THREAD_MODEL_SERIALIZE_ALL_REQUESTS
def get_size():
return size
def pread(h, count, offset, flags):
result = bytearray()
remaining = count
current_offset = offset
while remaining > 0:
block_start = (current_offset // block_size) * block_size
block_offset = current_offset - block_start
read_size = min(remaining, block_size - block_offset)
if block_start in block_map:
checksum = block_map[block_start]
block_path = f"{blocks_dir}/{checksum[:2]}/{checksum[2:4]}/{checksum}.blk"
if os.path.exists(block_path):
with open(block_path, 'rb') as f:
f.seek(block_offset)
result.extend(f.read(read_size))
else:
result.extend(b'\x00' * read_size)
else:
result.extend(b'\x00' * read_size)
current_offset += read_size
remaining -= read_size
return bytes(result)
-146
View File
@@ -1,146 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="${BACKUP_DIR:-/mnt2/homey-backup}"
MOUNT_BASE="${MOUNT_BASE:-/mnt/longhorn-volumes}"
usage() {
echo "Usage: $0 <pvc-name-or-friendly-name> [mount-point]"
echo ""
echo "Mounts a Longhorn volume backup for exploration."
echo ""
echo "Arguments:"
echo " pvc-name-or-friendly-name The PVC ID or friendly name (e.g., 'nextcloud-data-pvc')"
echo " mount-point Optional custom mount point (default: $MOUNT_BASE/<name>)"
echo ""
echo "Examples:"
echo " $0 nextcloud-data-pvc"
echo " $0 pvc-5c1f48e3-346f-4c35-8e6a-8fc0c4c3a842-96d72815"
echo " $0 nextcloud-data-pvc /mnt/my-mount"
echo ""
echo "To unmount, run:"
echo " sudo umount <mount-point>"
echo " sudo losetup -d /dev/loopX"
exit 1
}
if [[ $# -lt 1 ]]; then
usage
fi
SEARCH_NAME="$1"
CUSTOM_MOUNT="${2:-}"
MANIFEST="$BACKUP_DIR/manifest.json"
if [[ ! -f "$MANIFEST" ]]; then
echo "Error: Manifest not found at $MANIFEST"
echo "Make sure you've run the backup script first."
exit 1
fi
find_volume() {
local search="$1"
local found=""
while IFS= read -r line; do
pvc_id=$(echo "$line" | grep -oP '"pvc_id":\s*"\K[^"]+')
friendly=$(echo "$line" | grep -oP '"friendly_name":\s*"\K[^"]+')
if [[ "$pvc_id" == "$search" ]] || [[ "$friendly" == "$search" ]]; then
echo "$pvc_id:$friendly"
return 0
fi
done < <(grep -A6 '"volumes"' "$MANIFEST" | grep -E '"pvc_id"|"friendly_name"')
return 1
}
VOLUME_INFO=$(find_volume "$SEARCH_NAME")
if [[ -z "$VOLUME_INFO" ]]; then
echo "Error: Volume '$SEARCH_NAME' not found in manifest."
echo ""
echo "Available volumes:"
grep -oP '"friendly_name":\s*"\K[^"]+' "$MANIFEST" | while read -r name; do
echo " - $name"
done
exit 1
fi
PVC_ID="${VOLUME_INFO%%:*}"
FRIENDLY_NAME="${VOLUME_INFO#*:}"
VOLUME_DIR="$BACKUP_DIR/volumes/$PVC_ID"
if [[ ! -d "$VOLUME_DIR" ]]; then
echo "Error: Volume directory not found: $VOLUME_DIR"
exit 1
fi
VOLUME_HEAD=$(find "$VOLUME_DIR" -name "volume-head-*.img" | head -1)
if [[ -z "$VOLUME_HEAD" ]]; then
echo "Error: No volume-head-*.img file found in $VOLUME_DIR"
echo "Contents:"
ls -la "$VOLUME_DIR"
exit 1
fi
if [[ -n "$CUSTOM_MOUNT" ]]; then
MOUNT_POINT="$CUSTOM_MOUNT"
else
MOUNT_POINT="$MOUNT_BASE/$FRIENDLY_NAME"
fi
echo "========================================"
echo " Mount Longhorn Volume"
echo "========================================"
echo ""
echo "PVC ID: $PVC_ID"
echo "Name: $FRIENDLY_NAME"
echo "Volume file: $(basename "$VOLUME_HEAD")"
echo "Mount point: $MOUNT_POINT"
echo ""
LOOP_DEV=$(sudo losetup -fP --show "$VOLUME_HEAD")
echo "Attached to: $LOOP_DEV"
sudo mkdir -p "$MOUNT_POINT"
echo ""
echo "Mounting..."
if sudo mount "$LOOP_DEV" "$MOUNT_POINT" 2>/dev/null; then
echo ""
echo "========================================"
echo " Mounted Successfully!"
echo "========================================"
echo ""
echo "Mount point: $MOUNT_POINT"
echo "Loop device: $LOOP_DEV"
echo ""
echo "Contents:"
ls -la "$MOUNT_POINT" 2>/dev/null | head -20
echo ""
echo "To unmount:"
echo " sudo umount $MOUNT_POINT"
echo " sudo losetup -d $LOOP_DEV"
else
echo "Mount failed. Trying with filesystem detection..."
FS_TYPE=$(sudo blkid -o value -s TYPE "$LOOP_DEV" 2>/dev/null || echo "")
if [[ -n "$FS_TYPE" ]]; then
echo "Detected filesystem: $FS_TYPE"
sudo mount -t "$FS_TYPE" "$LOOP_DEV" "$MOUNT_POINT"
echo ""
echo "Mounted successfully at $MOUNT_POINT"
else
echo "Could not detect filesystem. Volume may be empty or corrupted."
echo ""
echo "Loop device: $LOOP_DEV"
echo "Run 'sudo blkid $LOOP_DEV' to inspect."
echo ""
echo "To detach:"
echo " sudo losetup -d $LOOP_DEV"
fi
fi
-48
View File
@@ -1,48 +0,0 @@
#!/usr/bin/env python3
import json
import os
import sys
import gzip
from concurrent.futures import ThreadPoolExecutor, as_completed
backup_dir = "/mnt2/backed-up-drive/backupstore/volumes/2c/df/pvc-5c1f48e3-346f-4c35-8e6a-8fc0c4c3a842"
output_img = "/mnt/nextcloud-restored.img"
backup_cfg = f"{backup_dir}/backups/backup_backup-eac0221d1cab4a9c.cfg"
blocks_dir = f"{backup_dir}/blocks"
with open(backup_cfg) as f:
data = json.load(f)
blocks = data['Blocks']
total = len(blocks)
size = int(data['Size'])
print(f"Volume size: {size // 1024 // 1024 // 1024} GB")
print(f"Block count: {total}")
os.makedirs(os.path.dirname(output_img) if os.path.dirname(output_img) else '.', exist_ok=True)
if not os.path.exists(output_img):
import subprocess
subprocess.run(['truncate', '-s', str(size), output_img], check=True)
with open(output_img, 'r+b') as img:
for i, block in enumerate(blocks):
offset = block['Offset']
checksum = block['BlockChecksum']
block_path = f"{blocks_dir}/{checksum[:2]}/{checksum[2:4]}/{checksum}.blk"
if os.path.exists(block_path):
with gzip.open(block_path, 'rb') as bf:
img.seek(offset)
img.write(bf.read())
if (i + 1) % 500 == 0:
percent = (i + 1) * 100 // total
bar = '=' * (percent // 2) + ' ' * (50 - percent // 2)
sys.stdout.write(f"\r[{bar}] {percent}% ({i + 1}/{total})")
sys.stdout.flush()
print(f"\nDone! Image: {output_img}")
-94
View File
@@ -1,94 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="${1:-/mnt2/backed-up-drive/backupstore/volumes/2c/df/pvc-5c1f48e3-346f-4c35-8e6a-8fc0c4c3a842}"
OUTPUT_IMG="${2:-./nextcloud-data-restored.img}"
BACKUP_CFG="$BACKUP_DIR/backups/backup_backup-eac0221d1cab4a9c.cfg"
BLOCKS_DIR="$BACKUP_DIR/blocks"
if [[ ! -f "$BACKUP_CFG" ]]; then
echo "Error: Backup config not found at $BACKUP_CFG"
exit 1
fi
echo "========================================"
echo " Longhorn Backup Restore Tool"
echo "========================================"
echo ""
echo "Backup: $BACKUP_DIR"
echo "Output: $OUTPUT_IMG"
echo ""
SIZE=$(python3 -c "import json; print(json.load(open('$BACKUP_CFG'))['Size'])")
BLOCK_COUNT=$(python3 -c "import json; print(len(json.load(open('$BACKUP_CFG'))['Blocks']))")
echo "Volume size: $((SIZE / 1024 / 1024 / 1024)) GB ($SIZE bytes)"
echo "Block count: $BLOCK_COUNT"
echo ""
read -p "Continue? [y/N] " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
echo ""
echo "Creating sparse image..."
truncate -s "$SIZE" "$OUTPUT_IMG"
echo "Restoring blocks..."
python3 << 'PYEOF'
import json
import os
import sys
backup_cfg = os.environ['BACKUP_CFG']
blocks_dir = os.environ['BLOCKS_DIR']
output_img = os.environ['OUTPUT_IMG']
with open(backup_cfg) as f:
data = json.load(f)
blocks = data['Blocks']
total = len(blocks)
with open(output_img, 'r+b') as img:
for i, block in enumerate(blocks):
offset = block['Offset']
checksum = block['BlockChecksum']
block_path = f"{blocks_dir}/{checksum[:2]}/{checksum[2:4]}/{checksum}.blk"
if not os.path.exists(block_path):
print(f"Warning: Block not found: {checksum}")
continue
with open(block_path, 'rb') as bf:
img.seek(offset)
img.write(bf.read())
if (i + 1) % 1000 == 0 or i + 1 == total:
percent = (i + 1) * 100 // total
bar = '=' * (percent // 2) + ' ' * (50 - percent // 2)
sys.stdout.write(f"\r[{bar}] {percent}% ({i + 1}/{total})")
sys.stdout.flush()
print()
PYEOF
echo ""
echo "========================================"
echo " Restore Complete!"
echo "========================================"
echo ""
echo "Image: $OUTPUT_IMG"
echo "Size: $(du -sh "$OUTPUT_IMG" | cut -f1)"
echo ""
echo "To mount:"
echo " sudo losetup -fP $OUTPUT_IMG"
echo " sudo mount /dev/loopX /mnt/point"
echo ""
echo "Or directly:"
echo " sudo mount -o loop $OUTPUT_IMG /mnt/point"
-135
View File
@@ -1,135 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="${BACKUP_DIR:-/mnt2/homey-backup}"
RESTORE_BASE="${RESTORE_BASE:-/mnt/replicas}"
usage() {
echo "Usage: $0 <pvc-name-or-friendly-name> [--dry-run]"
echo ""
echo "Restores a Longhorn volume backup to the replicas directory."
echo ""
echo "Arguments:"
echo " pvc-name-or-friendly-name The PVC ID or friendly name"
echo " --dry-run Show what would be done without copying"
echo ""
echo "Examples:"
echo " $0 nextcloud-data-pvc"
echo " $0 nextcloud-data-pvc --dry-run"
echo ""
echo "WARNING: This will overwrite existing data in $RESTORE_BASE"
exit 1
}
if [[ $# -lt 1 ]]; then
usage
fi
SEARCH_NAME="$1"
DRY_RUN=false
if [[ "${2:-}" == "--dry-run" ]]; then
DRY_RUN=true
fi
MANIFEST="$BACKUP_DIR/manifest.json"
if [[ ! -f "$MANIFEST" ]]; then
echo "Error: Manifest not found at $MANIFEST_DIR"
exit 1
fi
find_volume() {
local search="$1"
while IFS= read -r line; do
pvc_id=$(echo "$line" | grep -oP '"pvc_id":\s*"\K[^"]+')
friendly=$(echo "$line" | grep -oP '"friendly_name":\s*"\K[^"]+')
if [[ "$pvc_id" == "$search" ]] || [[ "$friendly" == "$search" ]]; then
echo "$pvc_id:$friendly"
return 0
fi
done < <(grep -A6 '"volumes"' "$MANIFEST" | grep -E '"pvc_id"|"friendly_name"')
return 1
}
VOLUME_INFO=$(find_volume "$SEARCH_NAME")
if [[ -z "$VOLUME_INFO" ]]; then
echo "Error: Volume '$SEARCH_NAME' not found in manifest."
echo ""
echo "Available volumes:"
grep -oP '"friendly_name":\s*"\K[^"]+' "$MANIFEST" | while read -r name; do
echo " - $name"
done
exit 1
fi
PVC_ID="${VOLUME_INFO%%:*}"
FRIENDLY_NAME="${VOLUME_INFO#*:}"
BACKUP_VOLUME_DIR="$BACKUP_DIR/volumes/$PVC_ID"
RESTORE_VOLUME_DIR="$RESTORE_BASE/$PVC_ID"
echo "========================================"
echo " Restore Longhorn Volume"
echo "========================================"
echo ""
echo "PVC ID: $PVC_ID"
echo "Name: $FRIENDLY_NAME"
echo "Source: $BACKUP_VOLUME_DIR"
echo "Destination: $RESTORE_VOLUME_DIR"
echo "Dry run: $DRY_RUN"
echo ""
if [[ "$DRY_RUN" == "true" ]]; then
echo "[DRY RUN] Would copy:"
du -sh "$BACKUP_VOLUME_DIR" 2>/dev/null || echo " (size unknown)"
echo ""
echo "Files to copy:"
find "$BACKUP_VOLUME_DIR" -type f | head -20
exit 0
fi
if [[ -d "$RESTORE_VOLUME_DIR" ]]; then
echo "WARNING: Destination already exists!"
echo ""
read -p "Overwrite existing data? [y/N] " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
echo ""
echo "Removing existing data..."
sudo rm -rf "$RESTORE_VOLUME_DIR"
fi
echo "Creating destination directory..."
sudo mkdir -p "$RESTORE_VOLUME_DIR"
echo "Copying volume data..."
sudo rsync -a --no-owner --no-group --info=progress2 \
"$BACKUP_VOLUME_DIR/" \
"$RESTORE_VOLUME_DIR/"
echo ""
echo "Setting permissions..."
sudo chmod 700 "$RESTORE_VOLUME_DIR"
echo ""
echo "========================================"
echo " Restore Complete!"
echo "========================================"
echo ""
echo "Restored to: $RESTORE_VOLUME_DIR"
echo ""
echo "Size:"
sudo du -sh "$RESTORE_VOLUME_DIR"
echo ""
echo "Next steps:"
echo "1. Ensure Longhorn is configured to use $RESTORE_BASE"
echo "2. Restart Longhorn or the affected pod"
echo "3. Verify data integrity"
+10
View File
@@ -0,0 +1,10 @@
# Never commit an unencrypted secrets file.
# The encrypted version (produced by `sops -e -i secrets.yaml`) IS committed.
#
# If you accidentally add the plaintext version, sops-encrypted files
# contain a `sops:` key at the top — check before committing.
#
# Paranoia: ignore any plaintext variants you might create while editing.
secrets.yaml.plaintext
secrets.yaml.bak
*.plain
+24
View File
@@ -0,0 +1,24 @@
# sops configuration — controls which keys can decrypt secrets.yaml.
#
# SETUP STEPS (do this once on the Pi):
#
# 1. Install age: nix-shell -p age
# 2. Generate a key: age-keygen -o /var/lib/sops-nix/key.txt
# 3. Print the pubkey: age-keygen -y /var/lib/sops-nix/key.txt
# 4. Replace AGE-PUBLIC-KEY-PI-MAIN below with the output of step 3.
# 5. (Optional) add your own age key or GPG key as a second recipient so
# you can edit secrets from your workstation without the Pi being on.
#
# To encrypt / edit secrets.yaml:
# sops secrets/secrets.yaml
#
# sops will re-encrypt the file for all keys listed here every time you save.
creation_rules:
- path_regex: secrets/secrets\.yaml$
key_groups:
- age:
# Pi main host key — replace with output of `age-keygen -y /var/lib/sops-nix/key.txt`
- AGE-PUBLIC-KEY-PI-MAIN-REPLACE-ME
# (Optional) your workstation key for offline editing:
# - AGE-PUBLIC-KEY-YOUR-WORKSTATION
+54
View File
@@ -0,0 +1,54 @@
# =============================================================================
# Homey secrets — managed by sops-nix
#
# THIS FILE MUST BE ENCRYPTED WITH SOPS BEFORE COMMITTING.
# It is shown here as a plaintext template so you know what to fill in.
#
# Workflow:
# 1. Complete the .sops.yaml age key setup.
# 2. Fill in the values below.
# 3. Run: sops -e -i secrets/secrets.yaml
# This encrypts the file in-place. The encrypted version is safe to commit.
# 4. To edit later: sops secrets/secrets.yaml
#
# Ports from old deployment:
# - openldap/admin_password ← from k8s secret openldap-admin
# - openldap/config_password ← from k8s secret openldap-config
# - openldap/ro_password ← from k8s secret openldap-ro
# - gitea/admin_password ← from k8s secret gitea-admin-pass
# - nextcloud/admin_password ← from k8s secret nextcloud-admin-pass
# - nextcloud/postgres_password← from k8s secret nextcloud-postgres-pass
# The remaining secrets (authelia JWT, session key, encryption key, gitea
# LFS/OAuth2/internal tokens) are regenerated fresh — see notes below.
# =============================================================================
# --- OpenLDAP ---
openldap/admin_password: "REPLACE-WITH-OLD-VALUE"
openldap/config_password: "REPLACE-WITH-OLD-VALUE"
openldap/ro_password: "REPLACE-WITH-OLD-VALUE"
# --- Authelia (regenerated fresh — these are random strings) ---
authelia/jwt_secret: "GENERATE-random-64-chars"
authelia/session_secret: "GENERATE-random-64-chars"
authelia/storage_encryption_key: "GENERATE-random-64-chars"
# --- Gitea ---
gitea/admin_password: "REPLACE-WITH-OLD-VALUE"
# These three are regenerated — gitea will re-derive on first start:
gitea/lfs_jwt_secret: "GENERATE-random-43-chars-base64url"
gitea/oauth2_jwt_secret: "GENERATE-random-43-chars-base64url"
gitea/internal_token: "GENERATE-random-100-alphanum"
# --- Nextcloud ---
nextcloud/admin_password: "REPLACE-WITH-OLD-VALUE"
nextcloud/postgres_password: "REPLACE-WITH-OLD-VALUE"
# --- Cloudflare (DNS-01 ACME + tunnel) ---
cloudflare/api_token: "REPLACE-WITH-CF-DNS-EDIT-TOKEN"
cloudflare/tunnel_token: "REPLACE-WITH-CF-TUNNEL-TOKEN"
# --- Restic backup ---
restic/password: "GENERATE-random-passphrase"
# Repository destination — e.g. "sftp:user@nas:/backups/homey"
# or "b2:bucketname:homey" for Backblaze B2
# Set the actual repo URL in modules/backup.nix or override per-host.
-27
View File
@@ -1,27 +0,0 @@
---
{{- define "homey.lookuporgensecret" -}}
{{- $secretObj := (lookup "v1" "Secret" .Release.Namespace .secretname ) | default dict -}}
{{- $secretData := (get $secretObj "data") | default dict -}}
{{- $ret := (get $secretData "password" | b64dec ) | default (randAlphaNum 32 ) -}}
{{ $ret -}}
{{- end -}}
---
{{- define "homey.randomsecret"}}
apiVersion: v1
kind: Secret
metadata:
name: {{ (replace "\"" "" .secretname ) }}
type: Opaque
data:
password: {{ .secretval | b64enc | quote }}
{{- end }}
---
{{- define "homey.randHex"}}
{{- $result := "" }}
{{- range $i := until . }}
{{- $rand_hex_char := mod (randNumeric 4 | atoi) 16 | printf "%x" }}
{{- $result = print $result $rand_hex_char }}
{{- end }}
{{- $result }}
{{- end -}}
---
-668
View File
@@ -1,668 +0,0 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ldap-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Mi
storageClassName: longhorn
---
{{- $_ := set $ "homey_openldap_admin" (include "homey.lookuporgensecret" (merge (dict "secretname" "openldap-admin") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "openldap-admin" "secretval" .homey_openldap_admin) $) }}
# ---
{{- $_ := set $ "homey_openldap_config" (include "homey.lookuporgensecret" (merge (dict "secretname" "openldap-config") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "openldap-config" "secretval" .homey_openldap_config) $) }}
# ---
{{- $_ := set $ "homey_openldap_ro" (include "homey.lookuporgensecret" (merge (dict "secretname" "openldap-ro") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "openldap-ro" "secretval" .homey_openldap_ro) $) }}
---
{{- $_ := set $ "homey_authelia_jwt" (include "homey.lookuporgensecret" (merge (dict "secretname" "authelia-jwt") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "authelia-jwt" "secretval" .homey_authelia_jwt) $) }}
---
{{- $_ := set $ "homey_authelia_session" (include "homey.lookuporgensecret" (merge (dict "secretname" "authelia-session") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "authelia-session" "secretval" .homey_authelia_session) $) }}
---
{{- $_ := set $ "homey_authelia_encryption_key" (include "homey.lookuporgensecret" (merge (dict "secretname" "authelia-encryption-key") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "authelia-encryption-key" "secretval" .homey_authelia_encryption_key) $) }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: openldap
labels:
app.kubernetes.io/name: openldap
spec:
selector:
matchLabels:
app.kubernetes.io/name: openldap
replicas: 1
template:
metadata:
labels:
app.kubernetes.io/name: openldap
spec:
# securityContext:
# fsGroup: 0
containers:
- name: openldap
image: osixia/openldap
env:
- name: LDAP_ORGANISATION
value: {{ .Values.homey.organization }}
- name: LDAP_DOMAIN
value: {{ .Values.homey.url | quote}}
- name: LDAP_ADMIN_USERNAME
value: "admin"
- name: LDAP_READONLY_USER
value: "true"
- name: LDAP_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
key: password
name: openldap-admin
- name: LDAP_CONFIG_PASSWORD
valueFrom:
secretKeyRef:
key: password
name: openldap-config
- name: LDAP_READONLY_USER_PASSWORD
valueFrom:
secretKeyRef:
key: password
name: openldap-ro
ports:
- name: tcp-ldap
containerPort: 389
- name: ssl-ldap
containerPort: 636
volumeMounts:
- mountPath: /etc/ldap/slapd.d
subPath: openldap/etc/ldap/slapd.d
name: openldap-volume
- mountPath: /var/lib/ldap
subPath: openldap/var/lib/ldap
name: openldap-volume
volumes:
- name: openldap-volume
persistentVolumeClaim:
claimName: ldap-pvc
---
apiVersion: v1
kind: Service
metadata:
name: openldap
labels:
app.kubernetes.io/name: openldap
spec:
type: ClusterIP
ports:
- name: tcp-ldap
port: 389
targetPort: tcp-ldap
- name: ssl-ldap
port: 636
targetPort: ssl-ldap
selector:
app.kubernetes.io/name: openldap
---
apiVersion: v1
kind: ConfigMap
metadata:
name: authelia-conf
data:
configuration.yml: |-
{{ tpl (.Files.Get "files/authelia-config.yaml" | indent 4) . }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: authelia-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Mi
storageClassName: longhorn
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: authelia
labels:
app.kubernetes.io/name: authelia
spec:
selector:
matchLabels:
app.kubernetes.io/name: authelia
replicas: 1
template:
metadata:
labels:
app.kubernetes.io/name: authelia
spec:
enableServiceLinks: false
containers:
- name: authelia
image: authelia/authelia
imagePullPolicy: "IfNotPresent"
env:
- name: TZ
value: "Jerusalem/Israel"
ports:
- name: tcp
containerPort: 9091
volumeMounts:
- mountPath: /config/configuration.yml
name: authelia-conf
subPath: configuration.yml
readOnly: true
- mountPath: /config
subPath: authelia/config
name: authelia-volume
volumes:
- name: authelia-conf
configMap:
name: authelia-conf
items:
- key: configuration.yml
path: configuration.yml
- name: authelia-volume
persistentVolumeClaim:
claimName: authelia-pvc
---
apiVersion: v1
kind: Service
metadata:
name: authelia
labels:
app.kubernetes.io/name: authelia
spec:
type: ClusterIP
ports:
- name: tcp
port: 9091
targetPort: tcp
selector:
app.kubernetes.io/name: authelia
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: authelia
spec:
ingressClassName: {{ .Values.homey.ingress_class }}
tls:
- hosts:
- auth.{{ .Values.homey.url }}
secretName: {{ .Values.homey.certname }}
rules:
- host: auth.{{ .Values.homey.url }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: authelia
port:
number: 9091
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitea-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
storageClassName: longhorn
---
{{- $_ := set $ "homey_gitea_admin_pass" (include "homey.lookuporgensecret" (merge (dict "secretname" "gitea-admin-pass") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "gitea-admin-pass" "secretval" .homey_gitea_admin_pass) $) }}
---
{{- $_ := set $ "homey_gitea_lfs_jwt_secret" (include "homey.lookuporgensecret" (merge (dict "secretname" "gitea-lfs-jwt-secret") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "gitea-lfs-jwt-secret" "secretval" .homey_gitea_lfs_jwt_secret) $) }}
---
{{- $_ := set $ "homey_gitea_oauth2_jwt_secret" (include "homey.lookuporgensecret" (merge (dict "secretname" "gitea-oauth2-jwt-secret") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "gitea-oauth2-jwt-secret" "secretval" .homey_gitea_oauth2_jwt_secret) $) }}
---
apiVersion: v1
kind: Secret
metadata:
name: gitea-random-internal-token
annotations:
"helm.sh/resource-policy": "keep"
type: Opaque
data:
{{- $secretObj := (lookup "v1" "Secret" .Release.Namespace "gitea-random-internal-token") | default dict -}}
{{- $secretData := (get $secretObj "data") | default dict -}}
{{- $pass := (get $secretData "password") | default (randAlphaNum 100 | b64enc) -}}
{{- $_ := set $ "homey_gitea_random_internal_token" ($pass | b64dec) }}
password: {{ $pass | quote }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: gitea-conf
data:
app.ini: |-
{{ tpl (.Files.Get "files/gitea-app.ini" | indent 4) . }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitea
spec:
replicas: 1
selector:
matchLabels:
app: gitea
template:
metadata:
labels:
app: gitea
spec:
containers:
- name: gitea
image: gitea/gitea:latest
ports:
- containerPort: 3000
name: http
volumeMounts:
- name: gitea-persistent-storage
mountPath: /data
subPath: gitea/gitea/data
- name: gitea-conf
mountPath: /data/gitea/conf/app.ini
subPath: app.ini
readOnly: true
# startProbe:
# httpGet:
# path: /
# port: 3000
# initialDelaySeconds: 15
# lifecycle:
# postStart:
# exec:
# {{- $gitea_cmd := (printf "gitea admin auth add-ldap --name ldap --security-protocol unencrypted --host ldap --port 389 --user-search-base ou=users,%s --user-filter \\\"(&(objectClass=inetOrgPerson)(|(uid=%[1]s)(mail=kk[1]s)))\\\" --email-attribute mail --bind-dn=\\\"cn=readonly,%s\\\" --bind-password=\\\"%s\\\"" ( .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim) ( .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim) (.homey_openldap_ro | replace "\"" ""))}}
# command: ["/bin/sh", "-c", "{{$gitea_cmd}}"]
volumes:
- name: gitea-persistent-storage
persistentVolumeClaim:
claimName: gitea-pvc
- name: gitea-conf
configMap:
name: gitea-conf
items:
- key: app.ini
path: app.ini
---
apiVersion: v1
kind: Service
metadata:
name: gitea-svc
spec:
selector:
app: gitea
ports:
- name: http-port
protocol: TCP
port: 3000
targetPort: http
selector:
app: gitea
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gitea-ingress
spec:
ingressClassName: {{ .Values.homey.ingress_class }}
tls:
- hosts:
- git.{{ .Values.homey.url }}
secretName: {{ .Values.homey.certname }}
rules:
- host: git.{{ .Values.homey.url }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gitea-svc
port:
number: 3000
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nextcloud-postgres-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
storageClassName: longhorn
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nextcloud-data-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Ti
storageClassName: longhorn
---
apiVersion: v1
kind: Secret
metadata:
name: nextcloud-postgres-pass
annotations:
"helm.sh/resource-policy": "keep"
type: Opaque
data:
{{- $secretObj := (lookup "v1" "Secret" .Release.Namespace "nextcloud-postgres-pass") | default dict }}
{{- $secretData := (get $secretObj "data") | default dict }}
{{- $pass := (get $secretData "password") | default (randAlphaNum 32 | b64enc) }}
password: {{ $pass | quote }}
---
{{- $_ := set $ "homey_nextcloud_postgres_pass" (include "homey.lookuporgensecret" (merge (dict "secretname" "nextcloud-postgres-pass") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "nextcloud-postgres-pass" "secretval" .homey_nextcloud_postgres_pass) $) }}
---
{{- $_ := set $ "homey_nextcloud_admin_pass" (include "homey.lookuporgensecret" (merge (dict "secretname" "nextcloud-admin-pass") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "nextcloud-admin-pass" "secretval" .homey_nextcloud_admin_pass) $) }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: nextcloud-postgres-config
labels:
app: nextcloud-postgres
data:
POSTGRES_DB: nextcloud_db
POSTGRES_USER: postgres
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nextcloud-postgres
labels:
app: nextcloud-postgres
spec:
replicas: 1
selector:
matchLabels:
app: nextcloud-postgres
template:
metadata:
labels:
app: nextcloud-postgres
name: nextcloud-postgres
spec:
containers:
- name: nextcloud-postgres
image: postgres
imagePullPolicy: "IfNotPresent"
ports:
- containerPort: 5432
envFrom:
- configMapRef:
name: nextcloud-postgres-config
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: nextcloud-postgres-pass
key: password
volumeMounts:
- mountPath: /var/lib/postgresql/data
subPath: nextcloud/db
name: nextcloud-postgredb
volumes:
- name: nextcloud-postgredb
persistentVolumeClaim:
claimName: nextcloud-postgres-pvc
---
apiVersion: v1
kind: Service
metadata:
name: nextcloud-postgres
labels:
app: nextcloud-postgres
spec:
ports:
- port: 5432
selector:
app: nextcloud-postgres
---
apiVersion: v1
kind: ConfigMap
metadata:
name: nextcloud-configmap
labels:
app: nextcloud
data:
POSTGRES_HOST: nextcloud-postgres
OVERWRITEPROTOCOL: https
NEXTCLOUD_ADMIN_USER: admin
NEXTCLOUD_TRUSTED_DOMAINS: nextcloud.{{ .Values.homey.url }} nextcloud.admin.home
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nextcloud
labels:
app: nextcloud
spec:
replicas: 1
selector:
matchLabels:
app: nextcloud
template:
metadata:
labels:
app: nextcloud
name: nextcloud
spec:
containers:
- name: nextcloud
image: nextcloud
imagePullPolicy: Always
volumeMounts:
- name: nextcloud-volume
mountPath: "/var/www/html"
subPath: html
envFrom:
- configMapRef:
name: nextcloud-postgres-config
- configMapRef:
name: nextcloud-configmap
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: nextcloud-postgres-pass
key: password
- name: NEXTCLOUD_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: nextcloud-admin-pass
key: password
volumes:
- name: nextcloud-volume
persistentVolumeClaim:
claimName: nextcloud-data-pvc
---
apiVersion: v1
kind: Service
metadata:
name: nextcloud
spec:
selector:
app: nextcloud
ports:
- port: 80
targetPort: 80
name: nextcloud
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nextcloud-ingress
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: 5g
nginx.ingress.kubernetes.io/server-snippet: |
# Make a regex exception for `/.well-known` so that clients can still
# access it despite the existence of the regex rule
# `location ~ /(\.|autotest|...)` which would otherwise handle requests
# for `/.well-known`.
location = /.well-known/carddav { return 301 https://nextcloud.{{ .Values.homey.url }}/remote.php/dav/; }
location = /.well-known/caldav { return 301 https://nextcloud.{{ .Values.homey.url }}/remote.php/dav/; }
spec:
ingressClassName: {{ .Values.homey.ingress_class }}
tls:
- hosts:
- nextcloud.{{ .Values.homey.url }}
secretName: {{ .Values.homey.certname }}
rules:
- host: nextcloud.{{ .Values.homey.url }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nextcloud
port:
number: 80
---
#START RADICALE
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: radicale-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
storageClassName: longhorn
---
apiVersion: v1
kind: ConfigMap
metadata:
name: radicale-conf
labels:
app: radicale
data:
config: |-
{{ tpl (.Files.Get "files/radicale-configmap.ini" | indent 4) . }}
---
{{- $_ := set $ "homey_radicale_basic_auth" (include "homey.lookuporgensecret" (merge (dict "secretname" "radicale-basic-auth") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "radicale-basic-auth" "secretval" .homey_radicale_basic_auth) $) }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: radicale
labels:
app.kubernetes.io/name: radicale
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: radicale
template:
metadata:
labels:
app.kubernetes.io/name: radicale
spec:
containers:
- name: radicale
image: tomsquest/docker-radicale
imagePullPolicy: IfNotPresent
ports:
- name: dav
containerPort: 5232
protocol: TCP
volumeMounts:
- name: collections
mountPath: /data/collections
- name: config
mountPath: /config/config
subPath: config
readOnly: true
restartPolicy: Always
volumes:
- name: collections
persistentVolumeClaim:
claimName: radicale-pvc
- name: config
configMap:
name: radicale-conf
---
apiVersion: v1
kind: Service
metadata:
name: radicale
labels:
app.kubernetes.io/name: radicale
spec:
type: ClusterIP
ports:
- name: dav
port: 5232
targetPort: dav
selector:
app.kubernetes.io/name: radicale
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: radicale-dav
annotations:
kubernetes.io/ingress.allow-http: "false"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/auth-method: GET
nginx.ingress.kubernetes.io/auth-url: http://authelia.{{ .Release.Namespace }}.svc.cluster.local:9091/api/verify
nginx.ingress.kubernetes.io/auth-signin: https://auth.{{ .Values.homey.url }}?rm=$request_method
nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
nginx.ingress.kubernetes.io/auth-snippet: |
proxy_set_header X-Forwarded-Method $request_method;
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
auth_request_set $name $upstream_http_remote_name;
auth_request_set $email $upstream_http_remote_email;
proxy_set_header X-Remote-User $user;
proxy_set_header X-Remote-Fullname $name;
proxy_set_header X-Remote-Email $email;
spec:
ingressClassName: {{ .Values.homey.ingress_class }}
tls:
- hosts:
- dav.{{ .Values.homey.url }}
secretName: {{ .Values.homey.certname }}
rules:
- host: dav.{{ .Values.homey.url }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: radicale
port:
number: 5232
-211
View File
@@ -1,211 +0,0 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jellyfin-config-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
storageClassName: longhorn
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jellyfin-data-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 700Gi
storageClassName: longhorn
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: transmission-config-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
storageClassName: longhorn
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: jellyfin
spec:
replicas: 1
selector:
matchLabels:
app: jellyfin
template:
metadata:
labels:
app: jellyfin
spec:
containers:
- name: jellyfin
image: jellyfin/jellyfin:10.11.6
ports:
- containerPort: 8096
name: http
env:
- name: PGUID
value: "1000"
- name: PUID
value: "1000"
- name: JELLYFIN_PublishedServerUrl
value: jellyfin.{{ .Values.homey.url }}
imagePullPolicy: "Always"
volumeMounts:
- name: jellyfin-volume-config
mountPath: "/config"
subPath: jellyfin/config
- name: jellyfin-volume-data
mountPath: "/data/movies"
subPath: downloads/movies
- name: jellyfin-volume-data
mountPath: "/data/tvshows"
subPath: downloads/tvshows
volumes:
- name: jellyfin-volume-config
persistentVolumeClaim:
claimName: jellyfin-config-pvc
- name: jellyfin-volume-data
persistentVolumeClaim:
claimName: jellyfin-data-pvc
---
apiVersion: v1
kind: Service
metadata:
name: jellyfin-web
spec:
selector:
app: jellyfin
ports:
- port: 80
targetPort: 8096
name: jellyfin-web
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: jellyfin-ingress
spec:
ingressClassName: {{ .Values.homey.ingress_class }}
tls:
- hosts:
- jellyfin.{{ .Values.homey.url }}
secretName: {{ .Values.homey.certname }}
rules:
- host: jellyfin.{{ .Values.homey.url }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: jellyfin-web
port:
number: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: transmission
labels:
app: transmission
spec:
replicas: 1
selector:
matchLabels:
app: transmission
template:
metadata:
labels:
app: transmission
name: transmission
spec:
containers:
- name: transmission
image: linuxserver/transmission
imagePullPolicy: Always
volumeMounts:
- name: transmission-volume-config
mountPath: "/config"
subPath: transmission/config
- name: transmission-volume-data
mountPath: "/downloads/movies"
subPath: downloads/movies
- name: transmission-volume-data
mountPath: "/downloads/tvshows"
subPath: downloads/tvshows
- name: transmission-volume-data
mountPath: "/downloads/general"
subPath: downloads/general
- name: transmission-volume-data
mountPath: "/downloads/complete"
subPath: downloads/complete
volumes:
- name: transmission-volume-config
persistentVolumeClaim:
claimName: transmission-config-pvc
- name: transmission-volume-data
persistentVolumeClaim:
claimName: jellyfin-data-pvc
---
apiVersion: v1
kind: Service
metadata:
name: transmission-web
spec:
selector:
app: transmission
ports:
- port: 80
targetPort: 9091
name: transmission-web
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: torrent
annotations:
kubernetes.io/ingress.allow-http: "false"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/auth-method: GET
nginx.ingress.kubernetes.io/auth-url: http://authelia.{{ .Release.Namespace }}.svc.cluster.local:9091/api/verify
nginx.ingress.kubernetes.io/auth-signin: https://auth.{{ .Values.homey.url }}?rm=$request_method
nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
nginx.ingress.kubernetes.io/auth-snippet: |
proxy_set_header X-Forwarded-Method $request_method;
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
auth_request_set $name $upstream_http_remote_name;
auth_request_set $email $upstream_http_remote_email;
proxy_set_header X-Webauth-User $user;
proxy_set_header X-Webauth-Fullname $name;
proxy_set_header X-Webauth-Email $email;
spec:
ingressClassName: {{ .Values.homey.ingress_class }}
tls:
- hosts:
- torrent.{{ .Values.homey.url }}
secretName: {{ .Values.homey.certname }}
rules:
- host: torrent.{{ .Values.homey.url }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: transmission-web
port:
number: 80
-80
View File
@@ -1,80 +0,0 @@
---
#_PHPADMIN________
apiVersion: apps/v1
kind: Deployment
metadata:
name: phpldapadmin
labels:
app: phpldapadmin
spec:
replicas: 1
selector:
matchLabels:
app: phpldapadmin
template:
metadata:
labels:
app: phpldapadmin
spec:
containers:
- env:
- name: PHPLDAPADMIN_HTTPS
value: "false"
- name: PHPLDAPADMIN_LDAP_HOSTS
value: ldap://openldap:389
image: osixia/phpldapadmin
name: phpldapadmin
ports:
- containerPort: 80
name: http
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
name: phpldapadmin
spec:
ports:
- port: 80
targetPort: 80
name: http
selector:
app: phpldapadmin
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: phpldapadmin
annotations:
kubernetes.io/ingress.allow-http: "false"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/auth-method: GET
nginx.ingress.kubernetes.io/auth-url: http://authelia.{{ .Release.Namespace }}.svc.cluster.local:9091/api/verify
nginx.ingress.kubernetes.io/auth-signin: https://auth.{{ .Values.homey.url }}?rm=$request_method
nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
nginx.ingress.kubernetes.io/auth-snippet: |
proxy_set_header X-Forwarded-Method $request_method;
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
auth_request_set $name $upstream_http_remote_name;
auth_request_set $email $upstream_http_remote_email;
proxy_set_header X-Webauth-User $user;
proxy_set_header X-Webauth-Fullname $name;
proxy_set_header X-Webauth-Email $email;
spec:
ingressClassName: {{ .Values.homey.ingress_class }}
tls:
- hosts:
- ldapadmin.{{ .Values.homey.url }}
secretName: {{ .Values.homey.certname }}
rules:
- host: ldapadmin.{{ .Values.homey.url }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: phpldapadmin
port:
number: 80
-24
View File
@@ -1,24 +0,0 @@
---
{{- define "homey.auth.ingress.annotations" }}
# nginx.ingress.kubernetes.io/auth-signin: "https://auth.zakobar.com"
nginx.ingress.kubernetes.io/auth-url: "http://ldap-auth-internal.{{ .Release.Namespace }}.svc.cluster.local:80"
nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Email
nginx.ingress.kubernetes.io/location-snippets: |-
auth_request /auth
nginx.ingress.kubernetes.io/configuration-snippet: |-
location /auth {
# proxy_pass http://ldap-auth-internal;
proxy_pass_request_body off;
#THIS NEEDS TO BE SET BY ACTUAL SOMETHING LOGIN SHIT
# proxy_set_header X-Target http://ldap-auth-internal.{{ .Release.Namespace }}.svc.cluster.local:80;
proxy_set_header X-Ldap-URL "ldap://openldap";
proxy_set_header X-Ldap-BaseDN "ou=users,{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim }}";
proxy_set_header X-Ldap-BindDN "cn=readonly,{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim }}";
proxy_set_header X-Ldap-BindPass {{ (get (get (lookup "v1" "Secret" .Release.Namespace "openldap-ro") "data") "password") | b64dec | quote}};
proxy_set_header X-CookieName "homey.auth.cookie";
proxy_set_header Cookie $cookie_homey.auth.cookie;
proxy_set_header X-Remote-User $remote_user;
proxy_set_header X-Forwarded-Method $request_method;
proxy_set_header X-Ldap-Template "(uid=%(username)s)";
}
{{- end }}
-117
View File
@@ -1,117 +0,0 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: baikal-data-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
storageClassName: longhorn
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: baikal-config-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
storageClassName: longhorn
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: baikal
labels:
app: baikal
spec:
replicas: 1
selector:
matchLabels:
app: baikal
template:
metadata:
labels:
app: baikal
spec:
containers:
- name: baikal
image: ckulka/baikal
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: dav
volumeMounts:
- name: config
mountPath: /var/www/baikal/config
subPath: config
- name: data
mountPath: /var/www/baikal/Specific
subPath: Specific
restartPolicy: Always
volumes:
- name: data
persistentVolumeClaim:
claimName: baikal-data-pvc
- name: config
persistentVolumeClaim:
claimName: baikal-config-pvc
---
apiVersion: v1
kind: Service
metadata:
name: baikal
spec:
selector:
app: baikal
ports:
- name: dav
protocol: TCP
port: 80
targetPort: 80
selector:
app: baikal
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: baikal
annotations:
kubernetes.io/ingress.allow-http: "false"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/auth-method: GET
nginx.ingress.kubernetes.io/auth-url: http://authelia.{{ .Release.Namespace }}.svc.cluster.local:9091/api/verify
nginx.ingress.kubernetes.io/auth-signin: https://auth.{{ .Values.homey.url }}?rm=$request_method
nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
nginx.ingress.kubernetes.io/auth-snippet: |
proxy_set_header X-Forwarded-Method $request_method;
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
auth_request_set $name $upstream_http_remote_name;
auth_request_set $email $upstream_http_remote_email;
proxy_set_header X-Remote-User $user;
proxy_set_header X-Remote-Fullname $name;
proxy_set_header X-Remote-Email $email;
spec:
ingressClassName: {{ .Values.homey.ingress_class }}
tls:
- hosts:
- dav.{{ .Values.homey.url }}
secretName: {{ .Values.homey.certname }}
rules:
- host: dav.{{ .Values.homey.url }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: baikal
port:
number: 80
---
-71
View File
@@ -1,71 +0,0 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: baikal
labels:
app: baikal
spec:
replicas: 1
selector:
matchLabels:
app: baikal
template:
metadata:
labels:
app: baikal
spec:
containers:
- name: baikal
image: ckulka/baikal
ports:
- name: dav
containerPort: 80
protocol: TCP
volumeMounts:
- name: baikal-volume
mountPath: /var/www/baikal/Specific
subPath: baikal/data
- name: baikal-volume
mountPath: /var/www/baikal/config
subPath: baikal/config
restartPolicy: Always
volumes:
- name: baikal-volume
persistentVolumeClaim:
claimName: homey-pvc-longhorn
---
apiVersion: v1
kind: Service
metadata:
name: baikal
spec:
ports:
- name: dav
targetPort: 80
port: 80
selector:
app: baikal
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: baikal
spec:
ingressClassName: {{ .Values.homey.ingress_class }}
tls:
- hosts:
- dav.{{ .Values.homey.url }}
secretName: {{ .Values.homey.certname }}
rules:
- host: dav.{{ .Values.homey.url }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: baikal
port:
name: dav
---
-213
View File
@@ -1,213 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: davical-postgres-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
storageClassName: longhorn
---
{{- $_ := set $ "homey_davical_postgres_pass" (include "homey.lookuporgensecret" (merge (dict "secretname" "davical-postgres-pass") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "davical-postgres-pass" "secretval" .homey_davical_postgres_pass) $) }}
---
# apiVersion: extensions/v1beta1
apiVersion: v1
kind: ConfigMap
metadata:
name: davical-postgres-config
labels:
app: davical-postgres
data:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: davical-postgres
labels:
app: davical-postgres
spec:
replicas: 1
selector:
matchLabels:
app: davical-postgres
template:
metadata:
labels:
app: davical-postgres
name: davical-postgres
spec:
containers:
- name: davical-postgres
image: postgres
imagePullPolicy: "IfNotPresent"
ports:
- containerPort: 5432
envFrom:
- configMapRef:
name: davical-postgres-config
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: davical-postgres-pass
key: password
volumeMounts:
- mountPath: /var/lib/postgresql/data
subPath: data
name: davical-postgredb
volumes:
- name: davical-postgredb
persistentVolumeClaim:
claimName: davical-postgres-pvc
---
apiVersion: v1
kind: Service
metadata:
name: davical-postgres
labels:
app: davical-postgres
spec:
ports:
- port: 5432
selector:
app: davical-postgres
---
{{- $_ := set $ "homey_davical_admin_pass" (include "homey.lookuporgensecret" (merge (dict "secretname" "davical-admin-pass") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "davical-admin-pass" "secretval" .homey_davical_admin_pass) $) }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: davical-conf
data:
config.php: |-
{{ tpl (.Files.Get "files/davical-config.php" | indent 4) . }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: davical
labels:
app: davical
spec:
replicas: 1
selector:
matchLabels:
app: davical
template:
metadata:
labels:
app: davical
spec:
containers:
- name: davical
image: anerisgreat/davical-multiarch-docker:latest
imagePullPolicy: "Always"
ports:
- containerPort: 80
name: dav
env:
- name: PGHOST
value: "davical-postgres"
- name: PGUSER
value: "postgres"
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: davical-postgres-pass
key: password
- name: PGDATABASE
value: "davical"
- name: PGPORT
value: "5432"
- name: HOST_NAME
value:
"dav.{{ .Values.homey.url }}"
- name: DAVICAL_ADMIN_PASS
valueFrom:
secretKeyRef:
name: davical-admin-pass
key: password
- name: ROOT_PGUSER
value: "postgres"
- name: ROOT_PGPASSWORD
valueFrom:
secretKeyRef:
name: davical-postgres-pass
key: password
- name: RUN_MIGRATIONS_AT_STARTUP
value: "true"
volumeMounts:
- name: davical-conf
mountPath: /etc/davical/config.php
subPath: config.php
readOnly: true
volumes:
- name: davical-conf
configMap:
name: davical-conf
items:
- key: config.php
path: config.php
---
apiVersion: v1
kind: Service
metadata:
name: davical
spec:
selector:
app: davical
ports:
- name: dav
protocol: TCP
port: 80
targetPort: 80
selector:
app: davical
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: davical
annotations:
kubernetes.io/ingress.allow-http: "false"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/auth-method: GET
nginx.ingress.kubernetes.io/auth-url: http://authelia.{{ .Release.Namespace }}.svc.cluster.local:9091/api/verify
nginx.ingress.kubernetes.io/auth-signin: https://auth.{{ .Values.homey.url }}?rm=$request_method
nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
nginx.ingress.kubernetes.io/auth-snippet: |
proxy_set_header X-Forwarded-Method $request_method;
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
auth_request_set $name $upstream_http_remote_name;
auth_request_set $email $upstream_http_remote_email;
proxy_set_header Remote-User $user;
proxy_set_header Remote-Fullname $name;
proxy_set_header Remote-Email $email;
proxy_set_header Redirect-Remote-User $user;
proxy_set_header Redirect-Remote-Fullname $name;
proxy_set_header Redirect-Remote-Email $email;
spec:
ingressClassName: {{ .Values.homey.ingress_class }}
tls:
- hosts:
- dav.{{ .Values.homey.url }}
secretName: {{ .Values.homey.certname }}
rules:
- host: dav.{{ .Values.homey.url }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: davical
port:
number: 80
-131
View File
@@ -1,131 +0,0 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitea-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
storageClassName: longhorn
---
{{- $_ := set $ "homey_gitea_admin_pass" (include "homey.lookuporgensecret" (merge (dict "secretname" "gitea-admin-pass") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "gitea-admin-pass" "secretval" .homey_gitea_admin_pass) $) }}
---
{{- $_ := set $ "homey_gitea_lfs_jwt_secret" (include "homey.lookuporgensecret" (merge (dict "secretname" "gitea-lfs-jwt-secret") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "gitea-lfs-jwt-secret" "secretval" .homey_gitea_lfs_jwt_secret) $) }}
---
{{- $_ := set $ "homey_gitea_oauth2_jwt_secret" (include "homey.lookuporgensecret" (merge (dict "secretname" "gitea-oauth2-jwt-secret") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "gitea-oauth2-jwt-secret" "secretval" .homey_gitea_oauth2_jwt_secret) $) }}
---
apiVersion: v1
kind: Secret
metadata:
name: gitea-random-internal-token
annotations:
"helm.sh/resource-policy": "keep"
type: Opaque
data:
{{- $secretObj := (lookup "v1" "Secret" .Release.Namespace "gitea-random-internal-token") | default dict -}}
{{- $secretData := (get $secretObj "data") | default dict -}}
{{- $pass := (get $secretData "password") | default (randAlphaNum 100 | b64enc) -}}
{{- $_ := set $ "homey_gitea_random_internal_token" ($pass | b64dec) }}
password: {{ $pass | quote }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: gitea-conf
data:
app.ini: |-
{{ tpl (.Files.Get "files/gitea-app.ini" | indent 4) . }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitea
spec:
replicas: 1
selector:
matchLabels:
app: gitea
template:
metadata:
labels:
app: gitea
spec:
containers:
- name: gitea
image: gitea/gitea:latest
ports:
- containerPort: 3000
name: http
volumeMounts:
- name: gitea-persistent-storage
mountPath: /data
subPath: gitea/gitea/data
- name: gitea-conf
mountPath: /data/gitea/conf/app.ini
subPath: app.ini
readOnly: true
# startProbe:
# httpGet:
# path: /
# port: 3000
# initialDelaySeconds: 15
# lifecycle:
# postStart:
# exec:
# {{- set $gitea-cmd (printf "gitea admin auth add-ldap --name ldap --security-protocol unencrypted --host ldap --port 389 --user-search-base ou=users,%s --user-filter \\\"(&(objectClass=inetOrgPerson)(|(uid=\%[1]s)(mail=\%[1]s)))\\\" --email-attribute mail --bind-dn=\\\"cn=readonly,%s\\\" --bind-password=\\\"%s\\\"" ( .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim) () (.homey_openldap_ro | replace "\"" ""))}}
# command: ["/bin/sh", "-c", "{{cmd}}"]
volumes:
- name: gitea-persistent-storage
persistentVolumeClaim:
claimName: gitea-pvc
- name: gitea-conf
configMap:
name: gitea-conf
items:
- key: app.ini
path: app.ini
---
apiVersion: v1
kind: Service
metadata:
name: gitea-svc
spec:
selector:
app: gitea
ports:
- name: http-port
protocol: TCP
port: 3000
targetPort: http
selector:
app: gitea
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gitea-ingress
spec:
ingressClassName: {{ .Values.homey.ingress_class }}
tls:
- hosts:
- git.{{ .Values.homey.url }}
secretName: {{ .Values.homey.certname }}
rules:
- host: git.{{ .Values.homey.url }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gitea-svc
port:
number: 3000
---
-92
View File
@@ -1,92 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jellyfin-config-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Gi
storageClassName: longhorn
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jellyfin-data-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 700Gi
storageClassName: longhorn
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: jellyfin
spec:
replicas: 1
selector:
matchLabels:
app: jellyfin
template:
metadata:
labels:
app: jellyfin
spec:
containers:
- name: jellyfin
image: docker.io/jellyfin/jellyfin
volumeMounts:
- name: jellyfin-volume-config
mountPath: "/config"
subPath: jellyfin/config
- name: jellyfin-volume-data
mountPath: "/data/movies"
subPath: downloads/movies
- name: jellyfin-volume-data
mountPath: "/data/tvshows"
subPath: downloads/tvshows
- env:
- name: JELLYFIN_PublishedServerUrl
value: jellyfin.{{ .Values.homey.url }}
volumes:
- name: jellyfin-volume-config
persistentVolumeClaim:
claimName: jellyfin-config-pvc
- name: jellyfin-volume-data
persistentVolumeClaim:
claimName: jellyfin-data-pvc
---
apiVersion: v1
kind: Service
metadata:
name: jellyfin-web
namespace: homecenter
spec:
selector:
app: jellyfin
ports:
- port: 80
targetPort: 8096
name: jellyfin-web
---
ingressClassName: {{ .Values.homey.ingress_class }}
tls:
- hosts:
- jellyfin.{{ .Values.homey.url }}
secretName: {{ .Values.homey.certname }}
rules:
- host: jellyfin.{{ .Values.homey.url }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: jellyfin-web
port:
number: 80
---
-70
View File
@@ -1,70 +0,0 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ldap-auth
labels:
app: ldap-auth
spec:
replicas: 1
selector:
matchLabels:
app: ldap-auth
template:
metadata:
labels:
app: ldap-auth
name: ldap-auth
spec:
containers:
- name: ldap-auth
image: linuxserver/ldap-auth
imagePullPolicy: Always
---
#https://stackoverflow.com/questions/51149921/how-to-authenticate-nginx-with-ldap
apiVersion: v1
kind: Service
metadata:
name: ldap-auth
spec:
selector:
app: ldap-auth
ports:
- port: 80
targetPort: 9000
---
apiVersion: v1
kind: Service
metadata:
name: ldap-auth-internal
spec:
selector:
app: ldap-auth
ports:
- port: 80
targetPort: 8888
---
# apiVersion: networking.k8s.io/v1
# kind: Ingress
# metadata:
# name: ldap-auth-ingress
# annotations:
# spec:
# ingressClassName: {{ .Values.homey.ingress_class }}
# tls:
# - hosts:
# - auth.{{ .Values.homey.url }}
# secretName: {{ .Values.homey.certname }}
# rules:
# - host: auth.{{ .Values.homey.url }}
# http:
# paths:
# - path: /
# pathType: Prefix
# backend:
# service:
# name: ldap-auth
# port:
# number: 80
# ---
-206
View File
@@ -1,206 +0,0 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nextcloud-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 30Gi
storageClassName: longhorn
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nextcloud-postgres-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
storageClassName: longhorn
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nextcloud-data-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 30Gi
storageClassName: longhorn
---
apiVersion: v1
kind: Secret
metadata:
name: nextcloud-postgres-pass
annotations:
"helm.sh/resource-policy": "keep"
type: Opaque
data:
{{- $secretObj := (lookup "v1" "Secret" .Release.Namespace "nextcloud-postgres-pass") | default dict }}
{{- $secretData := (get $secretObj "data") | default dict }}
{{- $pass := (get $secretData "password") | default (randAlphaNum 32 | b64enc) }}
password: {{ $pass | quote }}
---
# apiVersion: extensions/v1beta1
apiVersion: v1
kind: ConfigMap
metadata:
name: nextcloud-postgres-config
labels:
app: nextcloud-postgres
data:
POSTGRES_DB: nextcloud_db
POSTGRES_USER: postgres
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nextcloud-postgres
labels:
app: nextcloud-postgres
spec:
replicas: 1
selector:
matchLabels:
app: nextcloud-postgres
template:
metadata:
labels:
app: nextcloud-postgres
name: nextcloud-postgres
spec:
containers:
- name: nextcloud-postgres
image: postgres
imagePullPolicy: "IfNotPresent"
ports:
- containerPort: 5432
envFrom:
- configMapRef:
name: nextcloud-postgres-config
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: nextcloud-postgres-pass
key: password
volumeMounts:
- mountPath: /var/lib/postgresql/data
subPath: nextcloud/db
name: nextcloud-postgredb
volumes:
- name: nextcloud-postgredb
persistentVolumeClaim:
claimName: nextcloud-postgres-pvc
---
apiVersion: v1
kind: Service
metadata:
name: nextcloud-postgres
labels:
app: nextcloud-postgres
spec:
ports:
- port: 5432
selector:
app: nextcloud-postgres
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nextcloud
labels:
app: nextcloud
spec:
replicas: 1
selector:
matchLabels:
app: nextcloud
template:
metadata:
labels:
app: nextcloud
name: nextcloud
spec:
containers:
- name: nextcloud
image: nextcloud
imagePullPolicy: Always
volumeMounts:
- name: nextcloud-volume
mountPath: "/var/www/html"
subPath: nextcloud/html
- name: nextcloud-media
mountPath: "/var/www/html/data"
subPath: nextcloud/html/data
envFrom:
- configMapRef:
name: nextcloud-postgres-config
env:
- name: POSTGRES_HOST
value: "nextcloud-postgres"
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: nextcloud-postgres-pass
key: password
- name: OVERWRITEPROTOCOL
value: "https"
volumes:
- name: nextcloud-volume
persistentVolumeClaim:
claimName: nextcloud-pvc
- name: nextcloud-media
persistentVolumeClaim:
claimName: nextcloud-data-pvc
---
apiVersion: v1
kind: Service
metadata:
name: nextcloud
spec:
selector:
app: nextcloud
ports:
- port: 80
targetPort: 80
name: nextcloud
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nextcloud-ingress
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: 5g
nginx.ingress.kubernetes.io/server-snippet: |
# Make a regex exception for `/.well-known` so that clients can still
# access it despite the existence of the regex rule
# `location ~ /(\.|autotest|...)` which would otherwise handle requests
# for `/.well-known`.
location = /.well-known/carddav { return 301 https://nextcloud.zakobar.com/remote.php/dav/; }
location = /.well-known/caldav { return 301 https://nextcloud.zakobar.com/remote.php/dav/; }
spec:
ingressClassName: {{ .Values.homey.ingress_class }}
tls:
- hosts:
- nextcloud.{{ .Values.homey.url }}
secretName: {{ .Values.homey.certname }}
rules:
- host: nextcloud.{{ .Values.homey.url }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nextcloud
port:
number: 80
---
-230
View File
@@ -1,230 +0,0 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: paperless-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 50Gi
storageClassName: longhorn
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: paperless-redis
spec:
replicas: 1
selector:
matchLabels:
app: paperless-redis
template:
metadata:
labels:
app: paperless-redis
spec:
containers:
- name: paperless
image: redis
imagePullPolicy: "IfNotPresent"
ports:
- containerPort: 6379
name: redis
volumeMounts:
- name: paperless-volume
mountPath: "/data"
subPath: paperless/redis-data
volumes:
- name: paperless-volume
persistentVolumeClaim:
claimName: paperless-pvc
---
apiVersion: v1
kind: Service
metadata:
name: paperless-redis
spec:
selector:
app: paperless-redis
ports:
- port: 80
targetPort: 8000
name: paperless-web
---
{{- $_ := set $ "homey_paperless_postgres_pass" (include "homey.lookuporgensecret" (merge (dict "secretname" "paperless-postgres-pass") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "paperless-postgres-pass" "secretval" .homey_paperless_postgres_pass) $) }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: paperless-postgres-config
labels:
app: paperless-postgres
data:
POSTGRES_DB: paperless
POSTGRES_USER: paperless
POSTGRES_PASSWORD: paperless
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: paperless-postgres
labels:
app: paperless-postgres
spec:
replicas: 1
selector:
matchLabels:
app: paperless-postgres
template:
metadata:
labels:
app: paperless-postgres
name: paperless-postgres
spec:
containers:
- name: paperless-postgres
image: postgres
imagePullPolicy: "IfNotPresent"
ports:
- containerPort: 5432
envFrom:
- configMapRef:
name: paperless-postgres-config
volumeMounts:
- mountPath: /var/lib/postgresql/data
subPath: paperless/db
name: paperless-volume
volumes:
- name: paperless-volume
persistentVolumeClaim:
claimName: paperless-pvc
---
apiVersion: v1
kind: Service
metadata:
name: paperless-postgres
labels:
app: paperless-postgres
spec:
ports:
- port: 5432
selector:
app: paperless-postgres
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: paperless
spec:
replicas: 1
selector:
matchLabels:
app: paperless
template:
metadata:
labels:
app: paperless
spec:
containers:
- name: paperless
image: ghcr.io/paperless-ngx/paperless-ngx:latest
imagePullPolicy: "IfNotPresent"
ports:
- containerPort: 8000
name: paperless-web
volumeMounts:
- name: paperless-volume
mountPath: "/usr/src/paperless/data"
subPath: paperless/data
- name: paperless-volume
mountPath: "/usr/src/paperless/media"
subPath: paperless/media
- name: paperless-volume
mountPath: "/usr/src/paperless/export"
subPath: paperless/export
- name: paperless-volume
mountPath: "/usr/src/paperless/consume"
subPath: paperless/consume
env:
- name: PAPERLESS_REDIS
value: redis://paperless-redis:6379
- name: PAPERLESS_DBHOST
value: paperless-postgres
- name: PAPERLESS_DEBUG
value: "true"
- name: PAPERLESS_ENABLE_HTTP_REMOTE_USER
value: "true"
- name: PAPERLESS_ENABLE_HTTP_REMOTE_USER_API
value: "true"
- name: PAPERLESS_DISABLE_REGULAR_LOGIN
value: "true"
- name: PAPERLESS_LOGOUT_REDIRECT_URL
value: "https://auth.{{ .Values.homey.url }}/logout"
- name: PAPERLESS_URL
value: "https://paperless.{{ .Values.homey.url }}"
- name: PAPERLESS_DBPASSWORD
valueFrom:
secretKeyRef:
name: paperless-postgres-pass
key: password
volumes:
- name: paperless-volume
persistentVolumeClaim:
claimName: paperless-pvc
---
apiVersion: v1
kind: Service
metadata:
name: paperless-web
labels:
app: paperless-web
spec:
selector:
app: paperless
ports:
- port: 80
targetPort: 8000
name: paperless-web
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: paperless-ingress
annotations:
kubernetes.io/ingress.allow-http: "false"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/auth-method: GET
nginx.ingress.kubernetes.io/auth-url: http://authelia.{{ .Release.Namespace }}.svc.cluster.local:9091/api/verify
nginx.ingress.kubernetes.io/auth-signin: https://auth.{{ .Values.homey.url }}?rm=$request_method
nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
nginx.ingress.kubernetes.io/auth-snippet: |
proxy_set_header X-Forwarded-Method $request_method;
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
auth_request_set $name $upstream_http_remote_name;
auth_request_set $email $upstream_http_remote_email;
proxy_set_header REMOTE_USER $remote_user;
proxy_set_header REMOTE_EMAIL $email;
proxy_set_header REMOTE_NAME $name;
spec:
ingressClassName: {{ .Values.homey.ingress_class }}
tls:
- hosts:
- paperless.{{ .Values.homey.url }}
secretName: {{ .Values.homey.certname }}
rules:
- host: paperless.{{ .Values.homey.url }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: paperless-web
port:
number: 80
---
-122
View File
@@ -1,122 +0,0 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: radicale-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
storageClassName: longhorn
---
apiVersion: v1
kind: ConfigMap
metadata:
name: radicale-conf
labels:
app: radicale
data:
config: |-
{{ tpl (.Files.Get "files/radicale-configmap.ini" | indent 4) . }}
---
{{- $_ := set $ "homey_radicale_basic_auth" (include "homey.lookuporgensecret" (merge (dict "secretname" "radicale-basic-auth") $))}}
{{ include "homey.randomsecret" (merge (dict "secretname" "radicale-basic-auth" "secretval" .homey_radicale_basic_auth) $) }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: radicale
labels:
app: radicale
spec:
replicas: 1
selector:
matchLabels:
app: radicale
template:
metadata:
labels:
app: radicale
spec:
containers:
- name: radicale
image: tomsquest/docker-radicale
imagePullPolicy: IfNotPresent
ports:
- name: dav
containerPort: 5232
protocol: TCP
volumeMounts:
- name: collections
mountPath: /data/collections
- name: config
mountPath: /config/config
subPath: config
readOnly: true
restartPolicy: Always
volumes:
- name: collections
persistentVolumeClaim:
claimName: radicale-pvc
- name: config
configMap:
name: radicale-conf
---
apiVersion: v1
kind: Service
metadata:
name: radicale
labels:
app.kubernetes.io/name: radicale
spec:
type: ClusterIP
ports:
- name: dav
port: 5232
targetPort: 5232
- name: http
port:80
targetPort: 80
selector:
app.kubernetes.io/name: radicale
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: radicale
annotations:
kubernetes.io/ingress.allow-http: "false"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/auth-method: GET
nginx.ingress.kubernetes.io/auth-url: http://authelia.{{ .Release.Namespace }}.svc.cluster.local:9091/api/verify
nginx.ingress.kubernetes.io/auth-signin: https://auth.{{ .Values.homey.url }}?rm=$request_method
nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
nginx.ingress.kubernetes.io/auth-snippet: |
proxy_set_header X-Forwarded-Method $request_method;
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
auth_request_set $name $upstream_http_remote_name;
auth_request_set $email $upstream_http_remote_email;
proxy_set_header X-Remote-User $user;
proxy_set_header X-Remote-Fullname $name;
proxy_set_header X-Remote-Email $email;
spec:
ingressClassName: {{ .Values.homey.ingress_class }}
tls:
- hosts:
- dav.{{ .Values.homey.url }}
secretName: {{ .Values.homey.certname }}
rules:
- host: dav.{{ .Values.homey.url }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: radicale
port:
number: 5232
---
-118
View File
@@ -1,118 +0,0 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: baikal-data-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
storageClassName: longhorn
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: baikal-config-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
storageClassName: longhorn
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: baikal
labels:
app: baikal
spec:
replicas: 1
selector:
matchLabels:
app: baikal
template:
metadata:
labels:
app: baikal
spec:
containers:
- name: baikal
image: ckulka/baikal-docker
imagePullPolicy: IfNotPresent
ports:
- name: dav
containerPort: 80
protocol: TCP
volumeMounts:
- name: config
mountPath: /var/www/baikal/config
subPath: config
- name: data
mountPath: /var/www/baikal/Specific
subPath: Specific
restartPolicy: Always
volumes:
- name: data
persistentVolumeClaim:
claimName: baikal-data-pvc
- name: config
persistentVolumeClaim:
claimName: baikal-config-pvc
---
apiVersion: v1
kind: Service
metadata:
name: baikal
labels:
app.kubernetes.io/name: baikal
spec:
type: ClusterIP
ports:
- name: dav
port: 80
targetPort: 80
selector:
app.kubernetes.io/name: baikal
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: baikal
annotations:
kubernetes.io/ingress.allow-http: "false"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/auth-method: GET
nginx.ingress.kubernetes.io/auth-url: http://authelia.{{ .Release.Namespace }}.svc.cluster.local:9091/api/verify
nginx.ingress.kubernetes.io/auth-signin: https://auth.{{ .Values.homey.url }}?rm=$request_method
nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email
nginx.ingress.kubernetes.io/auth-snippet: |
proxy_set_header X-Forwarded-Method $request_method;
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
auth_request_set $name $upstream_http_remote_name;
auth_request_set $email $upstream_http_remote_email;
proxy_set_header X-Remote-User $user;
proxy_set_header X-Remote-Fullname $name;
proxy_set_header X-Remote-Email $email;
spec:
ingressClassName: {{ .Values.homey.ingress_class }}
tls:
- hosts:
- dav.{{ .Values.homey.url }}
secretName: {{ .Values.homey.certname }}
rules:
- host: dav.{{ .Values.homey.url }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: baikal
port:
number: 80
---
-162
View File
@@ -1,162 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sogo-postgres-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
storageClassName: longhorn
---
apiVersion: v1
kind: Secret
metadata:
name: sogo-db-pass
type: Opaque
data:
password: sogo
---
apiVersion: v1
kind: ConfigMap
metadata:
name: sogo-postgres-config
labels:
app: sogo-postgres
data:
POSTGRES_DB: sogo
POSTGRES_USER: sogo
POSTGRES_PASSWORD: sogo
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sogo-postgres
labels:
app: sogo-postgres
spec:
replicas: 1
selector:
matchLabels:
app: sogo-postgres
template:
metadata:
labels:
app: sogo-postgres
name: sogo-postgres
spec:
containers:
- name: postgres
image: postgres
imagePullPolicy: "IfNotPresent"
ports:
- containerPort: 5432
envFrom:
- configMapRef:
name: sogo-postgres-config
volumeMounts:
- mountPath: /var/lib/postgresql/data
subPath: sogo/db/data
name: sogo-postgresdb
volumes:
- name: sogo-postgresdb
persistentVolumeClaim:
claimName: sogo-postgres-pvc
---
apiVersion: v1
kind: Service
metadata:
name: sogo-postgres
labels:
app: sogo-postgres
spec:
ports:
- port: 5432
selector:
app: sogo-postgres
---
apiVersion: v1
kind: ConfigMap
metadata:
name: sogo-conf
data:
sogo.conf: |-
{{ tpl (.Files.Get "files/sogo.conf" | indent 4) . }}
---
apiVersion: v1
kind: Service
metadata:
name: sogo
labels:
app: sogo
spec:
ports:
- port: 80
targetPort: 80
selector:
app: sogo
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sogo
spec:
# Stop old container before starting new one.
# No known upgrade policy know. Save to stop and start a new one.
strategy:
type: Recreate
rollingUpdate: null
selector:
matchLabels:
app: sogo
replicas: 1
template:
metadata:
labels:
app: sogo
spec:
containers:
- name: sogo
image: mailcow/sogo:nightly-1.119
resources:
requests:
cpu: 100m
memory: 400Mi
ports:
- containerPort: 80
volumeMounts:
- mountPath: /etc/sogo/sogo.conf
name: sogo-conf
subPath: sogo.conf
readOnly: true
volumes:
- name: sogo-conf
configMap:
name: sogo-conf
optional: false
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: sogo-ingress
spec:
ingressClassName: {{ .Values.homey.ingress_class }}
tls:
- hosts:
- git.{{ .Values.homey.url }}
secretName: {{ .Values.homey.certname }}
rules:
- host: sogo.{{ .Values.homey.url }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: soo
port:
number: 80
---
-65
View File
@@ -1,65 +0,0 @@
replicaCount: 1
homeyNamespace: homey
imagePullSecrets: []
nameOverride: "homey-app"
fullnameOverride: "homey-chart"
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: "homey"
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: ClusterIP
port: 80
resources: {} # We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
homey:
organization: "Zakobar Home Server"
url: zakobar.com
ip: 192.168.1.100
certname: zakobarcert
ingress_class: nginx