Things I've learned, collected in abdelhousni/til. Site pattern and tooling adapted from simonw/til.
Entries are drafted with Claude Code, which also runs the commands they describe, usually in a Linux cloud container; I choose the topics, review and edit every entry, and publish it. Each entry says what was run and what wasn't.
Browse these TILs at https://til.housni.eu/
137 TILs so far. Atom feed here.- Starting an Ansible role project with uv for the venv - 2026-09-05
- The Foreman/Satellite dynamic inventory plugin - 2026-09-05
- Targeting hosts with patterns, --limit and constructed groups - 2026-09-05
- Storing an Ansible Galaxy token as an environment variable, not in ansible.cfg - 2026-09-05
- git tag basics, grounded in how Ansible collection releases actually use them - 2026-09-12
- Deploying a Podman Quadlet stack on RHEL9 with linux-system-roles - 2026-09-16
- Ansible Vault encrypts the secret in git; Podman's default driver stores it in plaintext - 2026-09-20
- What Ansible Vault actually encrypts, and where that protection stops - 2026-09-24
- Handling deployment failures with Ansible's block/rescue/always - 2026-09-29
- Locking an Ansible development environment: pip, venv, pip-tools, uv or an execution environment - 2026-09-29
- Pinning ansible-core with pip-tools, uv and Poetry - 2026-09-29
- Building an Ansible execution environment from a locked requirements file - 2026-09-29
- Where to run an Ansible development environment: venv, Dev Container, Remote-SSH, code-server or Dev Spaces - 2026-09-29
- Ansible Development Tools (ADT): one install, and the Python version decides what you get - 2026-09-29
- Committing the VS Code Ansible settings with the repository - 2026-09-29
- Running playbooks locally in the execution environment production uses - 2026-09-30
- An Ansible Dev Container: choosing the scaffolded config, Podman, and the EE navigator falls back to - 2026-09-30
- A shared Ansible dev server for VS Code Remote-SSH, built with Ansible - 2026-09-30
- Linting Ansible before it reaches Git:
--fixin the editor, the same ansible-lint in CI - 2026-09-30 - Scaffolding with ansible-creator: roles inside a collection, and what to change in the output - 2026-09-30
- A collection-aware venv with ansible-dev-environment (ade): what it installs, what it edits, and how to pin it - 2026-09-30
- One sudoers line per command with community.general.dict_kv: building a list of dicts from a list of values - 2026-10-01
- Lists and dicts back and forth: dict2items, items2dict and zip on role, Foreman and Proxmox data - 2026-10-01
- Merging dicts with combine: PostgreSQL settings in layers, Quadlet units from a base - 2026-10-01
- Picking from a list of dicts: selectattr, rejectattr and map on Proxmox guests and host facts - 2026-10-02
- subelements versus product: Quadlet volume directories and PostgreSQL pg_hba rules - 2026-10-02
- Set operations on lists: declared Proxmox guests against the cluster, and why the order changes between runs - 2026-10-02
- Strings into structures: df with split and regex_findall, findmnt with from_json - 2026-10-02
- default, default(omit), mandatory and ternary: sudo rules that don't set every field - 2026-10-02
- Forcing types: extra vars arrive as strings, and ansible-core 2.19 stopped guessing - 2026-10-02
- Writing data out: a Caddy JSON configuration with to_nice_json, sort_keys and indent - 2026-10-03
- Data from other hosts: pg_hba rules from the app servers' facts, with hostvars and extract - 2026-10-03
- groupby, groupby_as_dict and lists_mergeby: Proxmox guests by node, and joined to what the team declares - 2026-10-03
- json_query or native filters: part 4's Proxmox selections written in JMESPath - 2026-10-03
- Network data with ansible.utils: checking pg_hba subnets and numbering Proxmox guests - 2026-10-03
- set_fact in a loop or one expression: part 11's pg_hba rules, built both ways - 2026-10-03
- Text on lists: building a systemd ExecStart line with regex_replace, join and replace - 2026-10-03
- Hosts and groups: the two groups every inventory has, all and ungrouped - 2026-10-03
- An inventory as a directory: a hosts file without variables, and group_vars per role - 2026-10-03
- INI or YAML for the hosts file, and why variables stay out of both - 2026-10-03
- Connection variables: ansible_host, ansible_port, ansible_user and ansible_connection, with ssh, docker and local - 2026-10-03
- Environments in an Ansible inventory: separate directories, or prod and staging as groups - 2026-10-03
- Checking what Ansible sees: ansible-inventory --graph, --list and --host - 2026-10-03
- Facts or variables: what Ansible discovers (as-is) against what you declare (to-be) - 2026-10-03
- Where a variable should live: role defaults, group_vars, host_vars, and extra vars as safety switches - 2026-10-03
- Inventory precedence: all, parent, child, host, and what ansible_group_priority really does - 2026-10-03
- Secrets in the inventory: vault.yml next to vars.yml, with plaintext aliases - 2026-10-03
- --limit in practice: every play, run_once per batch, and the facts of hosts left out - 2026-10-03
- Let the inventory be the loop: delegate_to instead of a list of hosts - 2026-10-03
- Several inventories at once: -i dir/, load order, and which source wins - 2026-10-03
- A single source of truth: inventory plugins, enable_plugins and auto - 2026-10-03
- The Proxmox inventory plugin: guests as hosts, filtered by tag and status - 2026-10-03
- ansible.builtin.constructed: keyed_groups, groups and compose on top of another source - 2026-10-03
- When the inventory cache lies: stale hosts, cache_timeout and --flush-cache - 2026-10-03
- The inventory cache and performance: request counts, want_facts and timeouts - 2026-10-03
- add_host: provision then configure in one run - 2026-10-03
- group_by: groups from facts, built during the run - 2026-10-03
- Writing an inventory plugin, after the trust order - 2026-10-03
- Inventory in AAP: sources from a project, smart and constructed inventories - 2026-10-03
- Testing the inventory in CI, with a JSON Schema and policy checks - 2026-10-03
- Local facts in facts.d: declared data that looks measured - 2026-10-04
- Publishing a TIL collection as a static GitHub Pages site - 2026-09-05
- Adding an Atom feed and syntax highlighting to a static site build script - 2026-09-05
- Rendering Mermaid diagrams in a Python-Markdown static site - 2026-09-12
- Hosting my own copy of Datasette Lite from a fork - 2026-10-08
- Hosting a simple PHP page with Podman + Caddy, with automatic HTTPS - 2026-09-05
- Moving a container image between hosts with podman save + scp + load, no registry - 2026-09-05
- Sanity-checking a fresh Docker or Podman install with each engine's own hello-world - 2026-09-06
- Podman equivalents to Docker's Dive image-layer explorer - 2026-09-12
- Root vs rootless Podman on RHEL 10 and Ubuntu 26.04 - 2026-09-13
- Auto-updating the Caddy/Adminer/PHP Quadlet stack needs more than one AutoUpdate key - 2026-09-21
- Pointing Podman at an Artifactory mirror without editing a single image name - 2026-09-21
podman composefails because it handed the job to docker-compose, which needs the Podman socket - 2026-10-09
- Installing a RIPE Atlas software probe in a Proxmox LXC - 2026-09-05
- Injecting qemu-guest-agent into an Ubuntu cloud template, from the CLI - 2026-09-07
- A libvirt RHEL Kickstart example ported to Proxmox, minus the one flag with no equivalent - 2026-09-20
- Debugging a Proxmox VM whose cloud-init config didn't apply - 2026-09-20
- A Proxmox VM on demand, NixOS from Git: OpenTofu builds a skeleton, nixos-anywhere replaces it, and the host key exists before the VM - 2026-09-27
- ntfy in a Proxmox LXC, private and behind Caddy for TLS - 2026-10-08
- A regex link checker breaks on the exact HTML it was meant to check - 2026-09-05
- Getting a newer Python on RHEL without touching the system python3 - 2026-09-29
- Don't hand-write a sitemap.xml, generate it from data you already have - 2026-09-05
- robots.txt for a small static site is basically a pointer to the sitemap - 2026-09-05
- A schema.org Person block is what actually helps you rank for your own name - 2026-09-05
- Verifying a GitHub Pages site with Bing Webmaster Tools (no DNS needed) - 2026-09-05
- Checking a TLS certificate's dates, issuer, and SANs with openssl - 2026-09-05
- Splitting a .pfx into a certificate, key, and CA chain with openssl - 2026-09-05
- Adding a certificate to a Java keystore/truststore with keytool - 2026-09-05
- A Let's Encrypt certificate for a LAN-only service: Caddy, DNS-01 and deSEC - 2026-10-08
- Layering extra cloud-init config without fighting Terraform/OpenTofu's auto-generated user-data - 2026-09-07
- Installing git, gh, and glab, and the auth each one actually needs - 2026-09-12
- Syncing a diverged fork: take the pipeline fixes, not the content - 2026-09-27
- Configuring a repository for coding agents: what the guidance actually says - 2026-09-27
- A branch you fetched was force-pushed: keep the old tip, then
rebase --onto- 2026-09-27
- GitLab with OAuth 2.0 / OIDC — the simple principle - 2026-09-12
- What Kubernetes actually is, and how it works - 2026-09-17
- Cleanly stopping an RKE2 node for planned maintenance - 2026-09-17
- Installing a single-node RKE2 server for a lab - 2026-09-18
- Getting kubectl to work against RKE2 from off the node - 2026-09-18
- What RKE2 actually is, and how its pieces fit together - 2026-09-18
- Pods, Deployments, Services — the minimum object model - 2026-09-18
- RKE2's default CNI is Canal, and you pick it before the first start - 2026-09-18
- RKE2 ships no default StorageClass, and a PVC will sit Pending forever - 2026-09-18
- RKE2's ingress default moved to Traefik, because ingress-nginx is ending - 2026-09-18
- Going HA with RKE2: three servers, one address, and the datastore choice - 2026-09-18
- RKE2's etcd snapshots run on schedule, but a fresh cluster starts with none - 2026-09-19
- Restoring RKE2 etcd across an HA cluster, and backing snapshots up to S3 - 2026-09-20
- RKE2 leaves its servers schedulable, so your workloads have been running on the control plane all along - 2026-09-20
- What Packer actually is, and how it works - 2026-09-17
- Kickstart, cloud-init and Image Builder are three layers, not three choices - 2026-09-20
- What Terraform/OpenTofu actually is, and how it works - 2026-09-17
- What Terraform/OpenTofu, Nomad, and Packer are, and how they relate - 2026-09-17
- Terraform state locking just dropped its DynamoDB requirement - 2026-09-23
- What Terraform, OpenTofu, and Packer promise about secrets, and where each promise stops - 2026-09-29
- What cgroups v2 actually is, and how Podman and Kubernetes use it - 2026-09-18
- Building a custom WSL2 kernel on GitHub Actions:
KCFLAGS, notCFLAGS- 2026-09-28 - Checking new firewall rules with nc and Python: open, refused, or dropped - 2026-09-29
- Sudo rules that hand out a root shell, and a CI check that refuses them - 2026-10-01
- Finding and force-closing a locked file on a Windows SMB share - 2026-09-18
- RDP into Windows 11 with a Microsoft account: the password, not the PIN, and
MicrosoftAccount\- 2026-09-28 - Oh My Posh in PowerShell and in WSL2 zsh, with one config file - 2026-09-28
- Installing PowerShell 7 on Windows, Debian, Ubuntu and RHEL, the way Microsoft documents it - 2026-09-29
- Running Linux containers on Windows with wslc, the container CLI built into WSL - 2026-10-06
- First steps on NixOS: the whole system is one file, and every change is a boot entry - 2026-09-25
- NixOS on WSL2: a short admin runbook, and the files WSL manages instead of NixOS - 2026-09-25
- Oh My Zsh on NixOS: the plugin list installs nothing, and NixOS aliases win - 2026-09-26
- Testing a NixOS configuration on GitHub Actions: evaluate on every push, boot it where KVM is - 2026-09-26
- Testing a NixOS configuration on self-managed GitLab CE: the eval job runs anywhere, the VM test needs a runner you prepare - 2026-09-26
- Home Manager as a NixOS module: dotfiles in the same rebuild, and the file that's in the way - 2026-09-27
- A user's PATH on NixOS: declare packages, and know which settings reach services - 2026-09-28
- A minimal IaC toolbox on WSL2 with standalone Home Manager: Nix owns Python and OpenTofu, uv owns Ansible - 2026-10-08
- Starting with Jev in Claude Code: a plugin that adds a skill, and an API key for experiments - 2026-09-29
The published site is built by .github/workflows/publish.yml running three scripts and uploading _site/. The Makefile runs the same three, in the same order, so a clean make preflight locally means a clean deploy.
git clone https://github.com/abdelhousni/til.git # not --depth 1, see below
cd til
make serve # creates .venv, installs deps, builds, serves on :8000| target | what it does |
|---|---|
make help |
list these targets |
make venv |
create .venv and install requirements.txt |
make build |
build the site into _site/ |
make serve |
build, then serve on localhost:8000 (make serve PORT=9000 to move it) |
make check |
build + internal link check, fast |
make check-external |
build + external link check, slow — this one is the deploy gate |
make readme |
regenerate the index in this README |
make preflight |
everything CI runs, in CI's order |
make clean / clean-all |
drop _site/, and .venv/ too |
Two things worth knowing, both of which produce wrong output rather than an error, which is why make checks for them:
- Clone with full history. Entry dates come from
git log --follow --diff-filter=A, so a--depth 1clone dates every entryunknownand scrambles the ordering. CI setsfetch-depth: 0for the same reason. - Commit before you build. An uncommitted
.mdfile has no history to read a creation date from, so it renders asunknownand leaks into the README index if you regenerate it.
Serve _site/ rather than opening it with file://. Internal links are all relative, so the locally-served copy behaves exactly like the /til/ subpath in production — but Mermaid diagrams load as an ES module from a CDN, which a file:// origin blocks. Canonical URLs, sitemap.xml and feed.atom always point at the production host; that is expected locally.