Skip to content

Adding a new server

This guide walks through the full process of provisioning a new server from scratch.

Overview

1. Add host to inventory
2. Create host variables (plain + vault)
3. Bootstrap from root
4. Apply full configuration
5. (Optional) Enable Zero Trust lockdown

Step 1 – Add the host to the inventory

Edit inventories/prod/hosts.yml and add the host under the appropriate group:

# Web server example
web:
  hosts:
    mysite-web-01:
      ansible_host: mysite-e01.subscribed.build

# Database server example
db:
  hosts:
    mysite-db-01:
      ansible_host: mysite-a01.subscribed.build

Step 2 – Create host variables

Create a directory for the host under host_vars/:

mkdir -p inventories/prod/host_vars/mysite-web-01

The easiest approach is to copy main.yml from a similar existing host of the same type and adjust it.

main.yml – non-sensitive variables

# inventories/prod/host_vars/mysite-web-01/main.yml

sites:
  - name: mysite
    domain: mysite.dk
    user: mysite
    project_path: /home/mysite/mysite-dk-bedrock
    cronjob:
      dir: mysite-dk
      enable:
        - name: as-global
        - name: wp-cron
        - name: as-payments-hub-15
        - name: as-payments-hub-30
        - name: as-payments-hub-45

system:
  users:
    - name: mysite
      sudo: true
      ssh:
        generation: true
        authorized_key: true
        connect: true

cloudflare:
  zero_trust:
    slug_web: mysite-e01
    application:
      public_hostnames:
        - subdomain: cms
          domain: mysite.dk
          path: /wp/wp-graphql
      private_hostnames:
        - subdomain: cms
          domain: mysite.dk
          path: '*'

vault.yml – encrypted secrets

Create and encrypt the vault file:

ansible-vault create inventories/prod/host_vars/mysite-web-01/vault.yml

Add the required secrets (the exact keys depend on the host type):

# Web host vault example
vault_cloudflared_tunnel_token: ""

# DB host vault example
vault_mariadb_root_password: "a-strong-password"
sqlbak_db_user: "sqlbak"
sqlbak_db_password: "another-strong-password"
sqlbak_install_script_url: "https://..."
sqlbak_install_script_sha256: "sha256hash"

See inventories/prod/group_vars/db.vault.yml.example and web.vault.yml.example for the full list of required keys per server type.


Step 3 – Bootstrap from root

When a cloud provider gives you fresh root access, run the root bootstrap playbook. This creates the pt and sd operator accounts, installs their SSH keys, and configures passwordless sudo.

ansible-playbook -i inventories/prod/hosts.yml bootstrap_root.yml --limit mysite-web-01

After this runs, you should be able to SSH as your own user:

ssh mysite-web-01

Step 4 – Apply full configuration

Web server

# Option A: full web playbook (recommended)
ansible-playbook -i inventories/prod/hosts.yml web.yml --limit mysite-web-01

# Option B: bootstrap first (Docker + cloudflared), then full config
ansible-playbook -i inventories/prod/hosts.yml bootstrap_web.yml --limit mysite-web-01
ansible-playbook -i inventories/prod/hosts.yml web.yml --limit mysite-web-01

Database server

ansible-playbook -i inventories/prod/hosts.yml bootstrap_db.yml --limit mysite-db-01
ansible-playbook -i inventories/prod/hosts.yml db.yml --limit mysite-db-01

Step 5 – Enable Zero Trust lockdown (optional)

If the server should only be accessible via Cloudflare Tunnel (no public SSH):

  1. Verify cloudflared is running and the tunnel is healthy:
ssh mysite-web-01 -- systemctl status cloudflared
  1. Apply lockdown:
ansible-playbook -i inventories/prod/hosts.yml lockdown.yml --limit mysite-web-01

This closes all inbound UFW ports and binds SSH to localhost. After this, you can only reach the server through cloudflared access ssh.

  1. Update main.yml to reflect the locked-down state:
# inventories/prod/host_vars/mysite-web-01/main.yml
zt_lockdown_enabled: true
  1. Update your SSH config to use the proxy:
# ~/.ssh/config
Host mysite-e01.subscribed.build
  User pt
  IdentityFile ~/.ssh/id_ed25519
  ProxyCommand cloudflared access ssh --hostname %h

Idempotency check

After the initial provisioning, run the playbook a second time and confirm no changes are applied:

ansible-playbook -i inventories/prod/hosts.yml web.yml --limit mysite-web-01
# Expected: changed=0

If you see unexpected changes on the second run, investigate before pushing the configuration to other hosts.