This repository is based on the code that Jay Lacroix (LearnLinuxTV) worked on from his Ansible Desktop tutorial on Youtube.
Additionnal inspiration could be taken from Jeef Geerling's work on Ansible
It was improved with the Ansible-pull tutorial and then coding was improved with ChatGPT's Codex (via opencode and pi.dev).
objective is to have something that can be run easily on a new machine or not (idempotent) with:
sudo ansible-pull -d /opt/ansible-pull -U https://github.com/djspatule/ansible-autoconfig.git -i hosts local.yml --vault-password-file ~/secret.txtThe -d /opt/ansible-pull matters: run as sudo, ansible-pull otherwise clones
into /root/.ansible/pull/… (mode 700), and the dotfiles stow step — which
runs as your normal user — cannot read the repo there and fails with a
permission error. Always check out into a shared, readable path. (The managed
auto-pull timer already does this.)
in test mode, to avoid having to "git push" each modification in order to test it locally on a development machine where the repo is present (because i'm currently working on it for instance), use:
sudo ansible-pull -U "~/Documents/ansible-autoconfig/" --check
or
sudo ansible-playbook -i "localhost," -c local local.yml --check
remember that ansible-pull is just a wrapper for ansible-playbook....so both cmds are kinda similar ... and the check option allows to test on a system without implementing anything ('dry run').
- initiate base role
- implement server roles (fresh arch and ubuntu machines compatible)
- keep server and workstation roles Arch-compatible where practical for server flexibility and future distro hopping
- use tags to only test/execute parts of the ansible-autoconfig script... that should accelerate dev/debug and limit need for protection against downloads (to preserve bandwidth).
- include running pi-hole update as part of the ansible playbook? Include also homepage update, etc. (since they are dockers, pull the latest image?)
- pull latest container images on each run unless it slows runs down too much
- server services
- nextcloud: implement "old" users ?
- re-establish when possible the conf file for Homepage as on raspi (with the same "widgets" when relevant). Update also the links there.
- other services:
- borg backups for
- server services — and the restore path is tested: a full archive restores cleanly onto a fresh Ubuntu VM (all 53k files decrypt and extract, DB dumps byte-for-byte). See Disaster Recovery.
- server files, including aumenuilya, tabletop-timer, and other
service data (these paths are not in
backup_pathsyet)
- timeshift back-up on SSD_512 for simple system-level checkpoints and restore points for serverannah internal storage only
- Odoo with small database stored on serverannah internal SSD under /home
- Plex reading media from SSD_1TO
- Dictation app/server (voxtype, whisper, etc.) ? Need to be discussed before.
- Excalidraw docker (at draw.dinnizer.com, secured via caddy auth)
- have docker-compose to try new dockers easily (Tryton, ERPNext, Hermes agent, Odysseus by PewDiePie, etc.)
- borg backups for
- aumenuilya:
- why is it an old version and not the one "running" on the pi ? use SSH to create the latest back-up and replace in SSD_1TO (keep the old back-up).
- updating wordpress to its latest version doesn't work....Debug.
- dinnizer
- App.dinnizer.com:
- correct minimally and deploy the dinnizer app at app.dinnizer.com.
- make the last debugs/improvements. These come from
Dinnizer app todo.csv(kept as the detailed scratch list until the items below are closed; the relevant still-open ones are folded in here):- BUG: front UI breaks when a dinner has too many guests or recipes
- BUG:
dinner_form - order index pages with
.order(:last_name, :created_at) - front: list each recipe's dinners and guests
- add a Markdown content editor for recipe descriptions (if an easy gem exists)
- seed the recipe base with aumenuilya recipes (or link to a recipe list)
- save photos in the seed so
db:seeddoes not drop them - link a recipe's ingredients to each guest's likes and dislikes
- allow recipes to be categorized
- generate default "initials" pictures for users, guests and recipes
- add a per-user stats page
- (later/marketing) usecase video, JBB feedback
- develop a very simple dinnizer landing page at dinnizer.com for a CFO
part time service.
- serve it as a standalone static Caddy site
- write it in HTML with the latest Material Design implementation
- use this palette: https://colorhunt.co/palette/f9f7f7dbe2ef3f72af112d4e
- define page contents later
- App.dinnizer.com:
- tabletop-timer
- slightly upgrade the tabletop-timer app ? focus on mobile responsivness and upgrade the app's security and usability without changing the code significantly as it was and still needs to be the fruit of my work for the major part.
- analytics and monetization
- Google Analytics :
- configure tabletop-timer with G-5E4TWCP28D
- configure app.dinnizer with G-SB091QKKGE
- leave aumenuilya alone because it was set up another way
- add advertising/monetization to these three sites?
- Google Analytics :
- secure server
- implement tailscale — delayed on purpose. It is a private overlay: every device (you and family) must install the app and authenticate before reaching a hosted service, which is the wrong trade-off for the public family-facing sites already handled well by Caddy auth + fail2ban. Its real value is your own remote admin access, so it is a later nice-to-have, not a security upgrade over what exists.
- implement caddy auth for homepage and bentopdf and fail2ban jail after several failed attemps.
- update server automatically
- run ansible-pull on a schedule. Implemented in
roles/baseas a systemd timer (autoconfig-pull.timer,OnCalendar=daily,RandomizedDelaySec=30m,Persistent=true) plus a helper script and a oneshot service. Works on both Arch and Debian families. - make the daily pull resilient to a dirty checkout: the helper (and
bootstrap-server.sh) force-sync to the remote branch (fetch+checkout -f+reset --hard+clean -fd) so a file an app rewrote through a dotfile symlink cannot break the run. - optionally add a dedicated passwordless-sudo user. The timer currently runs as root via systemd, so this is now a hardening nicety, not a blocker.
- run ansible-pull on a schedule. Implemented in
- implement workstation role
- dotfiles:
- create first CLI/TUI/dotfiles foundation
- migrate the first CLI/TUI dotfiles from the standalone
omarchy-dotfilesrepo intofiles/dotfiles/:nvim,git,ghconfig,lazygit, plus the existingbash/starship/tmux/ssh/opencode. Stow runs with--no-foldingso app-written files (gh token, ssh keys, lock files) never land in the repo; validated idempotent (changed=0) on the VM. - keep migrating remaining CLI/TUI dotfiles as needed. Gotcha: omarchy
ships machine-specific theme symlinks (nvim
theme.lua,btoptheme) that must NOT be vendored — omarchy manages them locally. GUI/Omarchy desktop config stays out of scope (see below). - protect secrets with ansible-vault so nothing lands in this public
repo unencrypted.
~/.ssh/config.localis vault-encrypted and deployed by copy-decrypt; theghOAuth token is deliberately not committed (see Secrets And Vault).
- test on the disposable Arch/Omarchy VM (
savannarchome). Self-pull viaansible-pullvalidated: base + workstation, Stow dotfiles, and the vaultedconfig.localdeploy all apply withfailed=0. See Testing On A VM. - vault-password-file plumbing is wired end to end (bootstrap script,
ansible-pull timer,
ansible.cfg) and proven on the VM. - workstation stays CLI/TUI only for now. GUI/Omarchy config is intentionally out of scope (the old Betterbird/Thunderbird automation was removed as stale).
- dotfiles:
- implement kids role
- find inspiration in jaylacroix's code and eventually omakub's code or Jeff Geerling's code to improve the whole.
- Use ansible-pull (sudo ansible-pull -U https://github.com/djspatule/ansible-autoconfig.git --vault-password-file ~/secret.txt -C main) to automate the set-up of any of my family’s computer if they ever have to be reinstalled from scratch (on the same or different hardware).
- The set-up must be idem-potent so that it can be set to run automatically once a day on each of these machine (via a crontab job or else)
- I need to be able to “test” on a virtual machine.
My dotfiles are currently specific to Omarchy and managed with stow (https://github.com/djspatule/omarchy-dotfiles)... I want to transition from the dotfiles specific repo to this repo and have stow managed by ansible as part of the setup. Maintenance of the files need to be feasible and easy.
- Use this for inspiration of structuration, etc. https://github.com/LearnLinuxTV/personal_ansible_desktop_configs/tree/main
- I’m writing this code also as an opportunity to learn about ansible, GNU/linux, neovim, opencode and other coding agents (and vibecoding in general), tmux, git, networking, etc. Thus explain all the code, design choices, structure, etc in that perspective...
- I would ideally like to lay here the foundations of an auto-config ansible setup that will be still valid and usable in 20 years….(comment your code profusely in that regard)
- The short term priority is to work on the server but the rest will be addressed as well (kids computer, adult's workstations, etc.).
- Server (Serverannah) destined to become a homelab that runs Ubuntu server 25.10 (headless Optiplex 3070, accessed via SSH) : It is running both natively (on bare metal) an Nginx server to host 3 websites (including a complex wordpress) and more to come. It's also DNS Filtering with pi-hole (installed on bare metal). last, it's running multiple docker services such as frigate for cameras, N8N, Odoo, bentopdf, Plex, Timeshift (to secure backups on 1 master and 3 different external disks copies), Nextcloud (to manage my data and make it accessible to the whole family) and the like. For now, those functions are managed on a raspi 4B (192.168.1.100) that should be retired soon. You don’t have to keep on serverannah things as they were on the raspi. It’s a good opportunity to “improve”.
- Kids are running Linux Mint (xfce but switch to cinnamon is envisaged) on a very old macbook. Their goal is gaming and learning. Parental control has to be strict
- “Mac” is my wife’s computer (not much to set-up there, she mostly works in a browser + GDrive and Spotify)
- Laptop is on Omarchy (hyprland) but could be switched to KDE or Gnome on Arch or even be switched to an Debian/Ubuntu distro
- ask whenever in doubt and ask for feedback (i can run commands for you, etc.)
- verify your work as much as possible
- remember as much as you can of these instructions and the other instructions to come
- Create a “guidelines.md” inside the directory to describe everything the project is about, how it works, terminal commands, etc. Templates on github….
- do not change code without explaining
- do not change multiples files at once when not strictly connected to the same taks/objective at hand
This repository is the family ansible-pull source of truth.
The active goal is simple:
- one main entry point:
local.yml - one common role:
roles/base - one server role:
roles/server - code that stays readable by a non-expert
- host-specific state stored in obvious places
- configuration changed in git first, not directly on the machine
- Prefer small Ansible modules over clever shell logic.
- Keep one source of truth per concern.
- Do not encode migration hacks for old machines unless they are still truly needed.
- If a value is host-specific, keep it in
host_vars/serverannah. - If a file is the real source of truth, store it in
files/and copy it.
local.yml: main playbookhosts: active inventorygroup_vars/all: tiny shared defaults onlyhost_vars/serverannah: server-specific switches, domains, URLs, and hardware pathsroles/base: packages, Starship shell init, dotfiles via Stow, ansible-pull automationroles/server: Docker services, reverse proxy, Pi-hole, Nextcloud, Frigate, fail2ban, storage bootstraproles/workstation: first CLI/TUI and dotfiles foundation for Arch/Omarchy workstation testingfiles/dotfiles/: Stow packages actually managed by this repofiles/serverannah/etc/fstab: source of truth forserverannahstorage mountsfiles/pihole/policy/: Pi-hole adlists and allowlist imported by Ansible
Run locally against the active inventory:
sudo ansible-playbook -i hosts --limit serverannah local.ymlRun with ansible-pull on the target machine:
sudo ansible-pull -U https://github.com/djspatule/ansible-autoconfig.git -C main -d /opt/ansible-pull local.ymlWhen testing on a VM or before public DNS/port forwarding is ready, do not ask
Caddy to obtain public HTTPS certificates yet. Keep the real serverannah vars
as the production target, and override only the staging network edge explicitly:
sudo ansible-pull -U https://github.com/djspatule/ansible-autoconfig.git -C main -d /opt/ansible-pull local.yml \
-e 'server_reverse_proxy_auto_https=false server_reverse_proxy_published_ports=["8081:80"] server_reverse_proxy_sites=[{"hostname":"homepage.localtest.me","upstream":"homepage:3000"},{"hostname":"bentopdf.localtest.me","upstream":"bentopdf:8080"},{"hostname":"game-timer.localtest.me","upstream":"game-timer:80"},{"hostname":"pihole.localtest.me","upstream":"pihole:80"}]'This keeps VM testing local and avoids confusing Let's Encrypt failures while the server is not publicly reachable.
Bootstrap a fresh server that does not have Ansible yet:
sudo ./scripts/bootstrap-server.shDry-run a local checkout when testing:
sudo ansible-playbook -i hosts --limit serverannah local.yml --checkThis is a public repo, so secrets are handled at two levels:
- Generated on the host, never committed. Service passwords (database
passwords, Nextcloud admin password, Dinnizer secret key base, etc.) are
generated on the target machine into
/etc/ansible/secrets/on first run and reused afterwards. They never enter git at all. This is the default and covers most services today. - Committed but encrypted with
ansible-vault. For files that genuinely have to live in the repo (for example SSH config or private keys, API tokens in dotfiles), encrypt them withansible-vaultbefore committing. The encrypted blob is safe in a public repo; only the password that unlocks it is secret.
The vault password itself is never committed. Supply it one of two ways:
# Per command:
sudo ansible-playbook -i hosts local.yml --vault-password-file ~/.ansible-vault-pass
# Or for every run: uncomment vault_password_file in ansible.cfg.The same --vault-password-file flows through scripts/bootstrap-server.sh
(AUTOCONFIG_VAULT_PASSWORD_FILE) and the managed autoconfig-pull timer
(autoconfig_vault_password_file), so a scheduled pull can decrypt vaulted
files unattended. Password-file names like ~/.ansible-vault-pass, secret.txt
and *.vault-pass are already in .gitignore.
Common commands:
ansible-vault encrypt files/dotfiles/ssh/.ssh/config # encrypt in place
ansible-vault edit files/dotfiles/ssh/.ssh/config # edit encrypted
ansible-vault view files/dotfiles/ssh/.ssh/config # read without decrypting to diskKeep
vault_password_filecommented inansible.cfguntil at least one encrypted file exists, otherwise runs fail looking for a password they do not need.
Linting is local-only. There is no GitHub Action and no cloud cost: everything runs on your machine.
-
scripts/lint.shrunsyamllint+ansible-lint. On first run it builds a throwaway virtualenv (.lint-venv/, gitignored) so nothing is installed system-wide. Run it any time:./scripts/lint.sh
-
A pre-commit hook is an optional convenience: a check that git runs automatically every time you
git commit, refusing the commit if linting fails (so mistakes never reach history). It is configured in.pre-commit-config.yamland is also fully local. One-time setup:pipx install pre-commit # or: pip install --user pre-commit pre-commit install # installs the hook into .git/hooks pre-commit run --all-files # optional: lint everything now
Config lives in .yamllint (lenient: long lines warn, real booleans only) and
.ansible-lint (basic profile; the role-prefix naming rule is deliberately
skipped because this repo names variables by concern, e.g. nextcloud_,
pihole_, not server_nextcloud_). The remaining findings are cosmetic (long
lines, a few unnamed import_tasks) and can be cleaned up over time.
The workstation role is proven on a disposable Arch/Omarchy VM (savannarchome)
before touching a real laptop. The VM configures itself with ansible-pull,
exactly like a real new machine would, so no host-to-guest SSH or
port-forwarding is needed.
GNOME Boxes uses QEMU user-mode networking (the guest gets 10.0.2.15, which
the host cannot reach), so the self-pull model is not just convenient, it is the
only simple option. Three lines inside the VM:
# 1. ansible-pull targets the inventory host whose name matches the machine's
# hostname, so the VM must call itself savannarchome.
sudo hostnamectl set-hostname savannarchome
# 2. The vault password (same one used on the laptop) so config.local can decrypt.
printf '%s' 'YOUR_VAULT_PASSWORD' > ~/secret.txt && chmod 600 ~/secret.txt
# 3. Fetch and run the bootstrap: installs git+ansible, clones, installs
# collections, runs ansible-pull for base + workstation.
curl -fsSL https://raw.githubusercontent.com/djspatule/ansible-autoconfig/main/scripts/bootstrap-server.sh \
| sudo env AUTOCONFIG_VAULT_PASSWORD_FILE=/home/lion/secret.txt shA healthy first run ends with failed=0 and shows, among others, the
Deploy vault-encrypted dotfiles (decrypted into place) task as changed (a
wrong vault password would fail there). Reset or snapshot the VM freely between
runs; the playbook is idempotent.
Live access from the host (for fast iteration): the self-pull above is
enough to validate a change, but to drive --check runs against the VM from the
host (or just poke around), a reverse SSH tunnel avoids fighting GNOME Boxes'
NAT. GNOME Boxes user-mode networking reaches the host at 10.0.2.2, so run
one command in the guest to dial out and forward host port 2222 back to the
guest's sshd, then one on the host to install your key through it:
# In the GUEST: hold open a reverse tunnel (host:2222 -> guest:22).
ssh -fN -R 2222:127.0.0.1:22 lion@10.0.2.2
# On the HOST: authorize your key over that tunnel.
ssh-copy-id -p 2222 lion@127.0.0.1After that, ssh -p 2222 lion@127.0.0.1 works, and because
host_vars/savannarchome already points at 127.0.0.1:2222, so does
ansible -i hosts savannarchome -m ping. The tunnel lasts until the VM reboots
or that ssh process is killed; wrap it in autossh + a systemd user unit if
you want it to persist.
Clipboard with GNOME Boxes (Hyprland guest): install spice-vdagent and
wl-clipboard, enable spice-vdagentd, and add exec-once = spice-vdagent
(without -x) to the Hyprland config so the client starts inside the Wayland
session. Sync is then automatic (normal copy/paste, no special shortcut). If the
client comes up in X11 mode (spice-vdagent -x), it cannot read the Wayland
clipboard; pkill -x spice-vdagent then re-run spice-vdagent from a terminal
in the live session.
requirements.yml is not just documentation.
It is the executable dependency manifest used by the bootstrap script and the
managed autoconfig-pull helper to install required collections such as:
community.generalcommunity.dockeransible.posix
Keeping that in code is simpler and safer than hoping the README stays in sync.
Very little, by design.
It now only holds shared defaults that are still useful for the active code:
desktop_useransible_python_interpreter
Anything more specific belongs in host vars or role defaults.
Storage was simplified on purpose, but Ansible does not replace the whole system
/etc/fstab anymore. The OS installer should keep owning /, /boot, EFI and
swap because their UUIDs are created during installation.
The repo ships only the data-disk entries:
- source:
files/serverannah/etc/fstab - deployed into: a marked Ansible block inside
/etc/fstab
The server role then:
- inserts or updates the marked data-mount block
- creates the mount points declared in that source file
- runs
mount -a
This keeps data mounts versioned without hard-coding boot/root UUIDs that belong to one specific installation.
Backups are Borg (see roles/server/tasks/backup.yml): a daily systemd timer
dumps the databases and creates an archive of the /opt/* service dirs into an
encrypted repo on the backup disk.
- Repo:
/mnt/back-up/borg/serverannah(encryptionrepokey— the key lives in the repo, unlocked by the passphrase, so the repo + passphrase are all you need to restore anywhere). - Passphrase:
/etc/ansible/secrets/borg-passphrase, generated on the host, never in this repo. Losing it means losing every archive — back it up separately (a password manager).
The restore path has been tested on a fresh Ubuntu VM (a full archive
extracts cleanly: every file decrypts, DB dumps intact). To restore on a new
machine you only need borgbackup, the repo (copy the disk, or stream it), and
the passphrase:
export BORG_PASSPHRASE='…'
borg list /path/to/repo # pick an archive
borg extract /path/to/repo::serverannah-<ts> # restores ./opt/... and ./var/...borg's standalone binary (GitHub releases, borg-linux-glibc236) needs no root,
so a restore works even on a machine where you cannot install packages. Match
the binary's major version to the one that wrote the repo (currently 1.4.x).
Dotfiles are handled with:
- Ansible as orchestrator
- Stow as symlink engine for plaintext configs
ansible-vault+ a copy-decrypt step for the few configs that contain secrets
Plaintext packages live under files/dotfiles/<package>/ mirroring the home
layout. The exact set applied to a machine is whatever its
dotfiles_stow_packages lists (see the host's host_vars/ file) — that list is
the single source of truth, so this README does not re-enumerate it. Every
package directory there is symlinked into $HOME by Stow.
Stow runs with --no-folding on purpose. By default Stow "folds" a config
directory that does not yet exist into a single directory symlink pointing into
the repo (e.g. ~/.config/gh -> repo/.../gh). Because our repo is a public
checkout managed in place, any file an app then writes into that dir (gh's
hosts.yml token, ssh known_hosts, nvim lock files) would be written straight
into the git tree. --no-folding keeps every directory a real local dir and
symlinks only the curated files, so app-written files stay local. The pre-stow
"preserve" step is directory-aware to match (it leaves an already-managed dir
alone instead of rebuilding it every run, which keeps pulls idempotent).
Do not vendor machine-specific symlinks. Omarchy points some configs at the
active theme (for example nvim lua/plugins/theme.lua or btop theme files) via
absolute symlinks into its local theme state. Those are gitignored / left out;
omarchy recreates them locally, and Stow --no-folding would (correctly) refuse
them.
Note two deliberate boundaries:
ghships onlyconfig.yml.hosts.ymlholds a GitHub OAuth token and is not committed, even encrypted — letgh auth loginrecreate it per host.- the
sshpackage ships only the sanitized public config, which does nothing butInclude ~/.ssh/config.local. The real host aliases live in that local file.
Secret dotfiles (currently ~/.ssh/config.local) are stored vault-encrypted
under files/dotfiles-secret/ and listed in dotfiles_secret_files. They
cannot be Stow-symlinked, because the symlink would hand the application the
ciphertext; instead Ansible copies them into place and decrypts on the way (see
Secrets And Vault). To update one:
ansible-vault edit --vault-password-file ~/secret.txt files/dotfiles-secret/ssh/config.localEditing a managed dotfile. After a pull the live file (e.g.
~/.config/espanso/match/base.yml) is a symlink into /opt/ansible-pull, the
root-owned pull checkout — so editing it in place fails as read-only
(E166 in nvim). That is by design. Always edit the source in your dev
clone, then push and pull:
nvim ~/Documents/ansible-autoconfig/files/dotfiles/<pkg>/<path>
cd ~/Documents/ansible-autoconfig && git commit -am "..." && git push
sudo ansible-pull -d /opt/ansible-pull -U https://github.com/djspatule/ansible-autoconfig.git -i hosts local.yml --vault-password-file ~/secret.txt(Never sudo-edit the /opt/ansible-pull copy: it is untracked and the next
pull overwrites it.)
Espanso is managed here too (package espanso, config under
files/dotfiles/espanso/). The binary is a special case: it is not in the
Arch official repos, so the workstation role builds espanso-wayland from the
AUR with yay, as the desktop user, guarded so it only compiles when the
binary is missing (a one-time cost on a fresh machine). Because the AUR helper
calls pacman, an unattended from-scratch build needs the desktop user to
sudo without a prompt; on a single-user Omarchy box that is the norm.
Otherwise the build is the one step that needs a hand — run yay -S espanso-wayland once and the next pull skips it. (secret.txt is the vault
password, not a sudo password, and would not help here anyway — the prompt comes
from yay's internal pacman call, not from Ansible's become.)
GUI/Omarchy desktop config beyond the migrated packages (parts of Hyprland, theme-specific bits) stays out for now; the workstation role is otherwise CLI/TUI focused.
Pi-hole policy import now reads from:
files/pihole/policy/adlists_enabled.txtfiles/pihole/policy/allowlist_exact.txt
Raw backups are not meant to be the long-term source of truth.
serverannah is the current production target.
Important host-specific values live in host_vars/serverannah, including:
- enabled services
- public domains
- Pi-hole mode
- hardware-specific device paths
- backup source paths still needed for migration
- package lists should stay in role defaults
- hostnames, domains, and paths should stay in host vars
- static config files should go under
files/ - comments should explain why a choice exists, not narrate every line
- the old raspi still matters as migration knowledge, but should not dictate the final design
- public DNS and NAT still need deliberate validation before full production cutover
ansible-pullmay print a hostname-pattern warning during its internal git step; the actual playbook result matters more than that warning
