Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Latest commit

 

History

2,489 Commits

Folders and files

Repository files navigation

Self-hosted services

Built for homelab operators who want a complete, opinionated platform they can understand and control.

This repository uses Ansible to deploy Kubernetes and a private suite of services for media, photos, files, home automation, monitoring, backups, and local AI. Applications share PostgreSQL, Redis, MinIO, Authentik, SMTP, observability, and VPN-routed egress where they support them.

What you get

  • Media and requests: Jellyfin, Jellyseerr, the Arr stack, and Transmission.
  • Photos, files, and archives: Immich, Nextcloud, MinIO, Filebrowser, and ArchiveBox.
  • Home and automation: Home Assistant, n8n, Mosquitto, and 3D-printing tools.
  • Platform operations: Authentik SSO, Argo CD, PostgreSQL, Redis, Velero, Grafana, Loki, and Tempo.
  • Local AI: llama.cpp and Open WebUI.

See the application catalog for the complete list and service-specific setup steps.

Prerequisites

  • Ubuntu 26.04 LTS hosts
  • A domain managed by Cloudflare
  • Ansible access to every host

Quick start

  1. Configure Cloudflare DNS.
  2. Optionally configure SES SMTP, ZFS, and Tailscale.
  3. Create an inventory from the example below.
  4. Complete the required entries in the application catalog.
  5. Deploy and open https://dash.<your-domain>.

Configure Cloudflare DNS

Cloudflare provides public DNS for the services. Its proxy can also shield eligible HTTP services from direct traffic.

  1. Buy a domain name and add it to Cloudflare.

  2. Set SSL/TLS encryption mode to Full (strict).

  3. Add an A record for @ that points to your public IP.

  4. Add a wildcard CNAME record (*) that points to @.

  5. Optionally enable Cloudflare's proxy for records that carry HTTP traffic.

  6. Create an API token under User Profile > API Tokens with:

    • Permissions:
      • Zone - DNS - Edit
      • Zone - Zone - Read
    • Zone resources:
      • Include - All Zones

    Store the token in the Cloudflare Kubernetes secret.

Configure SES SMTP (optional, recommended)

AWS SES provides outbound email for applications in this stack.

  1. Create an AWS account and verify your domain in AWS SES Console > Verified identities.
  2. Request production access from AWS SES Console > Account dashboard to leave sandbox mode.
  3. Create SMTP credentials under AWS SES Console > SMTP settings, then store them in the SMTP Kubernetes secret.
  4. Configure applications with the endpoint email-smtp.<region>.amazonaws.com and port 587 (STARTTLS) or 465 (TLS).

Configure ZFS (optional, recommended)

ZFS adds checksumming, compression, and reliable storage management. Put the zfs object directly under each storage-capable host in your Ansible inventory, as shown in Create an inventory. Not every Kubernetes node necessarily has disks intended for a ZFS pool. Use whole-disk, stable /dev/disk/by-id, /dev/disk/by-partuuid, or /dev/disk/by-uuid paths; the role rejects /dev/sdX, /dev/nvme*, and every other unstable device path.

The role creates cluster-local-storage (128K recordsize) and multimedia (1M recordsize) beneath the encrypted dataset. Any additional dataset must declare a recordsize.

Existing pools are never recreated or destroyed. Existing vdevs must exactly match the declared disk set and topology; otherwise the run fails before it can change the pool. Declared vdevs that are absent are added. A disk already owned by another pool is rejected. Existing encrypted datasets must retain their configured key location; an existing key at that location is reused. Back up each host's keyfile before relying on encrypted storage.

Set backup_keys_to_controller: true under inventory all.vars to back up generated or reused keys to the Ansible controller. They are stored in the gitignored keys_backup/<inventory> directory.

Configure Tailscale (optional)

Tailscale provides remote access to hosts without exposing static ports.

  1. Create an account at Tailscale.

  2. Add this ACL fragment in the ACL editor:

     "tagOwners": {
       "tag:ansible": ["autogroup:admin", "autogroup:owner"],
     },
     "autoApprovers": {
     	  "routes": {
         "192.168.0.0/16": ["tag:ansible"]
       },
       },
  3. Create an OAuth client. Enable Write for Device/Core and Keys/Auth Keys, and add the tag:ansible tag to each permission.

Intranet access through Tailscale

Use NextDNS to resolve your domain to the internal HAProxy virtual IP while remote devices are on Tailscale.

  1. In NextDNS, add a rewrite under Settings > Rewrites for *.example.org that returns the HAProxy virtual IP, for example 192.168.1.2.
  2. In the Tailscale admin console under DNS, add the IPv6 resolver address from NextDNS as a global nameserver and enable Override local DNS.

Create an inventory

Create an inventory file such as inventory.yml and adapt this example to your hosts:

all:
  vars:
    k3s_control_node: false
    skip_system_setup: false
    skip_zfs_setup: false
    skip_firewall_setup: false
    skip_vpn_setup: false
    skip_k8s_setup: false
    skip_binary_update: false
    manifest_only_setup: false
    backup_keys_to_controller: false
    timezone: America/Vancouver
    tenants_count: 1
    tailscale_oauth_secret: "some_secret"
    chartValuesOverrides:
      fqdn: "example.com"
      storageLocation: /storage
      # Optional: disable high availability (reduces service replicas from 3 to 1)
      highAvailability: false
      # Optional: disable unwanted applications
      applications:
        crowdsec:
          enabled: false
