The runner's job images were built by ansible into localhost/ only, so the nightly CI prune deleted them and every idle stretch ended with CI failing in under a second on `docker pull localhost/gitea-ci:latest` until someone re-ran the role and waited out a rebuild. The previous commit moved them to the Gitea registry; this moves the *build* off the deploy path entirely. - .gitea/workflows/ci-images.yml builds files/Containerfile.* and pushes to git.debyl.io/gitbot/. Per-image change detection, so an ESP-IDF pin bump does not rebuild the other two; weekly schedule for base-image updates; a workflow_dispatch selector. PRs build under a throwaway :pr-<n> tag and drop it -- the build lands in the live runner's store, and act_runner will not re-pull a tag it already has, so a PR using the real tag would hand every later job on this host an unmerged image. - The Containerfiles stop being ansible templates: their version vars are now --build-arg, read by the workflow out of the same defaults/main.yml the role interpolates, so CI and ansible build the same bytes from one set of pins. - LABEL io.debyl.ci-base moves into each Containerfile so neither builder can forget the prune exemption; the workflow re-checks it before pushing. - roles/gitea-actions pulls instead of building. gitea_ci_build_local=true restores the local build+push for seeding a cold registry or when CI is down -- the workflow that builds gitea-ci runs in gitea-ci. - Lint .gitea/ alongside ansible/, and document the flow in the role README. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.5 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Overview
This is a home infrastructure deployment repository using Ansible for automated server configuration and container deployment. The project follows a "one-button deployment" philosophy for managing a home server environment with various self-hosted services.
Development Commands
Core Commands
makeormake lint- Run yamllint on all YAML files. Output may only show "Running yamllint..." and "Done." with no errors listed — this means linting passed. Do NOT run yamllint or ansible-lint manually;make lintis the only lint step needed.make deploy- Deploy all configurations to the home servermake deploy TAGS=sometag- Deploy only specific tagged tasksmake deploy TARGET=specific-host- Deploy to specific host instead of allmake check- Run deployment in dry-run mode showing potential changesmake deploy-labelprint/make check-labelprint- Deploy (or dry-run) the label print proxy Pi. Separate inventory and playbook from the home server - seeansible/roles/labelprint/README.mdmake bootfs BOOTFS=/Volumes/bootfs- Render the label print proxy's cloud-init first-boot files onto a freshly imaged SD cardmake vault- Edit encrypted Ansible vault filemake list-tags- List all available Ansible tagsmake list-tasks- List all Ansible tasksmake git-crypt-backup- Backup git-crypt symmetric key (encrypted with GPG)make git-crypt-restore- Restore git-crypt symmetric key from backup
Environment Setup
The project uses Python virtualenv for dependency management:
- Dependencies are locked in
requirements.txt(ansible + yamllint) - Makefile automatically creates
.venv/and installs dependencies - Vault password is sourced from password manager via
.pass.sh
Architecture
Directory Structure
ansible/
├── deploy.yml # Main playbook entry point (imports deploy_home.yml)
├── deploy_home.yml # Core playbook with role definitions
├── deploy_labelprint.yml # Label print proxy Pi (separate host, Debian)
├── inventories/home/ # Inventory configuration
├── inventories/labelprint/ # Label print proxy inventory
├── roles/ # Ansible roles organized by function
│ ├── common/ # Base system configuration
│ ├── git/ # Git repository management
│ ├── podman/ # Container orchestration
│ ├── ssl/ # Legacy SSL management (deprecated - Caddy handles certificates automatically)
│ ├── github-actions/# CI/CD runner setup
│ ├── labelprint/ # 4x6 label print proxy (Raspberry Pi, CUPS/TSPL)
│ ├── gitea-actions/ # Gitea Actions runners + CI job images (see its README)
│ └── pihole/ # DNS filtering
└── vars/
└── vault.yml # Encrypted secrets
Container Organization
Containers are organized in ansible/roles/podman/tasks/containers/:
base/- Core infrastructure containers (Caddy web server, AWS DDNS)home/- Home-specific services (Home Assistant, PartKeepr, Immich photos, Nextcloud, Redis)debyltech/- Personal/business services (Fulfillr)skudak/- Additional services (BookStack wiki, Nextcloud)
Security Model
- Ansible vault for encrypted secrets management
- Password sourced from external password manager
- Git-crypt for repository-level encryption (see
.gitattributes)- Symmetric key can be backed up locally in
.git-crypt-backup/(encrypted with GPG) - Use
make git-crypt-backupto create a local encrypted backup - Use
make git-crypt-restoreto recover from git-crypt corruption
- Symmetric key can be backed up locally in
- SSH key-based authentication to target hosts
- Caddy provides automatic HTTPS with LetsEncrypt certificates
- Built-in security headers and IP-based access restrictions
Key Patterns
Role Structure
Each Ansible role follows standard structure:
tasks/main.yml- Main task entry pointdefaults/main.yml- Default variableshandlers/main.yml- Event handlersmeta/main.yml- Role metadata and dependencies
Container Deployment Pattern
Container tasks follow consistent patterns:
- Firewall configuration
- Container image specification via variables
- Service configuration through imported task files
- Tag-based selective deployment
Tagging Strategy
Tasks are tagged by service/component for selective deployment:
caddy- Web server tasks (replaced nginx)ddns- Dynamic DNS tasksdrone- CI/CD tasks (decommissioned)hass- Home Assistant tasksgitea-actions- Gitea Actions runners and their CI job images- Common infrastructure tags like
common,ssl
Configuration Files
ansible.cfg- Ansible configuration with performance optimizations.yamllint.yml- YAML linting rules (braces disabled).lint-vars.sh- Ansible-lint skip configurationrequirements.txt- Python dependencies with pinned versions
Target Environment
- Primary target host:
home.debyl.io - OS: Fedora (ansible_user: fedora)
- Container runtime: Podman
- Web server: Caddy with automatic HTTPS and built-in security (replaced nginx + ModSecurity)
- All services accessible via HTTPS with automatic certificate renewal
CI/CD: Drone CI infrastructure completely decommissioned
Label print proxy
- Second target host:
stickah.local(Raspberry Pi 3B+, Raspberry Pi OS 64-bit, apt not dnf) with a Phomemo PM246 4x6 label printer on USB - Deployed only via
make deploy-labelprint- it is in its own inventory so the Fedora roles can never run against it - The SD card image is written by hand with rpi-imager, but its cloud-init files
come from
make bootfs, rendered from the same templates the role uses - Login is
stickah/stickahwith SSH key auth preferred; password auth is a deliberate LAN-only fallback so the box is never unreachable - LAN-only (192.168.1.0/24), discovered over mDNS, no DNS record
- Falls back to its own rescue Wi-Fi AP at 192.168.4.1 when the home SSID is unreachable
Gitea Actions CI images
The runner's job images (gitea-ci, gitea-ci-espidf, gitea-ci-platformio)
are built by .gitea/workflows/ci-images.yml and published to the Gitea
container registry under git.debyl.io/gitbot/. The gitea-actions role only
pulls them - do NOT add build steps back to it. To change an image, edit
ansible/roles/gitea-actions/files/Containerfile.* (or a version pin in that
role's defaults/main.yml) and push to master; CI rebuilds only what changed.
See ansible/roles/gitea-actions/README.md for the registry rationale, the
prune-exemption label, and the bootstrap path when CI itself cannot build.
Remote SSH Commands for Service Users
The podman user (and other service users) have /bin/nologin as their shell. To run commands as these users via SSH:
- One-off commands:
sudo -H -u podman bash -c 'cd; command here'— thecd;preamble is REQUIRED (it moves into the podman user's home so podman finds its rootless storage/config; without it commands fail). Replacecommand herewith whatever you need to run. - Interactive shell:
sudo -H -u podman bash -c 'cd; bash' - systemctl --user requires
XDG_RUNTIME_DIR:sudo -H -u podman bash -c 'export XDG_RUNTIME_DIR=/run/user/$(id -u); systemctl --user <action> <service>'
Podman is a user-specific (rootless) container runtime, not a system service like Docker. The user context matters for all podman and systemctl --user operations. The default SSH user (fedora) has sudo access and can run commands directly.