README.md
Rama main · réplica de lectura del repositorio original.
# 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](APPLICATIONS.md) 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](#configure-cloudflare-dns).
2. Optionally configure [SES SMTP](#configure-ses-smtp), [ZFS](#configure-zfs), and [Tailscale](#configure-tailscale).
3. Create an inventory from the example below.
4. Complete the required entries in [the application catalog](APPLICATIONS.md#services-that-need-manual-setup).
5. [Deploy](#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](https://developers.cloudflare.com/fundamentals/manage-domains/add-site/).
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](charts/services/templates/cert-manager.yaml).
## 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](charts/services/templates/smtp-secret.yaml).
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](#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](https://login.tailscale.com).
2. Add this ACL fragment in the [ACL editor](https://login.tailscale.com/admin/acls/file):
```jsonc
"tagOwners": {
"tag:ansible": ["autogroup:admin", "autogroup:owner"],
},
"autoApprovers": {
"routes": {
"192.168.0.0/16": ["tag:ansible"]
},
},
```
3. Create an [OAuth client](https://login.tailscale.com/admin/settings/oauth). 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](https://nextdns.io), 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:
```yml
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:
```bash
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:
```yml
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.
## Deploy
Complete the applicable [manual application setup](APPLICATIONS.md#services-that-need-manual-setup), then run:
```bash
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](https://tailscale.com/kb/1028/key-expiry#disabling-key-expiry).
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](APPLICATIONS.md#backup-and-disaster-recovery).
## 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](https://docs.renovatebot.com/modules/platform/github/#running-as-a-github-app).
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:
- `archanes_github_app_id`: the App ID
- `archanes_github_app_installation_id`: the installation ID
- `archanes_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](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows).
4. Edit the open Dependency Dashboard issue. The workflow starts a Woodpecker pipeline after the issue body has stayed unchanged for 60 seconds.