Files
server_cloud_ladkau_de/docs/runbook.md
T
ml a462ff1729 Fix provisioning errors found during first real run
- ansible.cfg: suppress Python interpreter discovery warning
- ansible/inventory.ini: add ansible_host; server IP is now defined in
  one place and read dynamically by run-bootstrap.sh
- group_vars/all/ → directory layout so vault.yml is auto-loaded by Ansible
  (previously vault.yml did not match any group name and was silently ignored)
- scripts/run-bootstrap.sh: read server IP from inventory, chmod 600 private
  keys automatically, show actual SSH error when root login fails
- scripts/bootstrap-deploy-user.sh: add sudoers.d entry for passwordless sudo
  (deploy user has no password so sudo group membership alone was not enough)
- nextcloud: fix Redis healthcheck (CMD-SHELL pipe was unreliable in Alpine,
  switched to CMD form with start_period)
- group_vars/all/vars.yml: fix Roundcube image tag (1.6-apache and 1.6 do not
  exist; correct tag is 1.6.x-apache)
- docs/runbook.md: expand prerequisites (SSH keys section), add step 1 for
  setting the server IP, expand bootstrap step with preflight detail, fix
  step numbering
2026-06-28 05:53:29 +02:00

6.9 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)
  • htpasswd available (apt install apache2-utils or brew install httpd)
  • docker available (for generating the registry htpasswd entry)
  • openssl available (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 SSH access is available before running anything. The server IP is defined in ansible/inventory.ini (ansible_host).

Steps

1. Set the server IP

Open ansible/inventory.ini and set ansible_host to the server's public IP:

cloud.ladkau.de ansible_host=<server-ip> ansible_user=deploy ansible_ssh_private_key_file=keys/notroot_cloud_ladkau_de

This is the only place the IP needs to be set — the bootstrap script and all Ansible roles read it from here.

2. Install Ansible collections

From the repo root:

ansible-galaxy collection install -r ansible/requirements.yml

Required collections:

  • community.general — ufw, timezone modules
  • ansible.posix — authorized_key module
  • community.docker — docker_network, docker_compose_v2 modules

3. Configure DNS

Ensure the following DNS A records all point to the server IP (ansible_host in ansible/inventory.ini) 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 → server IP
gitea.ladkau.de A → server IP
nextcloud.ladkau.de A → server IP
sso.ladkau.de A → server IP
mail.ladkau.de A → server IP
cr.ladkau.de A → server IP
k8s.ladkau.de A → server IP

4. Create the vault and populate secrets

Create ansible/group_vars/all/vault.yml (gitignored) and encrypt it with Ansible Vault:

ansible-vault create ansible/group_vars/all/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/all/vault.yml

5. Bootstrap the deploy user

bash scripts/run-bootstrap.sh

The script runs the following steps in order:

Preflight checks (local)

  1. Verifies all four key files exist under keys/ (both root and deploy key pairs)
  2. Verifies scripts/bootstrap-deploy-user.sh exists
  3. Sets chmod 600 on the private key files (SSH refuses keys with open permissions)
  4. Runs scripts/check-vault.sh — decrypts the vault and confirms all 11 required secrets are present and non-empty

Remote actions 5. Opens a test SSH connection as root to confirm the root key works 6. Copies bootstrap-deploy-user.sh to /root/ on the server via scp 7. Executes it as root — creates the deploy user, grants passwordless sudo, and installs keys/notroot_cloud_ladkau_de.pub as the only authorized key 8. Opens a test SSH connection as deploy to confirm the new user can log in

If any step fails the script exits immediately with a descriptive error message.

6. 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/all/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