- Jinja 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .idea | ||
| files/keys | ||
| inventory/development | ||
| playbooks | ||
| roles | ||
| .gitignore | ||
| ansible.cfg | ||
| README.md | ||
| requirements.yml | ||
ansible_dev
Ansible playbooks for managing wieken.dev and future development infrastructure (webservers, dataservers, aiservers).
Layout
inventory/development/ Inventory for the dev environment
hosts.yml Groups: devservers, webservers, dataservers, aiservers
group_vars/all/ Vars applied to every host
vars.yml Plaintext vars (safe to diff in git)
group_vars/webservers/ Overrides for the webservers group (e.g. open 80/443)
group_vars/devservers/ Overrides for the devservers group (Forgejo ports/secrets)
vars.yml Plaintext vars (safe to diff in git)
vault.yml.example Template for secrets -> copy to vault.yml & encrypt
host_vars/ Per-host overrides
playbooks/
site.yml Runs everything below, in order
common.yml Baseline: common, users, ssh, firewall roles on `all`
devservers.yml Docker etc. for boxes used as dev environments
webservers.yml Group-specific plays (currently empty, add roles as built)
dataservers.yml
aiservers.yml
roles/
common/ Minimum toolset (sudo, nano, curl, git, ...)
users/ Create users/groups, deploy SSH keys, sudo config
ssh/ sshd hardening (no root login, no password auth, ...)
firewall/ ufw default-deny with explicitly allowed ports
docker/ Docker Engine + docker compose plugin (any group can use it)
caddy/ Shared reverse proxy + automatic Let's Encrypt TLS (devservers)
forgejo/ Forgejo + Postgres via docker compose, proxied by caddy
development/ Stub - not implemented yet (python, pixi, ...)
letsencrypt/ Stub - not implemented yet
files/keys/ Public SSH keys (safe to commit)
New roles just get dropped into roles/ and added to the relevant
group's role list (or playbooks/common.yml if it applies to every host).
Prerequisites: bootstrap the ansible control user
Ansible needs SSH access to a host before it can manage anything, so each
new Debian host needs a one-time manual bootstrap: create a dedicated
ansible user, grant it passwordless sudo (required for become to work
non-interactively), and install your control machine's public key. The same
key pair is reused for the ansible user across all hosts.
If you don't already have a dedicated key for this, generate one once:
ssh-keygen -t ed25519 -f ~/.ssh/ansible -C "ansible-control"
Log in to the target as root (or an existing sudo user) via whatever
initial access your provider gives you, and run:
adduser --disabled-password --gecos "" ansible
usermod -aG sudo ansible
echo 'ansible ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ansible
chmod 440 /etc/sudoers.d/ansible
mkdir -p /home/ansible/.ssh
chmod 700 /home/ansible/.ssh
chown ansible:ansible /home/ansible/.ssh
Then, from your local machine, install the public key into that user's
authorized_keys (run once per host, while you still have root access):
cat ~/.ssh/ansible.pub | ssh root@<host> \
"cat >> /home/ansible/.ssh/authorized_keys && \
chmod 600 /home/ansible/.ssh/authorized_keys && \
chown ansible:ansible /home/ansible/.ssh/authorized_keys"
Verify it works and sudo is passwordless:
ssh -i ~/.ssh/ansible ansible@<host> sudo whoami
# should print: root, with no password prompt
inventory/development/hosts.yml already points every host at
~/.ssh/ansible via ansible_ssh_private_key_file. If you generated the
key somewhere else, update that path (or override it per-host in
host_vars/<host>.yml).
This ansible account is intentionally separate from the human accounts
managed by roles/users below - it's bootstrapped once outside of Ansible
and not touched by playbook runs.
First-time setup
ansible-galaxy collection install -r requirements.yml
Secrets (ansible-vault)
Secrets are split from plaintext vars: each group_vars/<group>/vars.yml
references vault_-prefixed variables that live encrypted in that same
directory's vault.yml. Currently only devservers/ (Forgejo DB password)
has a vault file - group_vars/all/ has none since users_sudo_nopasswd: true means no password hashes are needed there right now.
cd inventory/development/group_vars/devservers
cp vault.yml.example vault.yml
ansible-vault encrypt vault.yml
Vault password file
ansible.cfg is already configured with vault_password_file = .vault_pass,
so every ansible-playbook/ansible-vault command auto-decrypts without
prompting - as long as that file exists. It's gitignored (never committed),
so each machine you run this from needs its own copy:
echo -n 'your-vault-password-here' > .vault_pass
chmod 600 .vault_pass
Without this file, you'll see [ERROR]: Attempting to decrypt but no vault secrets found - either create .vault_pass as above, or pass
--ask-vault-pass on every command instead.
Edit secrets later with:
ansible-vault edit inventory/development/group_vars/devservers/vault.yml
Users & SSH keys
Add accounts under users: in group_vars/all/vars.yml. Public keys go in
files/keys/<name>.pub (safe to commit); password hashes go in vault.yml.
Before running roles/ssh (which disables password auth), verify new
accounts can actually log in with their private key - keep your current
session open and, in a second terminal, test:
ssh <name>@<host>
Reverse proxy (Caddy, devservers)
roles/caddy deploys Caddy via docker compose
to /opt/caddy on every host in the devservers group. Caddy owns ports
80/443 exclusively and handles Let's Encrypt automatically (no
certbot/cron - certs are requested and renewed by Caddy itself).
Apps aren't proxied by editing Caddy's own config. Instead:
- The app's container joins the shared
proxydocker network (external, created by this role) in addition to its own private network(s). - The app's role templates its own
<name>.caddyfile intocaddy_sites_dir(/opt/caddy/sites/) with areverse_proxy <container_name>:<port>block, then runsdocker exec caddy caddy reload.roles/forgejois the reference implementation for this pattern - copy it for future services.
This means adding a new subdomain/app later never touches roles/caddy,
the firewall (only 80/443 are ever needed, for any number of apps), or
DNS beyond one new CNAME - it's fully self-contained in the new app's role.
Forgejo (devservers)
roles/forgejo deploys Forgejo + Postgres via docker compose to
/opt/forgejo on every host in the devservers group, proxied by Caddy
(see above). Requires vault_forgejo_db_password to be set (see Secrets
above).
- HTTPS:
https://git.<host>/- the subdomain (git) isforgejo_subdomaininroles/forgejo/defaults/main.yml; the domain isinventory_hostname(e.g.wieken.dev), so this resolves automatically with zero manual config. Point a CNAME (or A record) forgit.<host>at the server before running the playbook, or Let's Encrypt issuance fails. No host port is published for the web UI - it's only reachable through Caddy on the sharedproxynetwork. - Git-over-SSH: port
23(host) ->22(container), e.g.git clone ssh://git@<host>:23/<user>/<repo>.git- unaffected by Caddy, since git-SSH is a raw TCP protocol Caddy can't proxy. - Compose/env files are Ansible-managed. Every run ends with an
unconditional
docker compose up -d --remove-orphans, so the stack is reconciled to match the templates every time - config changes get applied, and containers that were manually stopped/removed get started again.restart: unless-stoppedadditionally covers container restarts across host reboots.
Running
# Full run
ansible-playbook playbooks/site.yml
# Just the security/users baseline
ansible-playbook playbooks/common.yml
# Just one host
ansible-playbook playbooks/site.yml --limit wieken.dev
# Dry run
ansible-playbook playbooks/site.yml --check --diff