From e197ba73706234cc42b0089d1748ad6b0788d708 Mon Sep 17 00:00:00 2001 From: ml Date: Sun, 28 Jun 2026 07:05:43 +0200 Subject: [PATCH] Fix all issues found during first live provisioning run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Traefik: - Upgrade v3.3 → v3.6 to fix Docker API version negotiation failure with Docker Engine 29 (which dropped support for API < 1.40) Directory permissions: - Change all service parent directories from 0750 to 0755 so container processes can traverse them after dropping from root to a lower UID - Create db directories owned by postgres UID 999 (mode 0700) so PostgreSQL can access its data files across Ansible runs - Create /opt/gitea/data owned by git UID 1000 (Gitea writes there after dropping privileges) Healthchecks: - Fix Redis healthcheck: use CMD form instead of CMD-SHELL pipe (pipe was unreliable in Alpine) - Fix Keycloak healthcheck: use bash /dev/tcp on management port 9000 (curl not available in UBI image; was incorrectly targeting port 8080) Image tags: - Fix Roundcube: 1.6 and 1.6-apache do not exist; correct tag is 1.6.x-apache Bootstrap scripts: - bootstrap-deploy-user.sh: add sudoers.d entry for passwordless sudo (deploy user has no password so sudo group alone was not enough) - run-bootstrap.sh: read server IP from inventory.ini, chmod 600 keys automatically, show actual SSH error on failure Inventory / config: - Add ansible_host to inventory.ini — server IP now defined in one place - Restructure group_vars/ into all/ directory so vault.yml is auto-loaded (previously it did not match any group name) - Move ansible.cfg to repo root (Ansible looks in cwd, not playbook dir) Docs: - Split runbook.md into runbook-provisioning.md and runbook-configuration.md - Add step 1 (set server IP) to provisioning runbook - Expand prerequisites with SSH key generation and upload instructions - Expand bootstrap step with preflight check details --- README.md | 3 +- ansible/group_vars/all/vars.yml | 2 +- ansible/roles/gitea/tasks/main.yml | 29 ++++++-- ansible/roles/mail/tasks/main.yml | 12 +++- ansible/roles/nextcloud/tasks/main.yml | 12 +++- ansible/roles/registry/tasks/main.yml | 2 +- ansible/roles/sso/tasks/main.yml | 12 +++- .../roles/sso/templates/docker-compose.yml.j2 | 2 +- ansible/roles/traefik/tasks/main.yml | 2 +- docs/runbook-configuration.md | 68 +++++++++++++++++++ docs/{runbook.md => runbook-provisioning.md} | 39 +---------- 11 files changed, 129 insertions(+), 54 deletions(-) create mode 100644 docs/runbook-configuration.md rename docs/{runbook.md => runbook-provisioning.md} (86%) diff --git a/README.md b/README.md index 46ae141..04d96c3 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,8 @@ Repo layout: registry/ # container registry (cr.ladkau.de) k8s/ # k3s Kubernetes node docs/ - runbook.md # step-by-step reinstall instructions + runbook-provisioning.md # step-by-step reinstall instructions + runbook-configuration.md # first-run service configuration scripts/ run-bootstrap.sh # bootstrap deploy user on a fresh server bootstrap-deploy-user.sh # runs on server as root — creates deploy user diff --git a/ansible/group_vars/all/vars.yml b/ansible/group_vars/all/vars.yml index c9ce7b8..5046e02 100644 --- a/ansible/group_vars/all/vars.yml +++ b/ansible/group_vars/all/vars.yml @@ -22,7 +22,7 @@ timezone: Europe/Berlin traefik_network: traefik_public # Traefik -traefik_version: v3.3 +traefik_version: v3.6 traefik_data_dir: /opt/traefik # htpasswd-formatted user list for the dashboard. # Generate with: echo $(htpasswd -nB admin) | sed -e 's/\$/\$\$/g' diff --git a/ansible/roles/gitea/tasks/main.yml b/ansible/roles/gitea/tasks/main.yml index b636229..9608c32 100644 --- a/ansible/roles/gitea/tasks/main.yml +++ b/ansible/roles/gitea/tasks/main.yml @@ -1,15 +1,30 @@ --- -- name: Create Gitea data directories +- name: Create Gitea data directory ansible.builtin.file: - path: "{{ item }}" + path: "{{ gitea_data_dir }}" state: directory owner: root group: root - mode: "0750" - loop: - - "{{ gitea_data_dir }}" - - "{{ gitea_data_dir }}/data" - - "{{ gitea_data_dir }}/db" + mode: "0755" + tags: gitea + +# Gitea runs as git (UID 1000) and writes directly to this directory +- name: Create Gitea app data directory (git UID 1000) + ansible.builtin.file: + path: "{{ gitea_data_dir }}/data" + state: directory + owner: "1000" + group: "1000" + mode: "0755" + tags: gitea + +- name: Create Gitea database directory (postgres UID 999) + ansible.builtin.file: + path: "{{ gitea_data_dir }}/db" + state: directory + owner: "999" + group: "999" + mode: "0700" tags: gitea # Git-over-SSH runs on 2222 to avoid conflict with the host SSH on 22 diff --git a/ansible/roles/mail/tasks/main.yml b/ansible/roles/mail/tasks/main.yml index d3d01d5..6afae12 100644 --- a/ansible/roles/mail/tasks/main.yml +++ b/ansible/roles/mail/tasks/main.yml @@ -6,10 +6,18 @@ state: directory owner: root group: root - mode: "0750" + mode: "0755" loop: - "{{ mail_data_dir }}" - - "{{ mail_data_dir }}/db" + tags: mail + +- name: Create Roundcube database directory (postgres UID 999) + ansible.builtin.file: + path: "{{ mail_data_dir }}/db" + state: directory + owner: "999" + group: "999" + mode: "0700" tags: mail - name: Deploy Docker Compose file diff --git a/ansible/roles/nextcloud/tasks/main.yml b/ansible/roles/nextcloud/tasks/main.yml index 59ca274..4e58c87 100644 --- a/ansible/roles/nextcloud/tasks/main.yml +++ b/ansible/roles/nextcloud/tasks/main.yml @@ -5,14 +5,22 @@ state: directory owner: root group: root - mode: "0750" + mode: "0755" loop: - "{{ nextcloud_data_dir }}" - "{{ nextcloud_data_dir }}/html" - - "{{ nextcloud_data_dir }}/db" - "{{ nextcloud_data_dir }}/redis" tags: nextcloud +- name: Create Nextcloud database directory (postgres UID 999) + ansible.builtin.file: + path: "{{ nextcloud_data_dir }}/db" + state: directory + owner: "999" + group: "999" + mode: "0700" + tags: nextcloud + - name: Deploy Docker Compose file ansible.builtin.template: src: docker-compose.yml.j2 diff --git a/ansible/roles/registry/tasks/main.yml b/ansible/roles/registry/tasks/main.yml index 4f45d3c..b2dfbde 100644 --- a/ansible/roles/registry/tasks/main.yml +++ b/ansible/roles/registry/tasks/main.yml @@ -5,7 +5,7 @@ state: directory owner: root group: root - mode: "0750" + mode: "0755" loop: - "{{ registry_data_dir }}" - "{{ registry_data_dir }}/data" diff --git a/ansible/roles/sso/tasks/main.yml b/ansible/roles/sso/tasks/main.yml index 454fcb9..33c9720 100644 --- a/ansible/roles/sso/tasks/main.yml +++ b/ansible/roles/sso/tasks/main.yml @@ -5,10 +5,18 @@ state: directory owner: root group: root - mode: "0750" + mode: "0755" loop: - "{{ sso_data_dir }}" - - "{{ sso_data_dir }}/db" + tags: sso + +- name: Create Keycloak database directory (postgres UID 999) + ansible.builtin.file: + path: "{{ sso_data_dir }}/db" + state: directory + owner: "999" + group: "999" + mode: "0700" tags: sso - name: Deploy Docker Compose file diff --git a/ansible/roles/sso/templates/docker-compose.yml.j2 b/ansible/roles/sso/templates/docker-compose.yml.j2 index e6bda3a..8c581e2 100644 --- a/ansible/roles/sso/templates/docker-compose.yml.j2 +++ b/ansible/roles/sso/templates/docker-compose.yml.j2 @@ -48,7 +48,7 @@ services: - "traefik.http.routers.sso.tls.certresolver=letsencrypt" - "traefik.http.services.sso.loadbalancer.server.port=8080" healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8080/health/ready || exit 1"] + test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/localhost/9000"] interval: 30s timeout: 10s retries: 5 diff --git a/ansible/roles/traefik/tasks/main.yml b/ansible/roles/traefik/tasks/main.yml index 874fc49..8a8f675 100644 --- a/ansible/roles/traefik/tasks/main.yml +++ b/ansible/roles/traefik/tasks/main.yml @@ -5,7 +5,7 @@ state: directory owner: root group: root - mode: "0750" + mode: "0755" tags: traefik # acme.json must be 0600 or Traefik refuses to start diff --git a/docs/runbook-configuration.md b/docs/runbook-configuration.md new file mode 100644 index 0000000..516ddce --- /dev/null +++ b/docs/runbook-configuration.md @@ -0,0 +1,68 @@ +# Configuration Runbook + +First-run configuration steps to perform after the Ansible playbook has +provisioned the server. See `runbook-provisioning.md` for the provisioning steps. + +## Gitea + +`gitea_disable_registration` defaults to `true`. For the first deploy, override +it to `false` so the setup wizard can create the admin account: + +```bash +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. + +Keycloak takes ~90 seconds to start. If the login page is not immediately +available, wait and retry. + +## Nextcloud + +Nextcloud runs its first-time installation on the initial HTTP request, which +takes a minute or two. The admin credentials are set via `nextcloud_admin_user` +and `nextcloud_admin_password` in the vault. + +## Roundcube + +Roundcube is a webmail client — it does not host mail itself. Configure the IMAP +and SMTP servers it connects to via `ansible/group_vars/all/vars.yml`: + +```yaml +roundcube_imap_host: "ssl://mail.example.com" # implicit TLS (port 993) +roundcube_smtp_host: "mail.example.com" # STARTTLS (port 587) +``` + +Leave both empty to let users enter their own server at login. + +## Container registry + +```bash +# 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 +``` + +Registry credentials are managed via `registry_htpasswd` in the vault. To add +or rotate a user, regenerate the htpasswd entry and redeploy: + +```bash +docker run --entrypoint htpasswd httpd:2 -Bbn +# update registry_htpasswd in vault, then: +ansible-playbook -i ansible/inventory.ini ansible/site.yml --tags registry --ask-vault-pass +``` diff --git a/docs/runbook.md b/docs/runbook-provisioning.md similarity index 86% rename from docs/runbook.md rename to docs/runbook-provisioning.md index 7224c92..0aa9976 100644 --- a/docs/runbook.md +++ b/docs/runbook-provisioning.md @@ -3,6 +3,9 @@ Follow these steps to provision the server from scratch — whether setting it up for the first time or reinstalling after a wipe. +After provisioning completes, follow `runbook-configuration.md` for first-run +setup of individual services. + ## Prerequisites ### Local tools @@ -179,42 +182,6 @@ To apply a single role: ansible-playbook -i ansible/inventory.ini ansible/site.yml --tags --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: - -```bash -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 - -```bash -# 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 |