# 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 - `make` or `make 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 lint` is the only lint step needed. - `make deploy` - Deploy all configurations to the home server - `make deploy TAGS=sometag` - Deploy only specific tagged tasks - `make deploy TARGET=specific-host` - Deploy to specific host instead of all - `make check` - Run deployment in dry-run mode showing potential changes - `make deploy-labelprint` / `make check-labelprint` - Deploy (or dry-run) the label print proxy Pi. Separate inventory and playbook from the home server - see `ansible/roles/labelprint/README.md` - `make bootfs BOOTFS=/Volumes/bootfs` - Render the label print proxy's cloud-init first-boot files onto a freshly imaged SD card - `make vault` - Edit encrypted Ansible vault file - `make list-tags` - List all available Ansible tags - `make list-tasks` - List all Ansible tasks - `make 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` ### Git Workflow - Work directly on `master` - no feature branches and no pull requests in this repo. - Commit to `master` and push to `origin/master` when asked; don't create a branch first. - Pushing to `master` also triggers `.gitea/workflows/ci-images.yml` (see Gitea Actions CI images below), which only rebuilds images whose inputs changed. ## 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-backup` to create a local encrypted backup - Use `make git-crypt-restore` to recover from git-crypt corruption - 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 point - `defaults/main.yml` - Default variables - `handlers/main.yml` - Event handlers - `meta/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 tasks - ~~`drone` - CI/CD tasks (decommissioned)~~ - `hass` - Home Assistant tasks - `gitea-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 configuration - `requirements.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` / `stickah` with 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'` — the `cd;` preamble is REQUIRED (it moves into the podman user's home so podman finds its rootless storage/config; without it commands fail). Replace `command here` with whatever you need to run. - **Interactive shell**: `sudo -H -u podman bash -c 'cd; bash'` - **systemctl --user** requires `XDG_RUNTIME_DIR`: ```bash sudo -H -u podman bash -c 'export XDG_RUNTIME_DIR=/run/user/$(id -u); systemctl --user ' ``` 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.