# There must be a minimum of 3 controllers and the number must be odd for etcd to work
k3s_cluster:
  hosts:
    raspi:
      k3s_control_node: true
      labels:
        - local=true
    big_manager:
      k3s_control_node: true
      labels:
        - local=true
        - nas=true
      zfs:
        pools:
          - name: storage
            vdevs:
              - type: mirror
                disks:
                  - /dev/disk/by-id/wwn-0x5000...
                  - /dev/disk/by-id/wwn-0x5000...
            # Optional; defaults to /root/pool_name-keyfile-zfs
            encryption_key_location: /root/storage-keyfile-zfs
            # Optional; defaults to /storage
            mountpoint: /storage
            datasets:
              - name: backups
                recordsize: 1M
                # Optional; snapshots are enabled by default.
                snapshot: true
    small_manager:
      k3s_control_node: true
      labels:
        - local=true
        - public=true
    big_server:
      labels:
        - local=true
        - runner=true
      # Optional: advertise this host as a Tailscale exit node.
      # tailscale_exit_node: true
# Optional
headscale:
  hosts:
    headscale_control_server: {}
# Optional
routers:
  hosts:
    gateway:
      # wan_interface: "eth1" # Optional, will be auto-detected if not set
      # lan_ip: "192.168.1.1" # Optional, will be auto-detected from br_lan interface if not set
      haproxy:
        # virtual_ip: 192.168.1.2 # Optional, will be deduced from lan_ip if not set
        servers:
          s1: 192.168.1.11
          s2: 192.168.1.12
          s3: 192.168.1.13
      # (optional, recommended) QoS config
      qos:
        download_kbps: 95000    # 95% of your download speed
        upload_kbps: 19000      # 95% of your upload speed

Use an odd number of controller nodes. Add public to nodes that receive external traffic, nas to nodes that store large files, and local to nodes at the site. Nodes dedicated to AI workloads need the dedicated=ai label and dedicated=ai:NoSchedule taint.

Load balancing (optional)

On an OpenWrt-capable router, such as the GL-MT6000, HAProxy can distribute inbound traffic across the cluster. Add the router to the routers inventory group, then run:

ansible-playbook setup_router.yml -i inventory.yml

All inbound HTTP(S) traffic then passes through HAProxy on gateway ports 9080 and 9443. To skip this setup, set chartValuesOverrides.behindTcpProxy to false.

QoS (optional)

Smart Queue Management (SQM) reduces bufferbloat during heavy uploads and downloads. Measure your connection speed, then add 95% of those values to the router configuration:

routers:
  hosts:
    gateway:
      qos:
        download_kbps: 95000    # 95% of your download speed in kbps
        upload_kbps: 19000      # 95% of your upload speed in kbps

Run the router setup playbook to apply the configuration.

Router setup validates the existing firewall before changing settings and keeps a private temporary backup on the router. If a configuration task or firewall validation fails, it restores /etc/config/firewall for the next boot and stops without a recovery restart. Other settings are not rolled back. Loss of SSH or power can prevent recovery; this is not a substitute for console access or a firmware backup. Pending UCI firewall edits must be committed or reverted first.

On Qualcomm NSS firmware (including the Flint 3), leave qos_force_flow_offloading unset or false. Router setup rejects enabling kernel flow offloading on detected NSS hardware. The MediaTek GRO workaround is restricted to the mtk_soc_eth driver; VPN UDP offloading requires kernel 6.2 or newer.

The local regression tests use temporary files and fake firewall commands: python -m unittest discover -s tests -p 'test_router_safety.py' -v (run with Python from the Ansible environment).

Deploy

Complete the applicable manual application setup, then run:

ansible-playbook setup_cluster.yml -i inventory_static.yml -i inventory_ec2.yml

After deployment, disable Tailscale key expiry on every machine to avoid an unexpected loss of access. See Tailscale's instructions.

Open https://dash.<your-domain> to access the service dashboard.

Backup and recovery

The PostgreSQL and Velero recovery procedures, including point-in-time recovery, are documented in the application catalog.

Configure self-hosted Renovate

Woodpecker runs Renovate on its hourly schedule and when a person edits the body of the open Dependency Dashboard issue. The GitHub Actions job waits for 60 seconds without another body edit before it starts Woodpecker. A newer body edit cancels the waiting job and restarts the wait. The job skips edits from the Renovate app, so those edits do not start another Woodpecker run.

Create and install the GitHub App

  1. Create a GitHub App for this repository. It does not need a webhook URL or webhook events.
  2. Give it the permissions listed in the Renovate GitHub App guide.
  3. Install the app on this repository only.
  4. Copy the App ID from its settings. Get the installation ID from the installation settings page or GET /repos/{owner}/{repo}/installation.
  5. Create a private key for the app. Keep the PEM file private.
  6. Add these repository secrets in Woodpecker and scope them to ghcr.io/yyewolf/woodpecker-plugins/github-app-token plugin:
    • renovate_github_app_id: the App ID
    • renovate_github_app_installation_id: the installation ID
    • renovate_github_app_private_key: the PEM private key

Configure the GitHub issue trigger

  1. Add the WOODPECKER_URL and WOODPECKER_REPO_ID repository variables in GitHub.
  2. Add WOODPECKER_TOKEN as a GitHub repository secret. The token must be able to start pipelines in Woodpecker.
  3. Put .github/workflows/trigger-woodpecker.yml on the repository's default branch. GitHub requires this for issue-triggered workflows.
  4. Edit the open Dependency Dashboard issue. The workflow starts a Woodpecker pipeline after the issue body has stayed unchanged for 60 seconds.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages