- scripts/run-bootstrap.sh: copies and runs the deploy user bootstrap on a fresh server, verifies SSH login, checks vault as a preflight step - scripts/bootstrap-deploy-user.sh: runs on the server as root, accepts the SSH public key as an argument (read from keys/ by run-bootstrap.sh) - scripts/check-vault.sh: decrypts vault.yml and verifies all 11 required secrets are present and non-empty - docs/runbook.md: restructured into correct order (collections → DNS → vault → bootstrap → playbook), added scripts reference table, moved first-run notes for Gitea/Keycloak/registry into their own section - README.md: added scripts/ to repo layout
5.7 KiB
Provisioning Runbook
Follow these steps to provision the server from scratch — whether setting it up for the first time or reinstalling after a wipe.
Prerequisites
Local tools
- Ansible installed (
pip install ansible) htpasswdavailable (apt install apache2-utilsorbrew install httpd)dockeravailable (for generating the registry htpasswd entry)opensslavailable (for generating secrets)
SSH keys
Two SSH key pairs are required. Place them in the keys/ directory (private keys
are gitignored; public keys are committed).
| File | Purpose |
|---|---|
keys/root_cloud_ladkau_de |
Root access — used only during initial bootstrap |
keys/root_cloud_ladkau_de.pub |
Public counterpart |
keys/notroot_cloud_ladkau_de |
Deploy user — used by Ansible for all subsequent runs |
keys/notroot_cloud_ladkau_de.pub |
Public counterpart — installed on server by bootstrap |
Generate fresh key pairs if they do not already exist:
ssh-keygen -t ed25519 -f keys/root_cloud_ladkau_de -C "root@cloud.ladkau.de" -N ""
ssh-keygen -t ed25519 -f keys/notroot_cloud_ladkau_de -C "deploy@cloud.ladkau.de" -N ""
Upload the root public key to the server via the Strato control panel (or paste
it during the initial OS install) so that root@217.154.207.148 is accessible
before running anything.
Steps
1. Install Ansible collections
From the repo root:
ansible-galaxy collection install -r ansible/requirements.yml
Required collections:
community.general— ufw, timezone modulesansible.posix— authorized_key modulecommunity.docker— docker_network, docker_compose_v2 modules
2. Configure DNS
Ensure the following DNS A records point to 217.154.207.148 before running
the playbook. Traefik requests Let's Encrypt certificates on first start and
DNS must resolve at that point.
| Domain | Record |
|---|---|
| cloud.ladkau.de | A → 217.154.207.148 |
| gitea.ladkau.de | A → 217.154.207.148 |
| nextcloud.ladkau.de | A → 217.154.207.148 |
| sso.ladkau.de | A → 217.154.207.148 |
| mail.ladkau.de | A → 217.154.207.148 |
| cr.ladkau.de | A → 217.154.207.148 |
| k8s.ladkau.de | A → 217.154.207.148 |
3. Create the vault and populate secrets
Create ansible/group_vars/vault.yml (gitignored) and encrypt it with Ansible Vault:
ansible-vault create ansible/group_vars/vault.yml
Populate all required secrets:
# Traefik dashboard basic auth
# Generate: echo $(htpasswd -nB admin) | sed -e 's/\$/\$\$/g'
traefik_dashboard_users: "admin:$$2y$$05$$..."
# Gitea
# Generate secrets: openssl rand -hex 32
gitea_db_password: ""
gitea_secret_key: ""
gitea_internal_token: ""
# Keycloak
keycloak_db_password: ""
keycloak_admin_password: ""
# Nextcloud
nextcloud_db_password: ""
nextcloud_admin_password: ""
# Roundcube
# des_key must be exactly 24 characters: openssl rand -hex 12
roundcube_db_password: ""
roundcube_des_key: ""
# Container registry
# Generate: docker run --entrypoint htpasswd httpd:2 -Bbn <user> <password>
registry_htpasswd: "user:$2y$05$..."
Verify all secrets are present and non-empty:
bash scripts/check-vault.sh
To edit the vault later:
ansible-vault edit ansible/group_vars/vault.yml
4. Bootstrap the deploy user
Copies the bootstrap script to the server, runs it as root, and verifies the
deploy user can log in. Also runs check-vault.sh as a preflight check.
bash scripts/run-bootstrap.sh
5. Run the Ansible master playbook
ansible-playbook -i ansible/inventory.ini ansible/site.yml --ask-vault-pass
This runs all roles in order:
| # | Role | What it does |
|---|---|---|
| 1 | base |
OS hardening, deploy user, SSH config, ufw firewall, fail2ban |
| 2 | docker |
Docker Engine + Compose plugin, shared Traefik network |
| 3 | traefik |
Reverse proxy, automatic TLS via Let's Encrypt |
| 4 | gitea |
Self-hosted Git with PostgreSQL |
| 5 | nextcloud |
File storage with PostgreSQL, Redis, cron sidecar |
| 6 | sso |
Keycloak single sign-on with PostgreSQL |
| 7 | mail |
Roundcube webmail client with PostgreSQL |
| 8 | registry |
Docker Registry v2 with htpasswd auth |
| 9 | k8s |
Placeholder page at k8s.ladkau.de |
To apply a single role:
ansible-playbook -i ansible/inventory.ini ansible/site.yml --tags <role> --ask-vault-pass
First-run notes
Gitea
gitea_disable_registration defaults to true. For the first deploy, override
it to false in vault.yml so the setup wizard can create the admin account:
ansible-vault edit ansible/group_vars/vault.yml
# add: gitea_disable_registration: false
ansible-playbook -i ansible/inventory.ini ansible/site.yml --tags gitea --ask-vault-pass
After the admin account is created at https://gitea.ladkau.de, remove the
override and redeploy to close public registration.
Keycloak
KEYCLOAK_ADMIN / KEYCLOAK_ADMIN_PASSWORD bootstrap the initial admin account
on first start only — Keycloak ignores them once the account exists. After
logging in at https://sso.ladkau.de, change the admin password via the UI.
Container registry
# Login
docker login cr.ladkau.de
# Push
docker tag myimage:latest cr.ladkau.de/myimage:latest
docker push cr.ladkau.de/myimage:latest
# Pull
docker pull cr.ladkau.de/myimage:latest
Scripts reference
| Script | Purpose |
|---|---|
scripts/run-bootstrap.sh |
Copy and run the deploy user bootstrap on a fresh server |
scripts/bootstrap-deploy-user.sh |
Runs on the server as root — creates the deploy user |
scripts/check-vault.sh |
Verify vault.yml exists and all required secrets are non-empty |