add homekeeper stack
This commit is contained in:
@@ -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`.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: Homekeeper server setup
|
||||
hosts: homekeeper
|
||||
become: true
|
||||
roles:
|
||||
- common
|
||||
- podman
|
||||
- gitea
|
||||
- homekeeper
|
||||
- nginx
|
||||
Reference in New Issue
Block a user