Merge nixos-port: complete NixOS port of homey selfhosted stack

Replaces Helm/k8s deployment with flake-based NixOS config.
All core services working: Caddy, Authelia, OpenLDAP, phpLDAPadmin, Gitea.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aner Zakobar
2026-04-23 14:46:29 +03:00
64 changed files with 3523 additions and 4782 deletions
+1
View File
@@ -1,3 +1,4 @@
charts
*.lock
.agent-shell
result
+23
View File
@@ -0,0 +1,23 @@
# 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:
- pgp:
- 076AA297579A0064
age:
- age120j8ty7nn04l3s3kgph5ty3v9g4e52fknn8xtnmzwakq9nv2la3skgte0p
+256 -224
View File
@@ -1,255 +1,287 @@
# AGENTS.md
This is a Helm chart for deploying a self-hosted home environment (Homey) on Kubernetes.
Self-hosted home server configuration for a Raspberry Pi 4 (8 GB), managed
entirely through NixOS. Services run as podman containers under systemd.
Remote access is via Cloudflare Tunnel; local access goes through Caddy
with Let's Encrypt TLS (DNS-01, Cloudflare API).
## Project Overview
The original Kubernetes/Helm setup is preserved on the `main` branch.
This branch (`nixos-port`) is the active NixOS port.
- **Type**: Helm Chart (Kubernetes package manager)
- **Language**: YAML + Go template syntax (Helm templating)
- **Key Files**:
- `Chart.yaml` - Chart metadata
- `values.yaml` - Default configuration values
- `templates/` - Kubernetes manifest templates (auth.yaml, media.yaml, phpldapadmin.yaml, _definitions.yaml)
- `files/` - Configuration file templates (processed by Helm with `tpl` function)
---
## Build/Lint/Test Commands
## Project Structure
### Helm Validation
```bash
# Lint the Helm chart for errors
helm lint .
# Template rendering (dry-run install)
helm template test-release . --debug
# Install/upgrade in cluster
helm upgrade --install homey . -n homey
# Verify chart against Kubernetes API
helm kubeval .
# Check schema validation of values.yaml
helm schema generate
```
flake.nix # Entry point — defines all hosts
modules/
common.nix # Shared system config (nix, podman, sops, SSH)
storage.nix # External HD mount + per-service directory layout
caddy.nix # Caddy reverse proxy (DNS-01 ACME, forward_auth)
cloudflared.nix # Cloudflare Tunnel for remote access
backup.nix # Restic daily backups (S3 primary + manual offload)
services/
openldap.nix # OpenLDAP — central identity provider
authelia.nix # Authelia — SSO gateway
gitea.nix # Gitea — Git server
nextcloud.nix # Nextcloud + PostgreSQL
phpldapadmin.nix # phpLDAPadmin — LDAP web UI
jellyfin.nix # Jellyfin — media server (disabled by default)
transmission.nix # Transmission — torrent client (disabled by default)
hosts/
pi-main/
default.nix # Service selection + host-specific overrides
hardware.nix # Pi 4 boot, SD card labels, ARM platform
secrets/
.sops.yaml # Age key configuration
secrets.yaml # sops-encrypted secrets (commit only after encrypting)
PORTING.md # Step-by-step migration guide from the old Helm setup
```
### Manual Template Testing
```bash
# Render templates locally with custom values
helm template homey . -f values.yaml --set homey.url=example.com
## Services and URLs
# Template with debug output
helm template homey . --debug 2>&1 | less
All services live under `zakobar.com`.
| Service | URL | Auth |
|---------|-----|------|
| Authelia | `auth.zakobar.com` | Public (it is the auth portal) |
| Gitea | `git.zakobar.com` | Gitea-native (LDAP) |
| Nextcloud | `nextcloud.zakobar.com` | Nextcloud-native |
| phpLDAPadmin | `ldapadmin.zakobar.com` | Authelia two_factor, admins only |
| Jellyfin | `jellyfin.zakobar.com` | Jellyfin-native |
| Transmission | `torrent.zakobar.com` | Authelia two_factor, admins only |
Internal ports (all bound to `127.0.0.1`):
| Container | Port |
|-----------|------|
| openldap | 389 |
| authelia | 9091 |
| gitea | 3000 |
| nextcloud | 8080 |
| nextcloud-postgres | 5432 |
| phpldapadmin | 8081 |
| jellyfin | 8096 |
| transmission | 9092 (not 9091 — avoids clash with authelia) |
## Storage Layout
All persistent data lives on the external HD at `/mnt/data/`:
```
/mnt/data/
openldap/
etc-ldap-slapd.d/ → /etc/ldap/slapd.d in container
var-lib-ldap/ → /var/lib/ldap in container
authelia/config/ → /config
gitea/data/ → /data
nextcloud/
html/ → /var/www/html
db/ → /var/lib/postgresql/data
db-dump/ → pg_dump output (pre-backup)
jellyfin/config/ → /config
media/movies|tvshows|... → shared media (read-only to jellyfin)
transmission/config/ → /config
restic-cache/ → restic local cache
```
### Kubectl Validation
```bash
# Dry-run apply to validate manifests
kubectl apply -f templates/auth.yaml --dry-run=server
The drive device path is set per-host in `hosts/<name>/default.nix` via
`homey.storage.device`. Use a `/dev/disk/by-id/` path for stability.
# Get rendered template directly
helm template homey . | kubectl apply --dry-run=server -f -
## Build / Validate Commands
```bash
# Check flake structure and evaluate all hosts (no build)
nix flake check
# Dry-run: show what would change without applying
sudo nixos-rebuild dry-activate --flake .#pi-main
# Apply configuration
sudo nixos-rebuild switch --flake .#pi-main
# Build without switching (e.g. cross-compile on workstation)
nix build .#nixosConfigurations.pi-main.config.system.build.toplevel
# Show diff between running system and new config
nvd diff /run/current-system $(nix build --no-link --print-out-paths .#nixosConfigurations.pi-main.config.system.build.toplevel)
```
## Secret Management
Secrets are managed with [sops-nix](https://github.com/Mic92/sops-nix) and
age keys. The encrypted `secrets/secrets.yaml` is committed to the repo; the
age private key lives on the Pi at `/var/lib/sops-nix/key.txt`.
```bash
# Edit secrets (decrypts, opens $EDITOR, re-encrypts on save)
sops secrets/secrets.yaml
# Encrypt a plaintext secrets.yaml for the first time
sops --encrypt --in-place secrets/secrets.yaml
# Add a new host key (after generating it on the new machine)
# 1. Add the public key to secrets/.sops.yaml
# 2. Run:
sops updatekeys secrets/secrets.yaml
# Generate a new age key on a host
age-keygen -o /var/lib/sops-nix/key.txt
age-keygen -y /var/lib/sops-nix/key.txt # print public key
```
Secrets that must come from the old deployment (see `PORTING.md` for how to
extract them from the old k8s cluster):
- `openldap/admin_password`, `openldap/config_password`, `openldap/ro_password`
- `gitea/admin_password`
- `nextcloud/admin_password`, `nextcloud/postgres_password`
Everything else (authelia JWT/session/encryption keys, gitea JWT tokens,
restic password, Cloudflare tokens) can be generated fresh.
## Code Style Guidelines
### YAML Structure
### Nix
1. **Document Separators**: Use `---` at the start of each YAML document
```yaml
---
apiVersion: v1
kind: ConfigMap
1. **Module pattern** — every service is an opt-in module with an `enable` option:
```nix
options.homey.myservice.enable = lib.mkEnableOption "My service";
config = lib.mkIf config.homey.myservice.enable { ... };
```
2. **Indentation**: Use 2 spaces (not tabs)
```yaml
spec:
containers:
- name: app
image: nginx
```
2. **`homeyConfig` specialArgs** — top-level site config (domain, org name,
timezone) is passed via `specialArgs` in `flake.nix` and accessed as
`homeyConfig` in every module. Do not read domain/org from hardcoded strings.
3. **Trailing Commas**: Optional but preferred for multi-line lists
```yaml
accessModes:
- ReadWriteMany
- ReadOnlyMany
```
3. **No secrets in the Nix store** — secrets are always read from sops-managed
files at runtime, never embedded in the built config. Use
`config.sops.secrets."key".path` to get the runtime path of a secret file.
4. **Quotes**: Use quotes for strings that might be interpreted as other types
- Always quote: `.Values.homey.url | quote`
- Optional for simple strings like names
4. **Secret injection pattern** — because `oci-containers` `environmentFiles`
is limited, use a `systemd ExecStartPre` script to write an ephemeral env
file at `/run/<service>-secrets.env` and reference it via `EnvironmentFile`.
Clean it up in `postStop`.
### Kubernetes Resources
5. **`--network=host`** — all containers use host networking for simplicity on
a single-node setup. Services communicate via `127.0.0.1:<port>`.
1. **Labels**: Use Kubernetes recommended labels
```yaml
labels:
app.kubernetes.io/name: openldap
app.kubernetes.io/component: auth
```
2. **Naming**: Use kebab-case for resource names
```yaml
name: openldap-admin
name: nextcloud-postgres
```
3. **Storage**: Always specify `storageClassName: longhorn`
```yaml
spec:
storageClassName: longhorn
```
### Helm Template Syntax
1. **Variable Assignment**: Use `$_ := set` for complex assignments
```yaml
{{- $_ := set $ "varname" (include "homey.lookuporgensecret" (merge (dict "secretname" "secret-name") $)) }}
```
2. **Include with Merge**: Always pass `$` as the last argument
```yaml
{{ include "homey.randomsecret" (merge (dict "secretname" "secret-name" "secretval" $secretval) $) }}
```
3. **Quote Values from .Values**: Use `quote` filter
```yaml
value: {{ .Values.homey.url | quote }}
```
4. **Template Definitions**: Define reusable templates in `_definitions.yaml`
- `homey.lookuporgensecret` - Look up existing secrets or generate random
- `homey.randomsecret` - Generate a random secret
- `homey.randHex` - Generate random hex string
5. **Template Spacing**: Use whitespace control to avoid extra newlines
```yaml
{{- "leading minus" -}} # No newline before
{{ "trailing minus" -}} # No newline after
```
### Secret Management
1. **Annotations**: Always annotate managed secrets to prevent deletion
```yaml
annotations:
"helm.sh/resource-policy": "keep"
```
2. **Secret Generation Pattern**:
```yaml
# Check for existing secret, create if not exists
{{- $secretObj := (lookup "v1" "Secret" .Release.Namespace "secret-name") | default dict -}}
{{- $secretData := (get $secretObj "data") | default dict -}}
{{- $pass := (get $secretData "password") | default (randAlphaNum 32 | b64enc) -}}
```
3. **Never hardcode secrets** - Use the secret lookup pattern above
### Config Files (files/ directory)
1. **Go Templates in Configs**: Use `tpl` function to process config files
```yaml
data:
config.yml: |-
{{ tpl (.Files.Get "files/authelia-config.yaml" | indent 4) . }}
```
2. **Accessing Variables**: Config files can access `.Values.*` and custom variables set in templates
### Ingress Configuration
1. **TLS**: Always specify TLS with proper hosts and secret
```yaml
spec:
ingressClassName: {{ .Values.homey.ingress_class }}
tls:
- hosts:
- auth.{{ .Values.homey.url }}
secretName: {{ .Values.homey.certname }}
```
2. **Authelia Integration**: Use auth snippets for protected ingresses
```yaml
annotations:
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
```
### Resource Organization
1. **File Structure**:
- `templates/_definitions.yaml` - Helper templates (secrets, utilities)
- `templates/auth.yaml` - Authentication services (OpenLDAP, Authelia, Gitea, Nextcloud, Radicale)
- `templates/media.yaml` - Media services (Jellyfin, Transmission)
- `templates/phpldapadmin.yaml` - LDAP admin interface
2. **Manifest Order** (within a file):
- PersistentVolumeClaim
- Secrets
- ConfigMaps
- Deployments
- Services
- Ingress
3. **Unused Resources**: Keep deprecated manifests in `unused/` directory
### Environment Variables
1. **Naming**: Use uppercase with underscores
```yaml
- name: LDAP_ORGANISATION
value: {{ .Values.homey.organization }}
```
2. **Value Sources**: Prefer `valueFrom.secretKeyRef` over inline values
```yaml
- name: PASSWORD
valueFrom:
secretKeyRef:
name: secret-name
key: password
```
### Volume Mounts
1. **subPath**: Use `subPath` for shared PVCs
```yaml
volumeMounts:
- mountPath: /data
subPath: service-name/data
```
2. **Read-only ConfigMaps**: Mark config mounts as read-only
```yaml
readOnly: true
```
## Common Operations
6. **Systemd ordering** — always express `after`/`requires` dependencies
explicitly. The external HD mount unit is `mnt-data.mount`; containers that
need storage must depend on it.
### Adding a New Service
1. Add values to `values.yaml`
2. Create/extend template in `templates/`
3. Add PVC if persistent storage needed
4. Add Ingress with appropriate annotations
5. Test with `helm template .`
1. Create `modules/services/<name>.nix` following the existing module pattern.
2. Add `homey.<name>.enable = false` as the default option.
3. Import the new module in `flake.nix` (in the `modules` list inside `mkHost`).
4. Enable it in `hosts/pi-main/default.nix`.
5. Add a Caddy virtual host block in `modules/caddy.nix`.
6. Add the service data directory to `modules/storage.nix` `tmpfiles.rules`.
7. Add the data path to the `paths` list in `modules/backup.nix`.
8. Add any new secrets to `secrets/secrets.yaml` (plaintext) and document them.
### Updating Secrets
Secrets are generated on first install. To regenerate:
```bash
kubectl delete secret <secret-name> -n homey
helm upgrade --install homey . -n homey
```
### Debugging Templates
### Updating or Regenerating Secrets
```bash
# Show all template variables available
helm template . --show-only templates/_helpers.tpl
# Edit the encrypted file — sops opens $EDITOR
sops secrets/secrets.yaml
# Render single template
helm template . --show-only templates/auth.yaml
# Copy updated secrets to the Pi and rebuild
rsync secrets/secrets.yaml admin@pi-main:/path/to/homey/secrets/
ssh admin@pi-main 'sudo nixos-rebuild switch --flake /path/to/homey#pi-main'
```
### Debugging Containers
```bash
# List all running containers
podman ps
# Follow logs for a service
journalctl -fu podman-authelia.service
# Drop into a running container
podman exec -it authelia sh
# Restart a single service
sudo systemctl restart podman-gitea.service
# Check why a service failed to start
systemctl status podman-openldap.service
journalctl -u podman-openldap.service --since "5 min ago"
```
---
## Outstanding TODOs
These items are known gaps that need to be addressed before the setup is
production-ready:
- [ ] **`caddy.nix` — fix `vendorHash`**: The Caddy build with the Cloudflare
DNS plugin uses `lib.fakeHash` as a placeholder. After the first `nix build`,
replace it with the hash Nix reports in the error message.
- [ ] **`hosts/pi-main/default.nix` — fill in real values**:
- SSH public key in `users.users.admin.openssh.authorizedKeys.keys`
- External HD device path in `homey.storage.device`
- Backup repository URL in `homey.backup.repository` — must be an S3-compatible
URL, e.g. `"s3:https://s3.us-west-002.backblazeb2.com/your-bucket-name"`
- [ ] **`secrets/secrets.yaml` — populate and encrypt**: Fill in all secret
values (old passwords from k8s + freshly generated ones, including
`restic/s3_access_key_id` and `restic/s3_secret_access_key`), then run
`sops --encrypt --in-place secrets/secrets.yaml` before committing.
- [x] **`secrets/.sops.yaml` — PGP key**: The encryption subkey
`076AA297579A0064` is already in `.sops.yaml`.
- [ ] **Cloudflare Tunnel**: Create the tunnel in the Zero Trust dashboard,
copy the tunnel token into secrets, and configure public hostnames. See
`modules/cloudflared.nix` and Phase 3 of `PORTING.md` for details.
- [ ] **Second machine**: When ready, add `hosts/pi-secondary/` and uncomment
the `pi-secondary` entry in `flake.nix`. Services communicating cross-machine
should reference the primary Pi's LAN IP instead of `127.0.0.1`.
- [ ] **Jellyfin and Transmission**: Both modules are written and importable
but disabled. Enable in `hosts/pi-main/default.nix` when ready:
```nix
homey.jellyfin.enable = true;
homey.transmission.enable = true;
```
- [ ] **Backup — S3 credentials**: Add `restic/s3_access_key_id` and
`restic/s3_secret_access_key` to secrets, and set `homey.backup.repository`
to your S3-compatible bucket URL in `hosts/pi-main/default.nix`.
- [ ] **Backup — offload script**: Write `scripts/offload-backup.sh` for
manually copying snapshots to a local disk (USB attached to Pi, or a disk
on your workstation). Uses `restic copy` to clone from the S3 repo into a
local restic repo on the target path. See `TODO.org` for design notes.
### Post- Pi first boot
These items require the Pi to be built, flashed, and booted at least once.
- [ ] **`secrets/.sops.yaml` — add Pi age key**: After generating the age key
on the Pi (`age-keygen -o /var/lib/sops-nix/key.txt`), add the public key
to `.sops.yaml` alongside the existing PGP key, then run
`sops updatekeys secrets/secrets.yaml`.
- [ ] **`hosts/pi-main/hardware.nix` — verify SD card labels**: The file
assumes partition labels `NIXOS_SD` (root) and `FIRMWARE` (boot). Relabel
after flashing if they differ, or update the `fileSystems` entries.
- [ ] **Gitea LDAP auth**: After first start, configure LDAP authentication
in Gitea's admin panel (Admin → Authentication Sources → Add LDAP source).
The old Helm chart had this commented out; it must be done manually once.
Relevant settings:
- Host: `127.0.0.1`, Port: `389`, Security: Unencrypted
- Bind DN: `cn=readonly,dc=zakobar,dc=com`
- User search base: `ou=users,dc=zakobar,dc=com`
- [ ] **Nextcloud LDAP app**: After restoring the Nextcloud volume, verify
the LDAP Users and Contacts app is still configured correctly
(Admin → LDAP/AD Integration).
-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` | `zakobar.com` | `https://localhost:443` |
| `git` | `zakobar.com` | `https://localhost:443` |
| `nextcloud` | `zakobar.com` | `https://localhost:443` |
| `ldapadmin` | `zakobar.com` | `https://localhost:443` |
| `jellyfin` | `zakobar.com` | `https://localhost:443` |
| `torrent` | `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 `zakobar.com` pointing to your tunnel's UUID (Cloudflare
creates this automatically when you add hostnames). You do not need to add
`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=zakobar,dc=com -D "cn=admin,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.zakobar.com
# Gitea login?
# Visit https://git.zakobar.com — should redirect to authelia if not logged in
# Nextcloud?
# Visit https://nextcloud.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 zakobar.com
192.168.1.100 auth.zakobar.com
192.168.1.100 git.zakobar.com
192.168.1.100 nextcloud.zakobar.com
192.168.1.100 ldapadmin.zakobar.com
192.168.1.100 jellyfin.zakobar.com
192.168.1.100 torrent.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`.
+192 -7
View File
@@ -2,7 +2,146 @@
A home environment for everyone!
* Installation
* NixOS Deployment (active branch: nixos-port)
** Prerequisites
Before building, make sure the following are set in the repo:
- =hosts/pi-main/default.nix= — SSH public key, static IP, WiFi SSID
- =secrets/secrets.yaml= — all secrets populated and sops-encrypted
- WiFi password secret formatted as =wifi_psk=YourPassword= (see below)
** Adding / updating secrets
#+begin_src bash
sops secrets/secrets.yaml
#+end_src
Opens your editor with the decrypted file. Save and quit to re-encrypt.
The WiFi password entry must use the =wifi_psk== prefix so wpa_supplicant
can look up the value by name:
#+begin_src yaml
wifi/psk: "wifi_psk=YourActualWifiPassword"
#+end_src
** Phase 1 — Bootstrap image (flash this first)
The full =pi-main= config requires sops secrets, which require an age key
on the Pi — but the age key doesn't exist until after first boot. To
break the chicken-and-egg problem, flash a minimal bootstrap image first.
Before building, fill in the WiFi password in =flake.nix= in the
=pi-main-bootstrap= config (search for =WIFI_PASSWORD_HERE=):
#+begin_src nix
networks."Zakobar".psk = "your-actual-wifi-password";
#+end_src
Build the bootstrap SD image (requires =aarch64-linux= build capability —
either =boot.binfmt.emulatedSystems = ["aarch64-linux"]= on your
workstation, or an aarch64 remote builder):
#+begin_src bash
nix build .#nixosConfigurations.pi-main-bootstrap.config.system.build.sdImage \
--system aarch64-linux
#+end_src
Find your SD card device, then flash (double-check =/dev/sdX=!):
#+begin_src bash
lsblk
zstdcat result/sd-image/nixos-sd-image-*.img.zst | \
sudo dd of=/dev/sdX bs=4M status=progress conv=fsync
#+end_src
The Pi will boot at =192.168.1.100=, connect to =Zakobar= WiFi, and accept
SSH connections with your key. No services run yet.
** Phase 2 — Generate age key and re-encrypt secrets
#+begin_src bash
# SSH into the Pi
ssh admin@192.168.1.100
# Generate the age key
sudo age-keygen -o /var/lib/sops-nix/key.txt
# Print the public key — copy it
sudo age-keygen -y /var/lib/sops-nix/key.txt
#+end_src
Back on your workstation, add the public key to =secrets/.sops.yaml=
alongside the existing PGP key:
#+begin_src yaml
keys:
- &pi_main age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
creation_rules:
- path_regex: secrets/secrets.yaml$
key_groups:
- pgp:
- 076AA297579A0064
age:
- *pi_main
#+end_src
Then re-encrypt so the Pi can decrypt its own secrets:
#+begin_src bash
sops updatekeys secrets/secrets.yaml
#+end_src
** Phase 3 — Deploy the full config
#+begin_src bash
nixos-rebuild switch \
--flake .#pi-main \
--target-host admin@192.168.1.100 \
--build-host admin@192.168.1.100 \
--use-remote-sudo
#+end_src
The Pi builds its own config natively (no cross-compilation). sops-nix
will now decrypt all secrets and start all services.
** Caddy plugin hash
The first deploy will fail at the Caddy build step because =lib.fakeHash=
is a placeholder. Copy the correct hash from the error output and replace
it in =modules/caddy.nix=:
#+begin_src nix
caddyWithCloudflare = pkgs.caddy.withPlugins {
plugins = [ "github.com/caddy-dns/cloudflare@..." ];
hash = "sha256-REPLACE_WITH_REAL_HASH="; # ← paste here
};
#+end_src
Then re-run the deploy command from Phase 3.
** Ongoing deploys from workstation
All future config changes follow the same pattern:
1. Edit files on workstation
2. Run:
#+begin_src bash
nixos-rebuild switch \
--flake .#pi-main \
--target-host admin@192.168.1.100 \
--build-host admin@192.168.1.100 \
--use-remote-sudo
#+end_src
NixOS activates the new config on the Pi immediately, with an automatic
rollback if activation fails.
* Installation (legacy Helm)
Install using
@@ -12,11 +151,57 @@ helm upgrade --install homey . -n homey
* Backing up
We must find a better solution
Backups use [[https://restic.net/][restic]] and run automatically via systemd on a daily schedule.
https://perfectmediaserver.com/day-two/top10apps.html
** Strategy — two tiers
Nefarious
1. *Primary (automatic)*: Daily backup to an S3-compatible bucket (Backblaze B2,
Wasabi, AWS S3, etc.). Restic deduplicates and encrypts before upload.
Retention: 7 daily, 4 weekly, 6 monthly snapshots.
2. *Offload (manual)*: Run =scripts/offload-backup.sh --target /path/to/disk=
to clone snapshots from the S3 repo onto a local disk (USB plugged into the
Pi, or a disk on your workstation). Uses =restic copy= so deduplication is
preserved on the target.
** What is backed up
All service data under =/mnt/data/=:
- =openldap/= — LDAP database and config
- =authelia/= — Authelia config and state
- =gitea/= — Gitea repositories and data
- =nextcloud/= — Nextcloud files + a =pg_dump= of the database
- =jellyfin/= — Jellyfin metadata (media files are excluded — re-downloadable)
- =transmission/= — Torrent client config
Nextcloud is placed into maintenance mode and postgres is =pg_dump='d before
each backup to ensure a consistent snapshot.
** Configuration
Repository URL and credentials are set per-host:
#+begin_src nix
# hosts/pi-main/default.nix
homey.backup.repository = "s3:https://s3.us-west-002.backblazeb2.com/your-bucket";
#+end_src
S3 credentials live in =secrets/secrets.yaml= as =restic/s3_access_key_id= and
=restic/s3_secret_access_key=.
** Restore
#+begin_src bash
# List snapshots
restic -r s3:https://... snapshots
# Restore latest snapshot to /mnt/data
restic -r s3:https://... restore latest --target /mnt/data
# Restore a single service
restic -r s3:https://... restore latest --target /mnt/data --include /mnt/data/gitea
#+end_src
* LDAP Configuration
@@ -24,7 +209,7 @@ Logins are done to PHPLDAPADMIN
DN is like:
cn=admin,dc=home,dc=,dc=io
cn=admin,dc=,dc=io
get-secret-val.sh homey openldap-admin password
First thing we do is create an organization unit called users
@@ -62,9 +247,9 @@ Add a new LDAP Authentication source
Authentication name: Home LDAP
Host: openldap
Port: 389
Bind DN = cn=readonly,dc=home,dc=,dc=io
Bind DN = cn=readonly,dc=,dc=io
Bind Password: openldap-ro password
User Search Base: ou=users,dc=home,dc=,dc=io
User Search Base: ou=users,dc=,dc=io
user search filter = (uid=%s)
Admin filter (title=admin)
Username Attribute: uid
+286
View File
@@ -0,0 +1,286 @@
#+TITLE: Homey NixOS Port — Outstanding Tasks
#+DATE: 2026-04-15
#+OPTIONS: toc:nil
* Secrets Setup
** DONE Configure sops recipients in =secrets/.sops.yaml=
GPG encryption subkey =076AA297579A0064= is already configured in
=.sops.yaml=. The Pi's age key will be added post-boot (see Deployment section).
** DONE Populate =secrets/secrets.yaml= with real values
Every key in =secrets/secrets.yaml= needs a real value. Fill in each row below.
*** Recovered from k8s backup
| Key | Source k8s secret | Value |
|----------------------------------+-------------------------+----------------------------------|
| openldap/admin_password | openldap-admin | lfQWQgBZporyJT4xFSJTnu4vQMC7UevW |
| openldap/config_password | openldap-config | ZxlWbDAeHLdHi5lgdxmyZOWzsG3qDgrT |
| openldap/ro_password | openldap-ro | CZ7JLn23vSzhVjNW7UHGZ2YLFJPDLGsF |
| gitea/admin_password | gitea-admin-pass | y5kCPeCP1e1sCzahd7QmLyJqdQvd37ek |
| nextcloud/postgres_password | nextcloud-postgres-pass | hEq4zt1B1VKYtVAoiKYDmswcUmTbknSP |
| authelia/jwt_secret | jwt-secret | YJZBnQCD4OmhJkgdr6kksmMCatrKLCl3 |
| authelia/storage_encryption_key | fek-secret | KYWRYApCWWIN60gpSi7jhLuj1Wcm5z9Q |
*** Needs generation or manual creation
| Key | Action |
|----------------------------------+---------------------------------------------------|
| authelia/session_secret | Generate fresh (64 random chars) |
| gitea/lfs_jwt_secret | Generate fresh (43-char base64url) |
| gitea/oauth2_jwt_secret | Generate fresh (43-char base64url) |
| gitea/internal_token | Generate fresh (100-char alphanumeric) |
| restic/password | Generate fresh (passphrase) |
| nextcloud/admin_password | NOT in k8s backup; try old value or reset later |
| cloudflare/api_token | Create DNS Edit token in Cloudflare dashboard |
| cloudflare/tunnel_token | Create tunnel in Cloudflare Zero Trust dashboard |
| restic/s3_access_key_id | Needs S3 provider credentials |
| restic/s3_secret_access_key | Needs S3 provider credentials |
Generate random secrets with:
#+begin_src bash
# 64-char hex string
openssl rand -hex 32
# base64url (for gitea tokens)
openssl rand -base64 32 | tr '+/' '-_' | tr -d '='
# 100-char alphanumeric (gitea internal token)
openssl rand -base64 75 | tr -dc 'A-Za-z0-9' | head -c 100
#+end_src
** DONE Encrypt =secrets/secrets.yaml= with sops
Encrypted with PGP key =076AA297579A0064=. Safe to commit.
* Pi Hardware Setup
** DONE Fill in real values in =hosts/pi-main/default.nix=
- [X] =users.users.admin.openssh.authorizedKeys.keys= — SSH public key added
- [X] =homey.storage.device==/dev/disk/by-id/usb-WD_Ext_HDD_1021_5743415A4146313531393031-0:0-part1=
- [X] =homey.backup.repository==s3:https://s3.us-east-005.backblazeb2.com/zakobar-home-backup=
** DONE Integrate nixos-raspberrypi flake
Replaced =nixos-hardware= with =nixos-raspberrypi= for vendor kernel, firmware,
u-boot bootloader, and binary cache. Both =pi-main= and =pi-main-bootstrap=
now use =nixos-raspberrypi.lib.nixosSystem= and =raspberry-pi-4.base=.
=nix flake check= passes.
** TODO Verify SD card partition labels in =hosts/pi-main/hardware.nix=
The config assumes labels =NIXOS_SD= (root) and =FIRMWARE= (boot).
After flashing, check with:
#+begin_src bash
lsblk -o NAME,LABEL
#+end_src
Update =fileSystems= entries in =hosts/pi-main/hardware.nix= if they differ.
* Caddy Build
** TODO Fix =vendorHash= in =modules/caddy.nix=
The Caddy build with the Cloudflare DNS plugin currently uses =lib.fakeHash=
as a placeholder. After the first =nix build= attempt it will fail with the
correct hash in the error message. Replace =lib.fakeHash= with that value.
* Cloudflare Setup
** DONE Create Cloudflare Tunnel
1. Go to Cloudflare Zero Trust dashboard → Networks → Tunnels → Create tunnel
2. Name it (e.g. =homey=)
3. Copy the tunnel token into =secrets/secrets.yaml= under =cloudflare/tunnel_token=
4. Configure public hostnames for each service (see service/URL table in AGENTS.md)
** DONE Create Cloudflare DNS API token
1. Cloudflare dashboard → My Profile → API Tokens → Create Token
2. Use the "Edit zone DNS" template, scope to =zakobar.com=
3. Copy into =secrets/secrets.yaml= under =cloudflare/api_token=
* Deployment
** TODO Phase 1 — Build and flash bootstrap SD card image
The bootstrap image is a minimal NixOS with SSH + WiFi only (no sops, no
services). Its sole purpose is to boot the Pi so you can generate the age key
and then deploy the full config remotely.
Build on workstation (cross-compiles for aarch64):
#+begin_src bash
# Accept the nixos-raspberrypi cache config so pre-built kernel/firmware
# are fetched instead of compiled. First build still takes ~10-20 min.
nix build .#nixosConfigurations.pi-main-bootstrap.config.system.build.sdImage \
--accept-flake-config
#+end_src
Flash to SD card (replace =/dev/sdX= with your card's device):
#+begin_src bash
# Decompress and write in one step — avoids storing the raw image on disk
zstdcat result/sd-image/*.img.zst | sudo dd of=/dev/sdX bs=4M status=progress conv=fsync
sudo sync
#+end_src
Insert the SD card into the Pi and power it on.
It will connect to WiFi (=Zakobar=) with static IP =192.168.1.100=.
Verify SSH access (wait ~60 s for first boot):
#+begin_src bash
ssh admin@192.168.1.100
#+end_src
** TODO Phase 2 — Generate age key and add it to sops
On the Pi (over SSH):
#+begin_src bash
sudo mkdir -p /var/lib/sops-nix
sudo age-keygen -o /var/lib/sops-nix/key.txt
# Print the public key — copy this output to your workstation clipboard
sudo age-keygen -y /var/lib/sops-nix/key.txt
#+end_src
On the workstation — edit =secrets/.sops.yaml=, uncomment the age section
and replace the placeholder with the public key you just copied:
#+begin_src yaml
key_groups:
- pgp:
- 076AA297579A0064
age:
- age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # paste here
#+end_src
Re-encrypt =secrets/secrets.yaml= so the Pi's age key can decrypt it:
#+begin_src bash
sops updatekeys secrets/secrets.yaml
#+end_src
Commit and push:
#+begin_src bash
git add secrets/.sops.yaml secrets/secrets.yaml
git commit -m "add Pi age key to sops recipients"
#+end_src
** TODO Phase 3 — Fix Caddy vendorHash, then deploy full config
The full =pi-main= config includes Caddy built with the Cloudflare DNS
plugin. The first build will fail with the correct hash in the error output.
Attempt the build to get the hash:
#+begin_src bash
nix build .#nixosConfigurations.pi-main.config.system.build.toplevel \
--accept-flake-config 2>&1 | grep 'got:'
#+end_src
Copy the hash from the error message and replace =lib.fakeHash= in
=modules/caddy.nix=, then commit:
#+begin_src bash
git add modules/caddy.nix
git commit -m "fix caddy vendorHash"
#+end_src
Deploy to the Pi:
#+begin_src bash
# Dry-run first — shows what will change without applying
nixos-rebuild dry-activate \
--flake .#pi-main \
--target-host admin@192.168.1.100 \
--use-remote-sudo \
--accept-flake-config
# Apply when happy with the diff
nixos-rebuild switch \
--flake .#pi-main \
--target-host admin@192.168.1.100 \
--use-remote-sudo \
--accept-flake-config
#+end_src
After a successful switch, subsequent deploys can use the hostname:
#+begin_src bash
nixos-rebuild switch --flake .#pi-main --target-host admin@pi-main --use-remote-sudo
#+end_src
* Post-Deployment Manual Steps
** DONE Configure Gitea LDAP authentication
Admin → Site Administration → Authentication Sources → Add LDAP (via BindDN):
- Host: =127.0.0.1=, Port: =389=, Security: Unencrypted
- Bind DN: =cn=readonly,dc=zakobar,dc=com=
- Bind Password: see =openldap/ro_password= in sops
- User Search Base: =ou=users,dc=zakobar,dc=com=
- User Filter: =(&(objectClass=inetOrgPerson)(uid=%s))=
- Username attribute: =uid=
- First name attribute: =cn=
- Surname attribute: =sn=
- Email attribute: =mail=
** TODO Verify Nextcloud LDAP app configuration
After restoring the Nextcloud volume, check:
Admin → LDAP/AD Integration — confirm the LDAP Users and Contacts app is configured.
If reconfiguring from scratch, use the same settings as Gitea above but with
Nextcloud's LDAP wizard:
- Server: =127.0.0.1=, Port: =389=
- Bind DN: =cn=readonly,dc=zakobar,dc=com=
- Bind Password: see =openldap/ro_password= in sops
- Base DN: =dc=zakobar,dc=com=
- Users: filter =objectClass=inetOrgPerson=, search base =ou=users=
- Login attribute: =uid=
- Email attribute: =mail=
** TODO (Optional) Enable Jellyfin and Transmission
When ready, in =hosts/pi-main/default.nix=:
#+begin_src nix
homey.jellyfin.enable = true;
homey.transmission.enable = true;
#+end_src
* Backup Strategy
** TODO Configure S3-compatible automatic backup target
Update =homey.backup.repository= in =hosts/pi-main/default.nix= to point at
your S3-compatible bucket (Backblaze B2, Wasabi, AWS S3, etc.):
#+begin_src nix
homey.backup.repository = "s3:https://s3.us-west-002.backblazeb2.com/your-bucket-name";
# or for AWS:
# homey.backup.repository = "s3:s3.amazonaws.com/your-bucket-name";
#+end_src
Add the S3 credentials to =secrets/secrets.yaml=:
#+begin_src yaml
restic/s3_access_key_id: "YOUR_KEY_ID"
restic/s3_secret_access_key: "YOUR_SECRET_KEY"
#+end_src
Then wire them into =modules/backup.nix= via environment variables:
=AWS_ACCESS_KEY_ID= and =AWS_SECRET_ACCESS_KEY= (restic reads these natively).
The existing daily schedule + prune retention in =modules/backup.nix= will
handle the rest automatically.
** TODO Write manual offload script (=scripts/offload-backup.sh=)
A standalone script for copying backup data to an external disk — either
plugged directly into the Pi or mounted on your workstation.
Design:
- Accepts a =--target= argument: a local path to the mounted disk
(e.g. =/media/aner/backup-disk= or =/mnt/usb=)
- Uses =restic copy= to clone snapshots from the S3 repo into a local restic
repo on the target disk (deduplication is preserved, no double storage)
- Alternatively can use =rsync= for a plain directory copy if restic is not
available on the target machine
- Should be runnable from either the Pi or a workstation (with the Pi's data
disk mounted or accessible over SSH)
Example invocation:
#+begin_src bash
# On the Pi, with USB disk mounted at /mnt/usb:
./scripts/offload-backup.sh --target /mnt/usb/homey-backup
# On workstation, with Pi data disk mounted locally:
./scripts/offload-backup.sh --target /media/aner/backup-disk/homey-backup
#+end_src
This script does not exist yet — needs to be written.
* Future
** TODO Add second machine (=pi-secondary=)
When ready:
1. Create =hosts/pi-secondary/= directory with =default.nix= and =hardware.nix=
2. Uncomment the =pi-secondary= entry in =flake.nix=
3. Services communicating cross-machine should reference the primary Pi's LAN IP
instead of =127.0.0.1=
-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"
+321
View File
@@ -0,0 +1,321 @@
#+TITLE: Caddy, Cloudflare Tunnel & TLS Setup
#+DATE: 2026-04-23
#+AUTHOR: homey project
#+OPTIONS: toc:2 num:t
* Overview
This document describes the TLS and reverse-proxy architecture for the homey
self-hosted stack, the problems encountered while getting it working, and the
final configuration that resolved them. It is intended as a reference for
future debugging and for adding new services.
** Traffic flow
#+BEGIN_EXAMPLE
Browser
│ HTTPS (TLS terminated by Cloudflare edge, *.zakobar.com cert)
Cloudflare edge (anycast IP)
│ QUIC/HTTP2 tunnel (outbound from Pi, no open inbound ports)
cloudflared daemon on Pi (systemd: cloudflared-tunnel.service)
│ plain HTTP on loopback http://localhost:80
Caddy reverse proxy (systemd: caddy.service, port 80 + 443)
│ proxies to backend by Host header
Service container (podman, port on 127.0.0.1)
#+END_EXAMPLE
Key points:
- TLS to the browser is provided entirely by Cloudflare's Universal SSL cert
(~*.zakobar.com~), not by the Pi's Let's Encrypt cert.
- The Pi's Let's Encrypt cert (~*.zakobar.com~ via DNS-01) is used only for
direct LAN access (bypassing the tunnel).
- The tunnel leg (cloudflared → Caddy) is plain HTTP on loopback — this is
safe because both endpoints are the same machine.
* Components
** Caddy (~modules/caddy.nix~)
Caddy runs as a NixOS service (~services.caddy~) using a custom build that
includes the ~caddy-dns/cloudflare~ plugin for DNS-01 ACME challenges.
*** Custom build
The nixpkgs ~caddy~ package does not include the Cloudflare DNS plugin by
default. It is built using the ~withPlugins~ passthru function (backed by
xcaddy):
#+BEGIN_SRC nix
caddyWithCloudflare = pkgs.caddy.withPlugins {
plugins = [
"github.com/caddy-dns/cloudflare@v0.2.4"
];
hash = "sha256-...";
};
#+END_SRC
The ~hash~ is a fixed-output derivation hash that must be updated whenever
the plugin version changes. Use ~lib.fakeHash~ to trigger a build failure
that prints the correct hash, then substitute it.
*** API token injection
The Cloudflare API token is stored in sops (~cloudflare/api_token~) and
injected into the Caddy process via ~systemd LoadCredential~:
#+BEGIN_SRC nix
serviceConfig.LoadCredential =
"cloudflare_api_token:${config.sops.secrets."cloudflare/api_token".path}";
ExecStart = lib.mkForce [
""
(pkgs.writeShellScript "caddy-start" ''
export CLOUDFLARE_API_TOKEN=$(cat "$CREDENTIALS_DIRECTORY/cloudflare_api_token")
exec caddy run --environ --config /etc/caddy/caddy_config --adapter caddyfile
'')
];
#+END_SRC
*** Virtual hosts — dual HTTP/HTTPS entries
Each service has *two* Caddyfile vhost entries:
| Entry | Purpose |
|---|---|
| ~git.zakobar.com~ | HTTPS — for direct LAN access; Caddy handles TLS |
| ~http://git.zakobar.com~ | HTTP — for cloudflared on loopback; no redirect |
Caddy's default behaviour is to automatically redirect HTTP → HTTPS for any
hostname that has a matching HTTPS vhost. By explicitly defining an
~http://~ vhost, that redirect is suppressed and cloudflared gets a direct
200 response instead of a redirect loop.
Without the ~http://~ vhost, accessing via the tunnel produces:
~ERR_TOO_MANY_REDIRECTS~ in the browser because cloudflared follows the 308
back to HTTP indefinitely.
*** Global config
#+BEGIN_SRC caddyfile
{
email admin@zakobar.com
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
#+END_SRC
The ~acme_dns~ directive in the global block tells Caddy to use DNS-01
challenges for *all* HTTPS vhosts. This allows wildcard and multi-level
subdomain certs to be issued without any inbound port 80 requirement.
** Cloudflare Tunnel (~modules/cloudflared.nix~)
cloudflared runs as a plain systemd service using the token-based tunnel
approach (~cloudflared tunnel run --token~). No local credentials file or
config file is needed — just the tunnel token from the Zero Trust dashboard.
*** Tunnel configuration (Zero Trust dashboard)
One wildcard public hostname entry covers all services:
| Field | Value |
|---|---|
| Hostname | ~*.zakobar.com~ |
| Service | ~http://localhost:80~ |
| No TLS Verify | off (not needed for HTTP) |
| HTTP Host Header | (empty — cloudflared forwards the real Host header) |
| Origin Server Name | (empty — not needed for HTTP) |
cloudflared automatically forwards the incoming ~Host~ header (e.g.
~git.zakobar.com~) to Caddy, which uses it to select the correct vhost and
backend.
*** DNS records
A single wildcard CNAME record in Cloudflare DNS covers all subdomains:
#+BEGIN_EXAMPLE
*.zakobar.com CNAME <tunnel-id>.cfargotunnel.com (proxied, orange cloud)
#+END_EXAMPLE
This means new services require no DNS changes — only a new Caddy vhost.
*** Cloudflare SSL/TLS mode
Set to *Full (strict)* in the Cloudflare dashboard (SSL/TLS → Overview).
| Mode | Meaning |
|---|---|
| Off | No HTTPS to browser |
| Flexible | HTTPS to browser, HTTP to origin |
| Full | HTTPS to browser, HTTPS to origin (cert not validated) |
| Full (strict) | HTTPS to browser, HTTPS to origin (cert must be valid) |
Full (strict) works here because Cloudflare terminates TLS at its own edge
using its Universal cert, and the origin (cloudflared → Caddy) uses plain
HTTP which Cloudflare does not validate in this tunnel architecture.
* Problems Encountered & How They Were Resolved
** 1. ~caddy-dns/cloudflare~ rejected ~cfut_~ token format
*Symptom:*
#+BEGIN_EXAMPLE
provision dns.providers.cloudflare: API token 'cfut_...' appears invalid;
ensure it's correctly entered and not wrapped in braces nor quotes
#+END_EXAMPLE
*Cause:*
Cloudflare introduced new token formats with a ~cfut_~ (user token) or
~cfat_~ (account token) prefix. These tokens are 54 characters long. The
~caddy-dns/cloudflare~ plugin had a validation regex ~{35,50}~ that rejected
tokens longer than 50 characters, failing before even making an API call.
*Fix:*
The fix was merged into the plugin's master branch as commit ~a8737d0~ and
included in the ~v0.2.4~ tag (despite the tag previously being associated
with an older tree — the proxy confirmed ~v0.2.4~ resolves to ~a8737d0~).
Updating the ~hash~ in ~caddy.nix~ to the value produced by ~lib.fakeHash~
forced a fresh fetch of the corrected ~v0.2.4~ tree:
#+BEGIN_SRC nix
plugins = [ "github.com/caddy-dns/cloudflare@v0.2.4" ];
hash = lib.fakeHash; # replace with hash from build error output
#+END_SRC
Run ~nix build .#nixosConfigurations.pi-main.config.system.build.toplevel~,
copy the ~got:~ hash from the error, substitute it, and rebuild.
** 2. cloudflared ~tls: internal error~ (SNI mismatch)
*Symptom:*
#+BEGIN_EXAMPLE
Unable to reach the origin service: remote error: tls: internal error
originService=https://localhost:443
#+END_EXAMPLE
*Cause:*
cloudflared connected to ~https://localhost:443~ without sending an SNI
(Server Name Indication) hostname in the TLS ClientHello. Caddy could not
match any vhost, had no certificate for ~localhost~, and aborted the
handshake with a TLS internal error.
Setting the ~HTTP Host Header~ override in the dashboard fixes the HTTP
layer but does *not* affect the TLS SNI, which is negotiated before HTTP
headers are exchanged.
Setting the ~Origin Server Name~ field does set the SNI, but for a wildcard
rule (~*.zakobar.com~) the dashboard only accepts a static value, not a
dynamic placeholder — so it cannot be used for a catch-all.
*Fix:*
Switch the tunnel service from ~https://localhost:443~ to
~http://localhost:80~. The internal leg does not need TLS (loopback
interface, same machine). Caddy's HTTP vhosts handle the requests directly.
** 3. Cloudflare edge TLS handshake failure (~*.home.zakobar.com~)
*Symptom:*
#+BEGIN_EXAMPLE
TLS connect error: error:0A000410:SSL routines::ssl/tls alert handshake failure
#+END_EXAMPLE
*Cause:*
The domain was originally configured as ~home.zakobar.com~ (base domain),
making all services two levels deep: ~git.home.zakobar.com~,
~auth.home.zakobar.com~, etc. Cloudflare's free Universal SSL certificate
covers only one level of wildcard: ~*.zakobar.com~. It does *not* cover
~*.home.zakobar.com~ (two levels). The Cloudflare edge had no certificate to
present to browsers for these hostnames, causing a TLS handshake failure
before the request ever reached the tunnel.
*Fix:*
Move all services to single-level subdomains under ~zakobar.com~
(~git.zakobar.com~, ~auth.zakobar.com~, etc.). In the NixOS config this
required only one line change — the ~domain~ field in ~flake.nix~:
#+BEGIN_SRC nix
domain = "zakobar.com"; # was "home.zakobar.com"
#+END_SRC
All modules reference ~homeyConfig.domain~ and updated automatically on
rebuild. Tunnel hostnames and DNS records in the Cloudflare dashboard were
updated to match.
** 4. ~ERR_TOO_MANY_REDIRECTS~ via tunnel
*Symptom:*
Browser shows ~ERR_TOO_MANY_REDIRECTS~ when accessing any service through
the Cloudflare tunnel.
*Cause:*
cloudflared was talking to Caddy over plain HTTP (~http://localhost:80~).
Caddy's default behaviour is to issue a 308 permanent redirect from HTTP to
HTTPS for any hostname that has a matching HTTPS vhost. cloudflared followed
the redirect back to ~http://localhost:80~, which redirected again,
indefinitely.
*Fix:*
Add explicit ~http://~ vhost entries in ~caddy.nix~ for every service. When
Caddy has an explicit HTTP vhost for a hostname, it serves it directly
without redirecting:
#+BEGIN_SRC nix
"git.${domain}" = {
extraConfig = "reverse_proxy localhost:3000";
};
"http://git.${domain}" = { # ← suppresses HTTP→HTTPS redirect
extraConfig = "reverse_proxy localhost:3000";
};
#+END_SRC
* Adding a New Service
To expose a new service through the tunnel:
1. Create ~modules/services/<name>.nix~ following the module pattern.
2. Add both a plain and ~http://~ vhost in ~modules/caddy.nix~:
#+BEGIN_SRC nix
"<name>.${domain}" = {
extraConfig = "reverse_proxy localhost:<port>";
};
"http://<name>.${domain}" = {
extraConfig = "reverse_proxy localhost:<port>";
};
#+END_SRC
3. No DNS or tunnel changes needed — the wildcard CNAME and wildcard tunnel
rule (~*.zakobar.com~) cover new subdomains automatically.
4. Rebuild and switch: ~sudo nixos-rebuild switch --flake .#pi-main~
* Certificate Details
** Let's Encrypt cert (LAN access)
- Issued per-hostname by Caddy via DNS-01 ACME using the Cloudflare API.
- Covers each hostname individually (e.g. ~git.zakobar.com~).
- Stored in ~/var/lib/caddy/.local/share/caddy/certificates/~.
- Used only when accessing services directly on the LAN (bypassing tunnel).
- Auto-renewed by Caddy.
** Cloudflare Universal SSL cert (tunnel / remote access)
- Issued by Google Trust Services for ~*.zakobar.com~.
- Managed entirely by Cloudflare — no action required on the Pi.
- Covers all single-level subdomains (~git.zakobar.com~, ~auth.zakobar.com~, etc.).
- Does *not* cover two-level subdomains (~git.home.zakobar.com~) — this was
the root cause of problem #3 above.
* Quick Reference: Debugging Checklist
| Symptom | Where to look | Command |
|---|---|---|
| 502 Bad Gateway | cloudflared logs | ~journalctl -u cloudflared-tunnel -n 50~ |
| 502 Bad Gateway | Caddy → backend | ~curl http://localhost:<port>/~ |
| TLS internal error | SNI / cert issue | ~curl -sv --resolve host:443:127.0.0.1 https://host/~ |
| Too many redirects | HTTP vhost missing | check ~http://~ entries in caddy.nix |
| Handshake failure at edge | Cloudflare cert scope | check SSL/TLS → Edge Certificates |
| Token appears invalid | plugin version | check ~caddy-dns/cloudflare~ version vs token format |
| Caddy won't start | token / config error | ~journalctl -u caddy --since "5 min ago"~ |
-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;
}
+105
View File
@@ -0,0 +1,105 @@
{
description = "Homey - self-hosted home server NixOS configuration";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
# sops-nix for secret management
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
# nixos-hardware provides RPi4 wireless firmware.
# We use only the minimal pieces needed for a headless server —
# no display, audio, or bluetooth modules.
nixos-hardware.url = "github:NixOS/nixos-hardware/master";
};
outputs = { self, nixpkgs, sops-nix, nixos-hardware, ... }@inputs:
let
# Shared specialArgs passed to every host
commonArgs = {
inherit inputs;
# Top-level site config — override per-host if needed
homeyConfig = {
domain = "zakobar.com"; # base domain for all services
organization = "Zakobar Home Server";
timezone = "Asia/Jerusalem";
};
};
# Minimal RPi4 hardware module for a headless server.
# Provides only: bootloader, initrd modules, wireless firmware, DTB filter.
# Deliberately excludes display, audio, bluetooth from the full nixos-hardware module.
rpi4Headless = { pkgs, ... }: {
boot.loader.grub.enable = false;
boot.loader.generic-extlinux-compatible.enable = true;
boot.initrd.availableKernelModules = [
"pcie-brcmstb" # PCIe bus (USB3, NVMe)
"reset-raspberrypi" # required for vl805 firmware
"usb-storage"
"usbhid"
"vc4" # VideoCore (needed even headless for boot)
];
# sd-image-aarch64.nix lists modules for many SoCs (including sun4i-drm
# for Allwinner boards) that don't exist in linux_rpi4. Allow missing.
boot.initrd.includeDefaultModules = false;
hardware.deviceTree.filter = "bcm2711-rpi-*.dtb";
hardware.firmware = [
(pkgs.callPackage "${nixos-hardware}/raspberry-pi/common/raspberry-pi-wireless-firmware.nix" {})
];
};
mkHost = { hostPath, extraModules ? [] }:
nixpkgs.lib.nixosSystem {
specialArgs = commonArgs;
modules = [
sops-nix.nixosModules.sops
rpi4Headless
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 = {
# Bootstrap image — flash this first, then deploy pi-main.
# See hosts/pi-main-bootstrap/default.nix for details.
pi-main-bootstrap = nixpkgs.lib.nixosSystem {
specialArgs = commonArgs;
modules = [
rpi4Headless
({ modulesPath, ... }: {
imports = [ "${modulesPath}/installer/sd-card/sd-image-aarch64.nix" ];
})
./hosts/pi-main/hardware.nix
./hosts/pi-main-bootstrap/default.nix
];
};
# Primary Raspberry Pi 4
pi-main = mkHost {
hostPath = ./hosts/pi-main/default.nix;
};
# Future second machine (placeholder — uncomment and configure when ready)
# pi-secondary = mkHost {
# 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"
+70
View File
@@ -0,0 +1,70 @@
{ pkgs, lib, homeyConfig, ... }:
# Bootstrap image for the primary Raspberry Pi 4.
#
# Flash this image first. Its only purpose is to boot the Pi so you can:
# 1. Generate the age key: sudo age-keygen -o /var/lib/sops-nix/key.txt
# 2. Print the pubkey: sudo age-keygen -y /var/lib/sops-nix/key.txt
# 3. Add the pubkey to .sops.yaml, re-encrypt secrets, then deploy pi-main.
#
# No sops, no services, no external HD — just SSH + WiFi.
#
# WiFi PSK: uncomment and fill in before building. Do not commit the password.
# networks."YourSSID".psk = "your-wifi-password";
{
networking.hostName = "pi-main";
time.timeZone = homeyConfig.timezone;
i18n.defaultLocale = "en_US.UTF-8";
system.stateVersion = "25.05";
nix.settings = {
experimental-features = [ "nix-command" "flakes" ];
substituters = [
"https://cache.nixos.org"
"https://nix-community.cachix.org"
];
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"nix-community.cachix.org-1:mB9FkXj6Q3Q4ohOcbM4FJ9Z1X2kCrVK4vZOqsDqqNqk="
];
};
nixpkgs.config.allowUnfree = true;
# linux_rpi4 is pre-built in cache.nixos.org — fetched, not compiled.
boot.kernelPackages = pkgs.linuxKernel.packages.linux_rpi4;
networking.wireless = {
enable = true;
# networks."Zakobar".psk = "your-wifi-password";
};
networking.interfaces.wlan0.ipv4.addresses = [{
address = "192.168.1.100";
prefixLength = 24;
}];
networking.useDHCP = false;
networking.interfaces.wlan0.useDHCP = false;
networking.defaultGateway = "192.168.1.1";
networking.nameservers = [ "1.1.1.1" "8.8.8.8" ];
networking.firewall.allowedTCPPorts = [ 22 ];
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
};
};
users.mutableUsers = false;
users.users.admin = {
isNormalUser = true;
extraGroups = [ "wheel" ];
openssh.authorizedKeys.keys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDfzDDO5juINctECmWlsYtGghEiX/RnTJ1cazLvOWSrPfsTyEd+B1+Ig8kFefNryjkpApfRXqj5KtLPNlpLfdVBrOIfhIveEp2MGqhgOGZFNVxQyXnZgii8Zdh4cqZ2O3pZpMsaAQBaJ9nH6dK0dJjicWT5f6TqwrVcInywRc5SuyizoSxoFmg7ch2rnlVi0j5XMVqdh8XLzHXZ7yWCzXy7+hWl/d7pwpyuzoK8dBw2EU9TauhgRDruom5Q9vWJTLStALC9pAIb0v9UFj9y+1zwx7pXsXp5F1g73EYrE4QR+QQ6z2LebuK280W0t+VA/fSCEB13DnkmofgqZQxX5MSCmrxZ5lTFp1FjW6yJo7As9FheF/GECowYkMRIx4IiQsjjHjZqlLRpLas11yAp6tGoZnw59hFo6Lu0Kva39jGVVmioYHtAeE5rD5w+v5kseJR4jlQ8aKB5yOjYUQOIz2AHQyoidgaeR2jPWqZUeRQbACI+/p3CHO45r3hrjATtGloBg0xF95Qws7Be3mjHVhbBLOoob8MdZ8nYAGnhlWrZphlkvXsHC6OUkuDJW00tmMjWXRlFwhFJ+nqUQCgLVjxVHQJ5rq9GeXBUuNXAeCm5BKBsdq+9qqVlt7D9iGyfr0lcZ7peKz/96KwPCWpG2En1Ur0/cVcbWnXEfG/xWO10tQ== cardno:24_758_470"
];
};
security.sudo.wheelNeedsPassword = false;
environment.systemPackages = [ pkgs.age pkgs.vim ];
}
+115
View File
@@ -0,0 +1,115 @@
{ 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
];
# linux_rpi4 is the Raspberry Pi Foundation's kernel, sourced from nixpkgs
# and pre-built in cache.nixos.org. Avoids a multi-hour native compilation.
boot.kernelPackages = pkgs.linuxKernel.packages.linux_rpi4;
# -------------------------------------------------------------------------
# Identity
# -------------------------------------------------------------------------
networking.hostName = "pi-main";
# -------------------------------------------------------------------------
# WiFi — static IP, always connect to home network
# -------------------------------------------------------------------------
networking.wireless = {
enable = true;
# secretsFile is read by wpa_supplicant at runtime; values are literal
# (not env vars). The key name after "ext:" must match a line in the file
# formatted as: key_name=the-actual-password
secretsFile = config.sops.secrets."wifi/psk".path;
networks."Zakobar".pskRaw = "ext:wifi_psk";
};
# Static IP on wlan0
networking.interfaces.wlan0.ipv4.addresses = [{
address = "192.168.1.100";
prefixLength = 24;
}];
networking.defaultGateway = "192.168.1.1";
networking.nameservers = [ "1.1.1.1" "8.8.8.8" ];
# Disable DHCP on wlan0 — we're using a static address
networking.useDHCP = false;
networking.interfaces.wlan0.useDHCP = false;
# The secret file must contain exactly one line: wifi_psk=<your-password>
# Add it with: sops secrets/secrets.yaml → wifi/psk: "wifi_psk=YourPassword"
sops.secrets."wifi/psk" = { owner = "root"; mode = "0400"; };
# -------------------------------------------------------------------------
# Admin user
# -------------------------------------------------------------------------
users.users.admin = {
isNormalUser = true;
extraGroups = [ "wheel" "podman" ];
# Paste your SSH public key here
openssh.authorizedKeys.keys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDfzDDO5juINctECmWlsYtGghEiX/RnTJ1cazLvOWSrPfsTyEd+B1+Ig8kFefNryjkpApfRXqj5KtLPNlpLfdVBrOIfhIveEp2MGqhgOGZFNVxQyXnZgii8Zdh4cqZ2O3pZpMsaAQBaJ9nH6dK0dJjicWT5f6TqwrVcInywRc5SuyizoSxoFmg7ch2rnlVi0j5XMVqdh8XLzHXZ7yWCzXy7+hWl/d7pwpyuzoK8dBw2EU9TauhgRDruom5Q9vWJTLStALC9pAIb0v9UFj9y+1zwx7pXsXp5F1g73EYrE4QR+QQ6z2LebuK280W0t+VA/fSCEB13DnkmofgqZQxX5MSCmrxZ5lTFp1FjW6yJo7As9FheF/GECowYkMRIx4IiQsjjHjZqlLRpLas11yAp6tGoZnw59hFo6Lu0Kva39jGVVmioYHtAeE5rD5w+v5kseJR4jlQ8aKB5yOjYUQOIz2AHQyoidgaeR2jPWqZUeRQbACI+/p3CHO45r3hrjATtGloBg0xF95Qws7Be3mjHVhbBLOoob8MdZ8nYAGnhlWrZphlkvXsHC6OUkuDJW00tmMjWXRlFwhFJ+nqUQCgLVjxVHQJ5rq9GeXBUuNXAeCm5BKBsdq+9qqVlt7D9iGyfr0lcZ7peKz/96KwPCWpG2En1Ur0/cVcbWnXEfG/xWO10tQ== cardno:24_758_470"
];
};
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-label/homey-data";
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 = "s3:https://s3.us-east-005.backblazeb2.com/zakobar-home-backup";
# -------------------------------------------------------------------------
# Local DNS overrides (optional — makes LAN clients hit the Pi directly
# instead of going through Cloudflare for *.zakobar.com)
# -------------------------------------------------------------------------
# If you run Pi-hole or Adguard, add these records there instead.
# networking.extraHosts = ''
# 192.168.1.100 zakobar.com
# 192.168.1.100 auth.zakobar.com
# 192.168.1.100 git.zakobar.com
# 192.168.1.100 nextcloud.zakobar.com
# 192.168.1.100 ldapadmin.zakobar.com
# '';
}
+50
View File
@@ -0,0 +1,50 @@
{ config, lib, pkgs, modulesPath, ... }:
# Hardware configuration for the primary Raspberry Pi 4 (8 GB).
#
# nixos-raspberrypi's raspberry-pi-4.base module (imported in flake.nix)
# provides everything that nixos-hardware.raspberry-pi-4 previously did:
# - linuxPackages_rpi4 vendor kernel + matching firmware
# - u-boot bootloader with /boot/firmware partition management
# - initrd modules (xhci_pci, usbhid, usb_storage, vc4, pcie_brcmstb, etc.)
# - config.txt generation
#
# This file adds only host-specific overrides on top of that.
#
# External HD:
# Set homey.storage.device to the by-id path of your USB drive.
# Find it with: ls -la /dev/disk/by-id/
#
# TODO: Verify SD card partition labels after first flash.
# The config assumes labels NIXOS_SD (root) and FIRMWARE (boot).
# Check with: lsblk -o NAME,LABEL
# Update fileSystems entries below if they differ.
{
# tmpfs for /tmp — keep the SD card writes down
boot.tmp.useTmpfs = true;
# Filesystems
fileSystems."/" = {
device = "/dev/disk/by-label/NIXOS_SD";
fsType = "ext4";
options = [ "noatime" ];
};
fileSystems."/boot/firmware" = {
device = "/dev/disk/by-label/FIRMWARE";
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 = [];
# Platform
nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux";
# Power management
powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand";
}
+191
View File
@@ -0,0 +1,191 @@
{ 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.
#
# Backup strategy — two tiers:
#
# 1. Automatic daily backup to an S3-compatible bucket (primary offsite copy).
# Set the repository URL to your bucket in hosts/pi-main/default.nix, e.g.:
# homey.backup.repository = "s3:https://s3.us-west-002.backblazeb2.com/your-bucket";
# S3 credentials are injected via environment variables from sops secrets:
# restic/s3_access_key_id → AWS_ACCESS_KEY_ID
# restic/s3_secret_access_key → AWS_SECRET_ACCESS_KEY
#
# 2. Manual offload to a local disk (USB drive plugged into Pi, or workstation disk).
# Use scripts/offload-backup.sh --target /path/to/mounted/disk
# That script uses `restic copy` to clone snapshots from the S3 repo into a
# local restic repo on the target disk, preserving deduplication.
#
# Secrets consumed from sops:
# restic/password
# restic/s3_access_key_id (if using S3 backend)
# restic/s3_secret_access_key (if using S3 backend)
#
# The backup repository URL is set per-host in default.nix:
# homey.backup.repository = "s3:https://s3.us-west-002.backblazeb2.com/bucket";
#
# 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"; };
sops.secrets."restic/s3_access_key_id" = { owner = "root"; };
sops.secrets."restic/s3_secret_access_key" = { 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;
# Runtime env file written by ExecStartPre (see systemd override below)
environmentFile = "/run/restic-homey-secrets.env";
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 and inject secrets
systemd.services."restic-backups-homey" = {
requires = [ "homey-backup-pre.service" ];
after = [ "homey-backup-pre.service" ];
serviceConfig = {
# Write runtime env file with actual secret values (restic needs the
# raw values; it does not support _FILE suffix env vars).
ExecStartPre = [
(pkgs.writeShellScript "restic-inject-secrets" ''
install -m 0600 /dev/null /run/restic-homey-secrets.env
{
printf 'AWS_ACCESS_KEY_ID=%s\n' \
"$(cat ${config.sops.secrets."restic/s3_access_key_id".path})"
printf 'AWS_SECRET_ACCESS_KEY=%s\n' \
"$(cat ${config.sops.secrets."restic/s3_secret_access_key".path})"
printf 'RESTIC_CACHE_DIR=%s\n' "${dataDir}/restic-cache"
} >> /run/restic-homey-secrets.env
'')
];
ExecStopPost = [
(pkgs.writeShellScript "restic-cleanup-secrets" ''
rm -f /run/restic-homey-secrets.env
'')
];
};
};
systemd.services."homey-backup-post" = {
after = [ "restic-backups-homey.service" ];
wantedBy = [ "restic-backups-homey.service" ];
};
};
}
+246
View File
@@ -0,0 +1,246 @@
{ config, lib, pkgs, homeyConfig, ... }:
# Caddy reverse proxy.
#
# Features:
# - DNS-01 ACME via Cloudflare API → real wildcard cert for *.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 using the nixos-25.05 API.
# `withPlugins` is a passthru function on the caddy package; it uses xcaddy
# under the hood to produce a fixed-output derivation.
caddyWithCloudflare = pkgs.caddy.withPlugins {
plugins = [
# v0.2.4 tag points to commit a8737d0 which includes the fix for
# cfut_/cfat_ token format validation (PR #123).
"github.com/caddy-dns/cloudflare@v0.2.4"
];
hash = "sha256-pRrLBlYRaAyMYwPXeTy4WqWNRu/L9K6Mn2src11dGh8=";
};
# Reverse-proxy snippet for cloudflared http:// vhosts.
# Cloudflare terminates TLS; cloudflared connects to Caddy over plain HTTP.
# We must override X-Forwarded-Proto so upstream services (especially
# Authelia) know the client is actually on HTTPS.
cfProxy = port: ''
reverse_proxy localhost:${toString port} {
header_up X-Forwarded-Proto https
}
'';
# 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
# Always tell Authelia the scheme is https (cloudflared terminates TLS
# externally; Caddy's http:// vhosts are only for the tunnel loopback).
header_up X-Forwarded-Proto https
# 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.
#
# Each service gets two vhost entries:
# - "host" (no scheme) → Caddy handles HTTPS + auto cert (for LAN access)
# - "http://host" → plain HTTP for cloudflared on loopback (no redirect)
#
# Caddy auto-redirects HTTP→HTTPS only when no explicit http:// vhost exists.
# By defining http:// explicitly we suppress that redirect so cloudflared
# (which talks plain HTTP on port 80) gets a direct response.
virtualHosts = {
# ------------------------------------------------------------------
# Authelia — public, no auth gate (it IS the auth gate)
# ------------------------------------------------------------------
"auth.${domain}" = {
extraConfig = ''
reverse_proxy localhost:9091
'';
};
"http://auth.${domain}" = {
extraConfig = cfProxy 9091;
};
# ------------------------------------------------------------------
# Gitea — no forward_auth; git HTTP clients can't handle SSO redirects.
# Access control is handled by Gitea itself (LDAP auth + private repos).
# ------------------------------------------------------------------
"git.${domain}" = {
extraConfig = ''
reverse_proxy localhost:3000
'';
};
"http://git.${domain}" = {
extraConfig = cfProxy 3000;
};
# ------------------------------------------------------------------
# Nextcloud — public auth (Nextcloud manages its own users + LDAP)
# ------------------------------------------------------------------
"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
'';
};
"http://nextcloud.${domain}" = {
extraConfig = ''
redir /.well-known/carddav /remote.php/dav/ 301
redir /.well-known/caldav /remote.php/dav/ 301
request_body {
max_size 5GB
}
reverse_proxy localhost:8080 {
header_up X-Forwarded-Proto https
}
'';
};
# ------------------------------------------------------------------
# phpLDAPadmin — two_factor, admins only (enforced by authelia policy)
# ------------------------------------------------------------------
"ldapadmin.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
reverse_proxy localhost:8081
'';
};
"http://ldapadmin.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
${cfProxy 8081}
'';
};
# ------------------------------------------------------------------
# Jellyfin — no forward_auth; Jellyfin has its own login UI and
# native app clients can't handle SSO redirects.
# ------------------------------------------------------------------
"jellyfin.${domain}" = {
extraConfig = ''
reverse_proxy localhost:8096
'';
};
"http://jellyfin.${domain}" = {
extraConfig = cfProxy 8096;
};
# ------------------------------------------------------------------
# Transmission — two_factor, admins only (enforced by authelia policy)
# ------------------------------------------------------------------
"torrent.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
reverse_proxy localhost:9092
'';
# NOTE: transmission is bound to 9092 to avoid clash with authelia on 9091.
};
"http://torrent.${domain}" = {
extraConfig = ''
${autheliaForwardAuth}
${cfProxy 9092}
'';
};
};
};
# -----------------------------------------------------------------------
# Pass Cloudflare token as env var to the caddy systemd unit.
#
# The caddy-dns/cloudflare plugin reads CLOUDFLARE_API_TOKEN directly.
# sops decrypts the secret to a file at runtime; we write a transient
# env file to /run/ in ExecStartPre so systemd picks it up via
# EnvironmentFile. The file is removed in ExecStopPost.
# -----------------------------------------------------------------------
systemd.services.caddy = {
serviceConfig = {
# LoadCredential stages the sops-decrypted secret into a
# per-invocation directory ($CREDENTIALS_DIRECTORY) before any
# Exec* step. ExecStart then reads the file contents and exports
# CLOUDFLARE_API_TOKEN before exec-ing caddy, so there is no
# intermediate env file and no ordering race with EnvironmentFile.
LoadCredential = "cloudflare_api_token:${config.sops.secrets."cloudflare/api_token".path}";
# Systemd requires clearing ExecStart= before setting a new value for
# non-oneshot services. The empty string resets the list; the second
# entry is the actual start command.
ExecStart = lib.mkForce [
""
(pkgs.writeShellScript "caddy-start" ''
export CLOUDFLARE_API_TOKEN=$(cat "$CREDENTIALS_DIRECTORY/cloudflare_api_token")
exec ${caddyWithCloudflare}/bin/caddy run --environ --config /etc/caddy/caddy_config --adapter caddyfile
'')
];
};
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 ];
};
}
+88
View File
@@ -0,0 +1,88 @@
{ 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.zakobar.com → http://localhost:80 (or https://localhost:443)
# git.zakobar.com → https://localhost:443
# nextcloud.zakobar.com → https://localhost:443
# ldapadmin.zakobar.com → https://localhost:443
# jellyfin.zakobar.com → https://localhost:443
# torrent.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
#
# We use the token-based tunnel approach (cloudflared tunnel run --token).
# This needs no credentials file and no local tunnel config — just the
# token from the Cloudflare dashboard.
#
# Rather than using services.cloudflared.tunnels (which requires a
# credentialsFile), we create a plain systemd service that runs cloudflared
# directly with the token read from the sops secret.
# -----------------------------------------------------------------------
users.users.cloudflared = {
isSystemUser = true;
group = "cloudflared";
description = "cloudflared tunnel daemon";
};
users.groups.cloudflared = {};
systemd.services."cloudflared-tunnel" = {
description = "Cloudflare Tunnel (token-based)";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" "caddy.service" ];
wants = [ "network-online.target" "caddy.service" ];
serviceConfig = {
Type = "simple";
User = "cloudflared";
Group = "cloudflared";
Restart = "on-failure";
RestartSec = "5s";
ExecStart = pkgs.writeShellScript "cloudflared-start" ''
exec ${pkgs.cloudflared}/bin/cloudflared tunnel \
--no-autoupdate \
run \
--token "$(cat ${config.sops.secrets."cloudflare/tunnel_token".path})"
'';
# Hardening
NoNewPrivileges = true;
PrivateTmp = true;
};
};
};
}
+124
View File
@@ -0,0 +1,124 @@
{ 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" ];
auto-optimise-store = true;
# Extra binary caches — speeds up aarch64-linux builds significantly
substituters = [
"https://cache.nixos.org"
"https://nix-community.cachix.org"
];
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"nix-community.cachix.org-1:mB9FkXj6Q3Q4ohOcbM4FJ9Z1X2kCrVK4vZOqsDqqNqk="
];
};
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 = "25.05";
}
+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: zakobar.com → 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;
# No ports mapping — --network=host shares the host network stack directly.
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" ];
};
};
}
+229
View File
@@ -0,0 +1,229 @@
{ 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.)
#
# Configuration strategy: all settings are passed as GITEA__<SECTION>__<KEY>
# environment variables. Gitea writes its own app.ini into /data/gitea/conf/
# on first start; the env vars override every key at runtime without touching
# that file. This avoids the bind-mount / read-only-fs problem where Gitea
# needs to rewrite its own config file on startup.
#
# Non-secret settings go in the `environment` block (they are fine in the
# Nix store). Secret settings go into /run/gitea-secrets.env via ExecStartPre
# (never in the store).
#
# 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;
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"; };
# -----------------------------------------------------------------------
# Container
# -----------------------------------------------------------------------
virtualisation.oci-containers.containers.gitea = {
image = cfg.image;
# No ports mapping — --network=host means the container shares the host
# network stack directly. Gitea binds to 0.0.0.0:3000 on the host.
# All non-secret settings via GITEA__<SECTION>__<KEY> env vars.
# These are safe to store in the Nix store.
environment = {
USER_UID = "1000";
USER_GID = "1000";
GITEA_CUSTOM = "/data/gitea";
# [DEFAULT]
GITEA____APP_NAME = homeyConfig.organization;
GITEA____RUN_MODE = "prod";
# [repository]
GITEA__repository__ROOT = "/data/git/repositories";
# [server]
GITEA__server__APP_DATA_PATH = "/data/gitea";
GITEA__server__DOMAIN = "git.${domain}";
GITEA__server__HTTP_PORT = toString cfg.port;
GITEA__server__ROOT_URL = "https://git.${domain}/";
GITEA__server__DISABLE_SSH = "true";
GITEA__server__START_SSH_SERVER = "false";
GITEA__server__SSH_PORT = "2222";
GITEA__server__SSH_LISTEN_PORT = "2222";
GITEA__server__LFS_START_SERVER = "true";
GITEA__server__OFFLINE_MODE = "false";
# [lfs]
GITEA__lfs__PATH = "/data/git/lfs";
# [database]
GITEA__database__DB_TYPE = "sqlite3";
GITEA__database__PATH = "/data/gitea/gitea.db";
GITEA__database__LOG_SQL = "false";
# [indexer]
GITEA__indexer__ISSUE_INDEXER_PATH = "/data/gitea/indexers/issues.bleve";
# [session]
GITEA__session__PROVIDER = "file";
GITEA__session__PROVIDER_CONFIG = "/data/gitea/sessions";
# [picture]
GITEA__picture__AVATAR_UPLOAD_PATH = "/data/gitea/avatars";
GITEA__picture__REPOSITORY_AVATAR_UPLOAD_PATH = "/data/gitea/repo-avatars";
GITEA__picture__DISABLE_GRAVATAR = "false";
# [attachment]
GITEA__attachment__PATH = "/data/gitea/attachments";
# [log]
GITEA__log__MODE = "console";
GITEA__log__LEVEL = "info";
GITEA__log__ROOT_PATH = "/data/gitea/log";
# [security]
GITEA__security__INSTALL_LOCK = "true";
GITEA__security__REVERSE_PROXY_LIMIT = "1";
GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES = "*";
# [service]
GITEA__service__DISABLE_REGISTRATION = "true";
GITEA__service__REQUIRE_SIGNIN_VIEW = "false";
GITEA__service__REGISTER_EMAIL_CONFIRM = "false";
GITEA__service__ENABLE_NOTIFY_MAIL = "false";
GITEA__service__ALLOW_ONLY_EXTERNAL_REGISTRATION = "true";
GITEA__service__ENABLE_CAPTCHA = "false";
GITEA__service__DEFAULT_ALLOW_CREATE_ORGANIZATION = "true";
GITEA__service__DEFAULT_ENABLE_TIMETRACKING = "true";
GITEA__service__NO_REPLY_ADDRESS = "noreply.localhost";
GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION = "true";
GITEA__service__ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = "true";
# [mailer]
GITEA__mailer__ENABLED = "false";
# [openid]
GITEA__openid__ENABLE_OPENID_SIGNIN = "false";
GITEA__openid__ENABLE_OPENID_SIGNUP = "false";
# [oauth2]
GITEA__oauth2__ENABLED = "false";
};
# Secret env vars written at runtime by ExecStartPre — never in store.
environmentFiles = [ "/run/gitea-secrets.env" ];
volumes = [
"${dataDir}/gitea/data:/data"
];
extraOptions = [ "--network=host" ];
};
# -----------------------------------------------------------------------
# ExecStartPre: write ephemeral secrets env file
# ExecStopPost: clean it up
# -----------------------------------------------------------------------
systemd.services."podman-gitea" = {
serviceConfig = {
ExecStartPre = [
(pkgs.writeShellScript "gitea-write-secrets" ''
set -euo pipefail
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})
printf '%s\n' \
"GITEA__server__LFS_JWT_SECRET=$LFS" \
"GITEA__security__INTERNAL_TOKEN=$TOKEN" \
"GITEA__oauth2__JWT_SECRET=$OAUTH" \
> /run/gitea-secrets.env
chmod 600 /run/gitea-secrets.env
'')
];
ExecStopPost = [
(pkgs.writeShellScript "gitea-cleanup-secrets" ''
rm -f /run/gitea-secrets.env
'')
];
};
after = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" ];
requires = lib.mkAfter [ "mnt-data.mount" ];
};
# -----------------------------------------------------------------------
# Ensure the Gitea admin user exists with the correct password after start.
# Runs as a oneshot after podman-gitea; idempotent (create or update).
# -----------------------------------------------------------------------
systemd.services."gitea-admin-setup" = {
description = "Ensure Gitea admin user exists with correct password";
wantedBy = [ "multi-user.target" ];
after = [ "podman-gitea.service" ];
requires = [ "podman-gitea.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
set -euo pipefail
PASS=$(cat ${config.sops.secrets."gitea/admin_password".path})
# Wait until Gitea's HTTP endpoint is up (max 60 s)
for i in $(seq 1 60); do
if ${pkgs.curl}/bin/curl -sf http://127.0.0.1:${toString cfg.port}/ -o /dev/null; then
break
fi
sleep 1
done
# Sync password if admin exists; create if not.
if ! ${pkgs.podman}/bin/podman exec -u 1000 gitea \
gitea admin user change-password --username admin --password "$PASS" 2>/dev/null; then
${pkgs.podman}/bin/podman exec -u 1000 gitea \
gitea admin user create \
--username admin \
--password "$PASS" \
--email "admin@${domain}" \
--admin
fi
'';
};
};
}
+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;
# No ports mapping — --network=host shares the host network stack directly.
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" ];
};
};
}
+150
View File
@@ -0,0 +1,150 @@
{ 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;
# No ports mapping — --network=host shares the host network stack directly.
environment = {
POSTGRES_DB = "nextcloud_db";
POSTGRES_USER = "postgres";
# Password injected via env file
};
volumes = [
"${dataDir}/nextcloud/db:/var/lib/postgresql/data"
];
extraOptions = [
"--network=host"
"--env-file=/run/nc-postgres-secrets.env"
];
};
systemd.services."podman-nextcloud-postgres" = {
serviceConfig = {
LoadCredential = [
"nextcloud_postgres_password:${config.sops.secrets."nextcloud/postgres_password".path}"
];
ExecStartPre = [
(pkgs.writeShellScript "nc-postgres-secrets-env" ''
set -euo pipefail
install -m 600 /dev/null /run/nc-postgres-secrets.env
echo "POSTGRES_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/nextcloud_postgres_password")" \
>> /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;
# No ports mapping — --network=host shares the host network stack directly.
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}";
# With --network=host, port mappings are ignored and the container's
# Apache binds directly on the host. Force it onto port 8080 so Caddy
# can own 80/443.
APACHE_HTTP_PORT_NUMBER = toString cfg.port;
# Passwords injected via env file
};
volumes = [
"${dataDir}/nextcloud/html:/var/www/html"
];
extraOptions = [
"--network=host"
"--env-file=/run/nc-secrets.env"
];
};
systemd.services."podman-nextcloud" = {
serviceConfig = {
LoadCredential = [
"nextcloud_postgres_password:${config.sops.secrets."nextcloud/postgres_password".path}"
"nextcloud_admin_password:${config.sops.secrets."nextcloud/admin_password".path}"
];
ExecStartPre = [
(pkgs.writeShellScript "nc-secrets-env" ''
set -euo pipefail
install -m 600 /dev/null /run/nc-secrets.env
echo "POSTGRES_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/nextcloud_postgres_password")" >> /run/nc-secrets.env
echo "NEXTCLOUD_ADMIN_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/nextcloud_admin_password")" >> /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" ];
};
};
}
+125
View File
@@ -0,0 +1,125 @@
{ 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;
# No ports mapping — --network=host means the container shares the host
# network stack. OpenLDAP binds to 0.0.0.0:389, but the firewall
# (common.nix) only opens 22/80/443, so port 389 is unreachable from
# the LAN or internet.
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"
"--env-file=/run/openldap-secrets.env"
];
};
# -----------------------------------------------------------------------
# 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 = {
# LoadCredential stages the sops secrets into a per-invocation
# credential directory before any Exec* step, so they are available
# when ExecStartPre runs. ExecStartPre writes the env file that
# podman --env-file reads; this avoids the EnvironmentFile ordering
# race (EnvironmentFile is evaluated before ExecStartPre).
LoadCredential = [
"openldap_admin_password:${config.sops.secrets."openldap/admin_password".path}"
"openldap_config_password:${config.sops.secrets."openldap/config_password".path}"
"openldap_ro_password:${config.sops.secrets."openldap/ro_password".path}"
];
ExecStartPre = [
(pkgs.writeShellScript "openldap-secrets-env" ''
set -euo pipefail
install -m 600 /dev/null /run/openldap-secrets.env
echo "LDAP_ADMIN_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/openldap_admin_password")" >> /run/openldap-secrets.env
echo "LDAP_CONFIG_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/openldap_config_password")" >> /run/openldap-secrets.env
echo "LDAP_READONLY_USER_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/openldap_ro_password")" >> /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
# -----------------------------------------------------------------------
# No firewall rule needed; common.nix only opens 22/80/443.
};
}
+54
View File
@@ -0,0 +1,54 @@
{ 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.
#
# Networking: uses default bridge (podman) network with a port mapping
# 127.0.0.1:8081->80 so Caddy can reach it. OpenLDAP runs on the host
# network at 127.0.0.1:389; the container reaches it via the special
# host.containers.internal DNS name that podman injects automatically.
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;
environment = {
PHPLDAPADMIN_HTTPS = "false";
# host.containers.internal resolves to the host from inside a podman
# bridge container — reaches openldap which is on --network=host at :389
PHPLDAPADMIN_LDAP_HOSTS = "host.containers.internal";
};
# Bridge network (default) + port mapping: Apache binds inside the
# container on :80, podman maps it to 127.0.0.1:8081 on the host.
ports = [ "127.0.0.1:${toString cfg.port}:80" ];
};
systemd.services."podman-phpldapadmin" = {
after = lib.mkAfter [ "podman-openldap.service" ];
wants = lib.mkAfter [ "podman-openldap.service" ];
};
};
}
+66
View File
@@ -0,0 +1,66 @@
{ 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;
# No ports mapping — --network=host shares the host network stack directly.
environment = {
PUID = "1000";
PGID = "1000";
# With --network=host, port mappings are ignored; transmission binds
# directly on the host. Force it to cfg.port (9092) to avoid
# conflicting with Authelia on 9091.
TRANSMISSION_WEB_HOME = "/usr/share/transmission/web";
WEBUI_PORT = toString cfg.port;
};
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 1000 1000 -"
"d ${cfg.mountPoint}/gitea/data 0750 1000 1000 -"
"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
+61
View File
@@ -0,0 +1,61 @@
openldap:
admin_password: ENC[AES256_GCM,data:hg+Ly1bX4ao1AT4SDvQWXiT/KMzsz0wdnRauiB+FetE=,iv:TAX+NZCVUNiwMeBrW58IeI1OJX6rzzGAhWiQ+cZXreo=,tag:MrwYKKBb1Cg2JvADtQqYrQ==,type:str]
config_password: ENC[AES256_GCM,data:qKEurb0slGnr6nES7w7fTPDCy/DARns0BorDZMwpI/w=,iv:+p6Fh9a2g0eBueOxDk1J+hnM9fMgE6/NYwz+sAovGjE=,tag:kKZVsxdxdDACD9J0NAf4gQ==,type:str]
ro_password: ENC[AES256_GCM,data:82htWXdJ07tdZ81o7o9a4hizxcn39yQidD5e9PijVpo=,iv:fDT1chX4ZPIS02IMEW02haPa2IIlLFhgFOpUwD7KL50=,tag:9pSo/3vFikKQAe8jS+3Q6A==,type:str]
authelia:
jwt_secret: ENC[AES256_GCM,data:SwXd/mMsrgXItP8QZr4z9YaN1lgSSO4Cpdwl+XxFj6I=,iv:gAkyHKP5D5RGJ3X3hoh8oEJfYaFnvYxGAoKxe+G1N0I=,tag:NlfWC2pmjCYiiRn9prgl2w==,type:str]
session_secret: ENC[AES256_GCM,data:bbbgmxYLbtteuT628O+uSVeo7Gx3hI6uWVIV5l8AhtNSvXBHKb9i2NvQqJfVG8D2YR00+OW9XtreocpBuS7YJKp58cP/EgrF9x1im/8UaaifAYqD8YXZReUsBw+/PzIvqyA1K9tFd0coc8tSHJmwTwea4sf6Tc10N6j/nhNQfpQ=,iv:wrGE8XVsTINmf5505XVI7HHqA33w+SBh9lsJXD+YnwU=,tag:lvSoU+IRy8f+6VpYqzvacg==,type:str]
storage_encryption_key: ENC[AES256_GCM,data:8KnaWBTlSStdC/uI4GUOYP9DJygjfCTu62zWTU9eeVE=,iv:DNl0L2QgT1lNUDdPNm9bXcGvrLXLDtWdJ9pPgRH20C8=,tag:hVhcCl0Vrt0ZnaVGaUokSQ==,type:str]
gitea:
admin_password: ENC[AES256_GCM,data:WncwJlqb/3X5WZYgIXAu0niI0ISP0eHfmhsKDLeAvE4=,iv:JBZNZJRSHKG+cCoFNJBI+jS+/WcLueqJ/UN/7wXfK/0=,tag:f6+Lhfcxstq6pwS9xkVwqQ==,type:str]
lfs_jwt_secret: ENC[AES256_GCM,data:i05gr2ou03w0yu6/bhlJOW1huysAAPTidFEusWkhQfpDj4Pyh8LEKb09Og==,iv:aqkblyz0oIFHwzVCzlGDdQuCbsDPrfBaJMzgRTw+pYU=,tag:6gBSerOUK8Y3la/2Bg2AZQ==,type:str]
oauth2_jwt_secret: ENC[AES256_GCM,data:BVvQJCEfHPbemd1jz7MWpIRia1wfvPMGuLqoi/xUMSoQoN5RPefQnPR4Cg==,iv:JAZQUTxHZSnMEnl+BIZ1PXlznMwKuPtiPP/17rc6lSs=,tag:mUw5RuthZmZegXCtfsFNmQ==,type:str]
internal_token: ENC[AES256_GCM,data:gnOebJbRsh2Cues9WjGQp4rWa6OuE3xSnby9jc3Hk8ywvpL7CNWmlGW7zmmOyDAfIKfm8kf1FxotWLXtGZDretzdbMRM9c6gkwSJf5MCsdm27Er+IRKS/QFBuvLSTEH0,iv:aVgRvs3T3zCg+AV/BKUXQyZDKvunHvXsdfr9sqo2cI0=,tag:U9aP+N0CWyRQ/xJ27Vo2mw==,type:str]
nextcloud:
admin_password: ENC[AES256_GCM,data:iK6VoE94vFQmn3i4XQc5r/c03u3b0knDgBNK8d1qyns=,iv:P1wax2vAjn9iwBe9T7SN+pKrtrWcOYb5OWUyHF4hlVg=,tag:ET8KU4IKzhWqIDeRihwcag==,type:str]
postgres_password: ENC[AES256_GCM,data:ga4cwhYsAgEBvr+aDVwiRZXeT+TjXzeef1r3ud6uYHs=,iv:PMHCjO4wLW6PER4oGODEG9CHqrvVpAbgTGF7p49MCL0=,tag:mTNzsDhufqLlf1LFu7Rl1A==,type:str]
cloudflare:
api_token: ENC[AES256_GCM,data:Erzom4DKiam9SHGLdT3CQRkuT5kkhcuUaLwTbt2P5pPjr1V56p733KB1kHheO/PZ+TRsZg0=,iv:eO+ryffyoSkzAgUXe0MH+FKitgHCQ3ychLWEAShAd9o=,tag:2Z8Io0ylpAI9rws5NXCvIw==,type:str]
tunnel_token: ENC[AES256_GCM,data:AFlD990L8l1Rh9i8wdyXwwyolrlw2ln1uuyCTiT7k1FVc2JjTOrgc8HiBIpxM40eqFGEnzDMs87tgzh1Pl8UThwV7WcLFWHMvtYHNez/F2+THknnW+ZinJbZnNSngicrhRIoNFhjQgjaR1LaS4kcNbkGBi655bl2uqNXoUpNThUKGAlZ4KwByEzK4B7QtCgAkqxQEFehtjdj41p8r58ViJTogXQKXmDYLjbqo8nPmUGSaiR/VCY7Gw==,iv:q755yc6wTMCuqHLvfHOZzBf3KoG4vcw431stQA78Bjs=,tag:Zt/LPG41HxSg2gSQOTC/6A==,type:str]
restic:
password: ENC[AES256_GCM,data:X/pWmwakzQzRpSaY+T/kOqdrtXyvGPa3UQc/iIQFFAzUS1jHR9IvzW7KYdm/3IKXPBlZzPkWDsoRvVAChPwfQA==,iv:4+RD9UD5daMP04ixeagxbCNkTdOPx+BqfSOheh88OUU=,tag:SmrIz7jvbM74y5RBX0nCbA==,type:str]
s3_access_key_id: ENC[AES256_GCM,data:XxElPQF28ThfYuiF4jQu7BiS8sh+c4V4ng==,iv:aLUIYnRGqFLYwlP3nFwDY5uvy8pXtX5QMKLMfRTxdNk=,tag:CscNNWnpvLuxw1DPK92GyA==,type:str]
s3_secret_access_key: ENC[AES256_GCM,data:9ZWyhGJm4t2benDrLmnyQ9ZA5Jjl6l+pza1VmymTlw==,iv:xYsG6QlxXhQNO9szmsycxP6lT0cFF7lq3iNg6j+ED0E=,tag:wOJT4Vg3DuNFWTtx3QS9IQ==,type:str]
wifi:
psk: ENC[AES256_GCM,data:znk9Wr+vsntzbJ3H0TORUrAiDw==,iv:wbl8fUuKlgTqhajwjlTgFS7ijaTwXBFPRW2AmtiTklg=,tag:IK4oe8cJcccPaQ0V0NlncQ==,type:str]
sops:
age:
- recipient: age120j8ty7nn04l3s3kgph5ty3v9g4e52fknn8xtnmzwakq9nv2la3skgte0p
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZSGpPdTBIaTZ0TER2NkNO
U2ZPKzNwelJHUEpyU2VBSmd5Yjd5bEtibFZzCjlZZTRFa2FHN1JtK2JUSm51a3By
QmFyV1ZZNWI0OGJVM1NNZERjd2hWcDAKLS0tIG9VSVFTSTJBMjk5ZzBSL0ZQV2Ev
QXVkRlJHeW52NFZFYnVwaW8ycytDSzAKZt+p5QnZKcEOBghHA2xkH6d7NObtTEoE
wMwCYasnBHzy2unXRbZq/4v9NQ5HJd0Nu1iqbqKgIxMCD3dnxEdK7g==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-04-21T12:42:15Z"
mac: ENC[AES256_GCM,data:fNip/7A7iKCVZqP0EziyBG7K8SVfRJTBpn4RcDLOaciJHx5DkLLszE8we9MmzpKXQIiMcJl2BTj/uqJrgc5EHTSOHwRzNJ4s2NJfvQW+8QUDfTGzKOkP3L837RkEPzH4HZLqGlfYK7cNJU5qXRPbusKjAft7Fz3+ONXmodb/ONY=,iv:CdSs1a+74+MfzWyML2JQ/b2IKbktVdefFFYP5LOtUos=,tag:ikr9LwPnmdiPucOoBt3/Bw==,type:str]
pgp:
- created_at: "2026-04-21T06:39:49Z"
enc: |-
-----BEGIN PGP MESSAGE-----
hQIMAwdqopdXmgBkAQ//TzlOz/QYwiYAc6NGo2O8YJi5ERkS1+0qNpptD51g2dLF
V4iUx7400tc6IEEhZ0N54R7AO5mSX55XCWJxVQDTRJXmLDHcOR+9vThb4H571XBa
3mcmE8Dj3sN3a1K2RwajZJXl1o5d1oNvWJ83pVsCnrJegi92+GmvmOt4QZ1l5aCf
TGYgUXAz1RreqsGKjJsSXscZOvRnp+cslJ9xY8OXeKLbQvLg0Z3pSQG2QGgDmHPD
fRxYnlc2lKe32uoBlD2LXK+NoBnrRYEVrrwGf6P5GpTDpJbc0bR5BiRIYDhPxtqK
SiXWHaebg73+kbWdcm+2kiac6hW6xW/iJL4eFBT1v/NgZmNoQCnJOIA7v2vjv9vl
81Y1FM5MpIfwNiTwkJjVsgM2tHkANlbixBHJdbjlnKpo9pTS7RuttWtdCmFdmXr0
oiuKDDRPVGvykPqvHzvCLf/k5j1nYvqvb7Wn2Bycc5kIOjFYEDEeM0r37vOX9nDM
SW1HtaWoZuVceTJEit0WR63kmXYLZ/AHvXcmq6ucUw8Fmw79n+7brQiX2RMtCK1E
pfrNey3EEqvPs2RDd6XdF4/73CdMDN5s3xiFAIfLGeZ6h0Eq27fazSZNmdh4MGYb
Wzj81ur8dimoSP+W9eW1TjIfY4deH5FRnN19ldKPuHdazvikWWsdN05evNlSZsDS
XgHafkhKiNSNZLw/VVzf+1SDLhN1H5QoxZ2YsxCc+psd5CFxU1x3llIDg4hXScAR
OQvRR1VjQOLFCwdFErW7sd6nQlkS7LnAskgT/0ZJGsxfkh1gJO3YqDnEKF7+P9w=
=zKa+
-----END PGP MESSAGE-----
fp: 076AA297579A0064
unencrypted_suffix: _unencrypted
version: 3.12.2
-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