add homekeeper stack

This commit is contained in:
2026-07-06 19:25:38 +02:00
commit 58983e6855
90 changed files with 6816 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
# Homekeeper Infrastructure
Ansible setup for a Hetzner VM (Ubuntu 24.04) running Homekeeper via Podman.
## Architecture
```
nginx (system) ← HTTPS, routes by path
├── / ← static landing page (/var/www/html)
├── /gitea/ → Gitea on :3000 (git + container registry)
├── /api/ → homekeeper-api on :8000
├── /beekeeper/ → homekeeper-beekeeper on :3838
└── /listkeeper/ → homekeeper-listkeeper on :3839
Podman (rootful, Quadlets → systemd units)
├── homekeeper-db postgres:17, data at /opt/homekeeper/pg_data
├── homekeeper-api from Gitea registry, auto-update enabled
├── homekeeper-beekeeper from Gitea registry, auto-update enabled
└── homekeeper-listkeeper from Gitea registry, auto-update enabled
gitea container from docker.io, auto-update disabled
```
## First-time setup
### 1. Provision a Hetzner VM
- Ubuntu 24.04, min. CX22 (2 vCPU, 4 GB RAM)
- Add your SSH key in Hetzner console
- Point your domain DNS A-record to the VM IP
### 2. Configure variables
Edit `group_vars/all.yml`:
- Set `domain`
- Set all `CHANGE_ME` passwords (consider `ansible-vault encrypt_string`)
- Update `inventory/hosts.yml` with the VM IP
### 3. First run (without registry login — Gitea not yet up)
Comment out the "Login to Gitea registry" task in `roles/podman/tasks/main.yml`,
then run without the homekeeper role:
```bash
ansible-playbook -i inventory/hosts.yml site.yml --skip-tags homekeeper
```
### 4. Set up Gitea
- Visit https://git.friessn.de/ and complete the installation wizard
- Create user matching `gitea_admin_user`
- Create a repository for the code
- Generate an API token (Settings → Applications) with `package:write` scope
- Set `registry_token` in `group_vars/all.yml`
### 5. Push images to Gitea registry
On your local machine, build and push the three custom images:
```bash
# Login to Gitea registry
docker login git.friessn.de -u nico
# Build and push
docker build -t git.friessn.de/nico/homekeeper-api:latest ./api
docker build -t git.friessn.de/nico/homekeeper-beekeeper:latest ./beekeeper
docker build -t git.friessn.de/nico/homekeeper-listkeeper:latest ./listkeeper
docker push git.friessn.de/nico/homekeeper-api:latest
docker push git.friessn.de/nico/homekeeper-beekeeper:latest
docker push git.friessn.de/nico/homekeeper-listkeeper:latest
```
### 6. Full playbook run
```bash
ansible-playbook -i inventory/hosts.yml site.yml
```
## Deploy updates
After pushing a new image to the Gitea registry, `podman auto-update` picks it up
automatically within `autoupdate_schedule` (default: every 15 minutes).
For an immediate deploy:
```bash
ssh root@YOUR_IP podman auto-update
```
## Secrets management
For production, use ansible-vault:
```bash
ansible-vault encrypt_string 'mysecretpassword' --name 'db_password'
```
Paste the output into `group_vars/all.yml` and run playbooks with `--ask-vault-pass`.
+29
View File
@@ -0,0 +1,29 @@
---
# Main domain (apps + landing page)
domain: home.friessn.de
# Gitea on a subdomain — cleaner than a subpath, required for container registry
gitea_domain: git.friessn.de
gitea_data_dir: /opt/gitea
gitea_http_port: 3000 # internal port, nginx proxies HTTPS → here
gitea_admin_user: nico
gitea_admin_password: CHANGE_ME # replace — set via ansible-vault in production
gitea_admin_email: nico.friess@googlemail.com
# Container registry — Gitea's built-in OCI registry, same host as Gitea
# Images: git.friessn.de/nico/homekeeper-api:latest
registry_host: "{{ gitea_domain }}"
registry_user: "{{ gitea_admin_user }}"
registry_token: CHANGE_ME # Gitea API token with package:write — set after first Gitea start
# Homekeeper app
homekeeper_data_dir: /opt/homekeeper
db_name: homestead
db_user: homestead
db_password: CHANGE_ME # replace — set via ansible-vault in production
api_user: homestead
api_pass: CHANGE_ME # replace
initial_users: "nico:CHANGE_ME" # user:password pairs for API auth
# Auto-update timer interval (OnCalendar syntax)
autoupdate_schedule: "*:*:0/10" # every 10 seconds
+7
View File
@@ -0,0 +1,7 @@
---
all:
hosts:
homekeeper:
ansible_host: YOUR_HETZNER_IP # replace with actual IP
ansible_user: root
# ansible_ssh_private_key_file: ~/.ssh/id_ed25519
@@ -0,0 +1,45 @@
---
- name: Upgrade all packages
apt:
update_cache: true
upgrade: dist
cache_valid_time: 3600
- name: Install base packages
apt:
name:
- curl
- git
- ufw
- fail2ban
- unattended-upgrades
- ca-certificates
state: present
- name: Configure ufw — allow SSH
ufw:
rule: allow
name: OpenSSH
- name: Configure ufw — allow HTTP
ufw:
rule: allow
port: "80"
proto: tcp
- name: Configure ufw — allow HTTPS
ufw:
rule: allow
port: "443"
proto: tcp
- name: Enable ufw
ufw:
state: enabled
policy: deny
- name: Enable unattended-upgrades
service:
name: unattended-upgrades
enabled: true
state: started
+26
View File
@@ -0,0 +1,26 @@
---
- name: Create Gitea quadlet directory
file:
path: /etc/containers/systemd
state: directory
mode: "0755"
- name: Deploy Gitea container quadlet
template:
src: gitea.container.j2
dest: /etc/containers/systemd/gitea.container
mode: "0644"
notify:
- Reload systemd
- Restart gitea
handlers:
- name: Reload systemd
systemd:
daemon_reload: true
- name: Restart gitea
systemd:
name: gitea
state: restarted
enabled: true
@@ -0,0 +1,23 @@
[Unit]
Description=Gitea — Git service and container registry
After=network-online.target
[Container]
Image=docker.io/gitea/gitea:latest
ContainerName=gitea
PublishPort=127.0.0.1:{{ gitea_http_port }}:3000
Volume={{ gitea_data_dir }}:/data:Z
Environment=USER_UID=1000
Environment=USER_GID=1000
Environment=GITEA__server__DOMAIN={{ gitea_domain }}
Environment=GITEA__server__ROOT_URL=https://{{ gitea_domain }}/
Environment=GITEA__server__HTTP_PORT=3000
Environment=GITEA__packages__ENABLED=true
Environment=GITEA__packages__CHUNKED_UPLOAD_PATH=/data/tmp/package-upload
[Service]
Restart=always
TimeoutStartSec=120
[Install]
WantedBy=multi-user.target default.target
@@ -0,0 +1,43 @@
---
- name: Create quadlet directory
file:
path: /etc/containers/systemd
state: directory
mode: "0755"
- name: Deploy homekeeper network quadlet
template:
src: homekeeper.network.j2
dest: /etc/containers/systemd/homekeeper.network
mode: "0644"
notify: Reload systemd
- name: Deploy container quadlets
template:
src: "{{ item }}.j2"
dest: "/etc/containers/systemd/{{ item }}"
mode: "0644"
loop:
- homekeeper-db.container
- homekeeper-api.container
- homekeeper-beekeeper.container
- homekeeper-listkeeper.container
notify:
- Reload systemd
- Restart homekeeper
handlers:
- name: Reload systemd
systemd:
daemon_reload: true
- name: Restart homekeeper
systemd:
name: "{{ item }}"
state: restarted
enabled: true
loop:
- homekeeper-db
- homekeeper-api
- homekeeper-beekeeper
- homekeeper-listkeeper
@@ -0,0 +1,25 @@
[Unit]
Description=Homekeeper — FastAPI
After=network-online.target homekeeper-db.service
[Container]
Image={{ registry_host }}/{{ registry_user }}/homekeeper-api:latest
ContainerName=homekeeper-api
Network=homekeeper.network
PublishPort=127.0.0.1:8000:8000
Environment=DB_HOST=homekeeper-db
Environment=DB_PORT=5432
Environment=DB_NAME={{ db_name }}
Environment=DB_USER={{ db_user }}
Environment=DB_PASSWORD={{ db_password }}
Environment=ROOT_PATH=/api
Environment=INITIAL_USERS={{ initial_users }}
AutoUpdate=registry
Label=io.containers.autoupdate=registry
[Service]
Restart=always
TimeoutStartSec=120
[Install]
WantedBy=multi-user.target default.target
@@ -0,0 +1,21 @@
[Unit]
Description=Homekeeper — Beekeeper (Shiny)
After=network-online.target homekeeper-api.service
[Container]
Image={{ registry_host }}/{{ registry_user }}/homekeeper-beekeeper:latest
ContainerName=homekeeper-beekeeper
Network=homekeeper.network
PublishPort=127.0.0.1:3838:3838
Environment=API_URL=http://homekeeper-api:8000
Environment=API_USER={{ api_user }}
Environment=API_PASS={{ api_pass }}
AutoUpdate=registry
Label=io.containers.autoupdate=registry
[Service]
Restart=always
TimeoutStartSec=120
[Install]
WantedBy=multi-user.target default.target
@@ -0,0 +1,23 @@
[Unit]
Description=Homekeeper — PostgreSQL
After=network-online.target
[Container]
Image=docker.io/library/postgres:17
ContainerName=homekeeper-db
Network=homekeeper.network
Volume={{ homekeeper_data_dir }}/pg_data:/var/lib/postgresql/data:Z
Environment=POSTGRES_DB={{ db_name }}
Environment=POSTGRES_USER={{ db_user }}
Environment=POSTGRES_PASSWORD={{ db_password }}
HealthCmd=pg_isready -U {{ db_user }} -d {{ db_name }}
HealthInterval=5s
HealthRetries=10
# No AutoUpdate — avoid surprise postgres major-version upgrades
[Service]
Restart=always
TimeoutStartSec=120
[Install]
WantedBy=multi-user.target default.target
@@ -0,0 +1,22 @@
[Unit]
Description=Homekeeper — Listkeeper (Shiny)
After=network-online.target homekeeper-api.service
[Container]
Image={{ registry_host }}/{{ registry_user }}/homekeeper-listkeeper:latest
ContainerName=homekeeper-listkeeper
Network=homekeeper.network
PublishPort=127.0.0.1:3839:3839
Environment=PORT=3839
Environment=API_URL=http://homekeeper-api:8000
Environment=API_USER={{ api_user }}
Environment=API_PASS={{ api_pass }}
AutoUpdate=registry
Label=io.containers.autoupdate=registry
[Service]
Restart=always
TimeoutStartSec=120
[Install]
WantedBy=multi-user.target default.target
@@ -0,0 +1,2 @@
[Network]
Driver=bridge
+50
View File
@@ -0,0 +1,50 @@
---
- name: Install nginx and certbot
apt:
name:
- nginx
- certbot
- python3-certbot-nginx
state: present
update_cache: true
- name: Remove default nginx site
file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: Reload nginx
- name: Deploy homekeeper nginx config
template:
src: homekeeper.conf.j2
dest: /etc/nginx/sites-available/homekeeper.conf
mode: "0644"
notify: Reload nginx
- name: Enable homekeeper nginx site
file:
src: /etc/nginx/sites-available/homekeeper.conf
dest: /etc/nginx/sites-enabled/homekeeper.conf
state: link
notify: Reload nginx
- name: Obtain Let's Encrypt certificates
command: >
certbot --nginx -d {{ domain }} -d {{ gitea_domain }}
--non-interactive --agree-tos -m {{ gitea_admin_email }}
--redirect
args:
creates: /etc/letsencrypt/live/{{ domain }}/fullchain.pem
notify: Reload nginx
- name: Enable nginx
service:
name: nginx
enabled: true
state: started
handlers:
- name: Reload nginx
service:
name: nginx
state: reloaded
@@ -0,0 +1,77 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# ── Main app: friessn.de ────────────────────────────────────────────────────
server {
listen 80;
server_name {{ domain }};
# certbot --nginx adds SSL redirect + listen 443 block here
root /var/www/html;
index index.html;
location = / {
try_files /index.html =404;
}
location = /api { return 301 /api/; }
location /api/ {
rewrite ^/api/(.*) /$1 break;
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /beekeeper { return 301 /beekeeper/; }
location /beekeeper/ {
rewrite ^/beekeeper/(.*) /$1 break;
proxy_pass http://127.0.0.1:3838;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
location = /listkeeper { return 301 /listkeeper/; }
location /listkeeper/ {
rewrite ^/listkeeper/(.*) /$1 break;
proxy_pass http://127.0.0.1:3839;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
}
# ── Gitea: git.friessn.de ───────────────────────────────────────────────────
server {
listen 80;
server_name {{ gitea_domain }};
# certbot --nginx adds SSL redirect + listen 443 block here
client_max_body_size 512m;
location / {
proxy_pass http://127.0.0.1:{{ gitea_http_port }};
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
@@ -0,0 +1,59 @@
---
- name: Install podman
apt:
name:
- podman
- podman-compose # for ad-hoc use; production uses quadlets
state: present
update_cache: true
- name: Create homekeeper data directories
file:
path: "{{ item }}"
state: directory
mode: "0750"
loop:
- "{{ homekeeper_data_dir }}"
- "{{ homekeeper_data_dir }}/pg_data"
- "{{ gitea_data_dir }}"
- name: Configure Gitea as additional registry
template:
src: registries.conf.j2
dest: /etc/containers/registries.conf.d/gitea.conf
mode: "0644"
notify: Restart podman services
- name: Login to Gitea container registry
command: >
podman login {{ registry_host }}:{{ registry_port }}
-u {{ registry_user }} -p {{ registry_token }}
register: login_result
changed_when: "'Login Succeeded' in login_result.stdout"
# Run this after Gitea is up and registry_token is set
- name: Enable podman-auto-update timer
systemd:
name: podman-auto-update.timer
enabled: true
state: started
daemon_reload: true
- name: Override auto-update timer schedule
copy:
dest: /etc/systemd/system/podman-auto-update.timer.d/override.conf
content: |
[Timer]
OnCalendar=
OnCalendar={{ autoupdate_schedule }}
AccuracySec=1s
mode: "0644"
notify: Reload systemd
handlers:
- name: Reload systemd
systemd:
daemon_reload: true
- name: Restart podman services
command: systemctl daemon-reload
@@ -0,0 +1,3 @@
[[registry]]
location = "{{ registry_host }}:{{ registry_port }}"
insecure = false
+10
View File
@@ -0,0 +1,10 @@
---
- name: Homekeeper server setup
hosts: homekeeper
become: true
roles:
- common
- podman
- gitea
- homekeeper
- nginx