Ansible
Ansible bootstraps a fresh server with everything needed to run containerized services. It is idempotent — safe to re-run at any time.
Running playbooks
bin/playbook bootstrap
# or
make bootstrap
The bin/playbook wrapper handles SSH agent setup automatically via confit ssh, using the Terraform-generated private key.
Pass additional Ansible arguments after the playbook name:
bin/playbook bootstrap --tags users
bin/playbook bootstrap --tags deps,firewall
Bootstrap playbook
ansible/playbooks/bootstrap.yml is the main (and currently only) playbook. It runs three task groups:
Dependencies (--tags deps)
Installs base packages, Docker CE, and cloudflared:
- System packages:
curl,gnupg,ufw,tmux,git - Docker CE with buildx plugin
- Cloudflared (Cloudflare Tunnel client)
Users (--tags users)
Creates system users and configures SSH:
- Creates the admin user with passwordless sudo and docker group membership
- Creates all other users (from
project.usersin confit) with docker group access - Installs the Terraform-generated SSH public key for every user
- Hardens
sshd_config: disables root login and password auth, sets a centralizedAuthorizedKeysFilepath
Firewall (--tags firewall)
Configures UFW:
- Rate-limits SSH (port 22)
- Allows HTTP (80) and HTTPS (443)
- Allows high ports (32768–65535) for container networking
- Default deny incoming, allow outgoing
How config is resolved
The playbook self-serves its variables from confit using Ansible’s lookup('pipe', ...):
vars:
admin_user: "{{ lookup('pipe', 'confit resolve project.admin_user') }}"
users: "{{ lookup('pipe', 'confit resolve project.users') }}"
ssh_public_key: "{{ lookup('pipe', 'confit --set stage=production resolve credentials.ssh.public_key') }}"
No inventory file is needed — the server IP is resolved at runtime and passed as a comma-delimited host list.